github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/vfs/disk_full_test.go (about) 1 // Copyright 2021 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package vfs 6 7 import ( 8 "io" 9 "os" 10 "sync" 11 "sync/atomic" 12 "syscall" 13 "testing" 14 "time" 15 16 "github.com/cockroachdb/errors" 17 "github.com/stretchr/testify/require" 18 ) 19 20 var filesystemWriteOps = map[string]func(FS) error{ 21 "Create": func(fs FS) error { 22 _, err := fs.Create("foo") 23 return err 24 }, 25 "Lock": func(fs FS) error { 26 _, err := fs.Lock("foo") 27 return err 28 }, 29 "ReuseForWrite": func(fs FS) error { 30 _, err := fs.ReuseForWrite("foo", "bar") 31 return err 32 }, 33 "Link": func(fs FS) error { return fs.Link("foo", "bar") }, 34 "MkdirAll": func(fs FS) error { return fs.MkdirAll("foo", os.ModePerm) }, 35 "Remove": func(fs FS) error { return fs.Remove("foo") }, 36 "RemoveAll": func(fs FS) error { return fs.RemoveAll("foo") }, 37 "Rename": func(fs FS) error { return fs.Rename("foo", "bar") }, 38 } 39 40 func TestOnDiskFull_FS(t *testing.T) { 41 for name, fn := range filesystemWriteOps { 42 t.Run(name, func(t *testing.T) { 43 innerFS := &enospcMockFS{} 44 innerFS.enospcs.Store(1) 45 var callbackInvocations int 46 fs := OnDiskFull(innerFS, func() { 47 callbackInvocations++ 48 }) 49 50 // Call this vfs.FS method on the wrapped filesystem. The first 51 // call should return ENOSPC. Our registered callback should be 52 // invoked, then the method should be retried and return a nil 53 // error. 54 require.NoError(t, fn(fs)) 55 require.Equal(t, 1, callbackInvocations) 56 // The inner filesystem should be invoked twice because of the 57 // retry. 58 require.Equal(t, uint32(2), innerFS.invocations.Load()) 59 }) 60 } 61 } 62 63 func TestOnDiskFull_File(t *testing.T) { 64 t.Run("Write", func(t *testing.T) { 65 innerFS := &enospcMockFS{bytesWritten: 6} 66 var callbackInvocations int 67 fs := OnDiskFull(innerFS, func() { 68 callbackInvocations++ 69 }) 70 71 f, err := fs.Create("foo") 72 require.NoError(t, err) 73 74 // The next Write should ENOSPC. 75 innerFS.enospcs.Store(1) 76 77 // Call the Write method on the wrapped file. The first call should return 78 // ENOSPC, but also that six bytes were written. Our registered callback 79 // should be invoked, then Write should be retried and return a nil error 80 // and five bytes written. 81 n, err := f.Write([]byte("hello world")) 82 require.NoError(t, err) 83 require.Equal(t, 11, n) 84 require.Equal(t, 1, callbackInvocations) 85 // The inner filesystem should be invoked 3 times. Once during Create 86 // and twice during Write. 87 require.Equal(t, uint32(3), innerFS.invocations.Load()) 88 }) 89 t.Run("Sync", func(t *testing.T) { 90 innerFS := &enospcMockFS{bytesWritten: 6} 91 var callbackInvocations int 92 fs := OnDiskFull(innerFS, func() { 93 callbackInvocations++ 94 }) 95 96 f, err := fs.Create("foo") 97 require.NoError(t, err) 98 99 // The next Sync should ENOSPC. The callback should be invoked, but a 100 // Sync cannot be retried. 101 innerFS.enospcs.Store(1) 102 103 err = f.Sync() 104 require.Error(t, err) 105 require.Equal(t, 1, callbackInvocations) 106 // The inner filesystem should be invoked 2 times. Once during Create 107 // and once during Sync. 108 require.Equal(t, uint32(2), innerFS.invocations.Load()) 109 }) 110 } 111 112 func TestOnDiskFull_Concurrent(t *testing.T) { 113 innerFS := &enospcMockFS{ 114 opDelay: 10 * time.Millisecond, 115 } 116 innerFS.enospcs.Store(10) 117 var callbackInvocations atomic.Int32 118 fs := OnDiskFull(innerFS, func() { 119 callbackInvocations.Add(1) 120 }) 121 122 var wg sync.WaitGroup 123 for i := 0; i < 10; i++ { 124 wg.Add(1) 125 go func() { 126 defer wg.Done() 127 _, err := fs.Create("foo") 128 // They all should succeed on retry. 129 require.NoError(t, err) 130 }() 131 } 132 wg.Wait() 133 // Since all operations should start before the first one returns an 134 // ENOSPC, the callback should only be invoked once. 135 require.Equal(t, int32(1), callbackInvocations.Load()) 136 require.Equal(t, uint32(20), innerFS.invocations.Load()) 137 } 138 139 type enospcMockFS struct { 140 FS 141 opDelay time.Duration 142 bytesWritten int 143 enospcs atomic.Int32 144 invocations atomic.Uint32 145 } 146 147 func (fs *enospcMockFS) maybeENOSPC() error { 148 fs.invocations.Add(1) 149 v := fs.enospcs.Add(-1) 150 151 // Sleep before returning so that tests may issue concurrent writes that 152 // fall into the same write generation. 153 time.Sleep(fs.opDelay) 154 155 if v >= 0 { 156 // Wrap the error to test error unwrapping. 157 err := &os.PathError{Op: "mock", Path: "mock", Err: syscall.ENOSPC} 158 return errors.Wrap(err, "uh oh") 159 } 160 return nil 161 } 162 163 func (fs *enospcMockFS) Create(name string) (File, error) { 164 if err := fs.maybeENOSPC(); err != nil { 165 return nil, err 166 } 167 return &enospcMockFile{fs: fs}, nil 168 } 169 170 func (fs *enospcMockFS) Link(oldname, newname string) error { 171 if err := fs.maybeENOSPC(); err != nil { 172 return err 173 } 174 return nil 175 } 176 177 func (fs *enospcMockFS) Remove(name string) error { 178 if err := fs.maybeENOSPC(); err != nil { 179 return err 180 } 181 return nil 182 } 183 184 func (fs *enospcMockFS) RemoveAll(name string) error { 185 if err := fs.maybeENOSPC(); err != nil { 186 return err 187 } 188 return nil 189 } 190 191 func (fs *enospcMockFS) Rename(oldname, newname string) error { 192 if err := fs.maybeENOSPC(); err != nil { 193 return err 194 } 195 return nil 196 } 197 198 func (fs *enospcMockFS) ReuseForWrite(oldname, newname string) (File, error) { 199 if err := fs.maybeENOSPC(); err != nil { 200 return nil, err 201 } 202 return &enospcMockFile{fs: fs}, nil 203 } 204 205 func (fs *enospcMockFS) MkdirAll(dir string, perm os.FileMode) error { 206 if err := fs.maybeENOSPC(); err != nil { 207 return err 208 } 209 return nil 210 } 211 212 func (fs *enospcMockFS) Lock(name string) (io.Closer, error) { 213 if err := fs.maybeENOSPC(); err != nil { 214 return nil, err 215 } 216 return nil, nil 217 } 218 219 type enospcMockFile struct { 220 fs *enospcMockFS 221 File 222 } 223 224 func (f *enospcMockFile) Write(b []byte) (int, error) { 225 226 if err := f.fs.maybeENOSPC(); err != nil { 227 n := len(b) 228 if f.fs.bytesWritten < n { 229 n = f.fs.bytesWritten 230 } 231 return n, err 232 } 233 return len(b), nil 234 } 235 236 func (f *enospcMockFile) Sync() error { 237 return f.fs.maybeENOSPC() 238 } 239 240 // BenchmarkOnDiskFull benchmarks the overhead of the OnDiskFull filesystem 241 // wrapper during a Write when there is no ENOSPC. 242 func BenchmarkOnDiskFull(b *testing.B) { 243 fs := OnDiskFull(NewMem(), func() {}) 244 245 f, err := fs.Create("foo") 246 require.NoError(b, err) 247 defer func() { require.NoError(b, f.Close()) }() 248 249 payload := []byte("hello world") 250 for i := 0; i < b.N; i++ { 251 _, err := f.Write(payload) 252 require.NoError(b, err) 253 } 254 }