github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/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.atomic.enospcs = 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), atomic.LoadUint32(&innerFS.atomic.invocations)) 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 atomic.StoreInt32(&innerFS.atomic.enospcs, 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), atomic.LoadUint32(&innerFS.atomic.invocations)) 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 atomic.StoreInt32(&innerFS.atomic.enospcs, 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), atomic.LoadUint32(&innerFS.atomic.invocations)) 109 }) 110 } 111 112 func TestOnDiskFull_Concurrent(t *testing.T) { 113 innerFS := &enospcMockFS{ 114 opDelay: 10 * time.Millisecond, 115 } 116 innerFS.atomic.enospcs = 10 117 var callbackInvocations int32 118 fs := OnDiskFull(innerFS, func() { 119 atomic.AddInt32(&callbackInvocations, 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), atomic.LoadInt32(&callbackInvocations)) 136 require.Equal(t, uint32(20), atomic.LoadUint32(&innerFS.atomic.invocations)) 137 } 138 139 type enospcMockFS struct { 140 FS 141 opDelay time.Duration 142 bytesWritten int 143 atomic struct { 144 enospcs int32 145 invocations uint32 146 } 147 } 148 149 func (fs *enospcMockFS) maybeENOSPC() error { 150 atomic.AddUint32(&fs.atomic.invocations, 1) 151 v := atomic.AddInt32(&fs.atomic.enospcs, -1) 152 153 // Sleep before returning so that tests may issue concurrent writes that 154 // fall into the same write generation. 155 time.Sleep(fs.opDelay) 156 157 if v >= 0 { 158 // Wrap the error to test error unwrapping. 159 err := &os.PathError{Op: "mock", Path: "mock", Err: syscall.ENOSPC} 160 return errors.Wrap(err, "uh oh") 161 } 162 return nil 163 } 164 165 func (fs *enospcMockFS) Create(name string) (File, error) { 166 if err := fs.maybeENOSPC(); err != nil { 167 return nil, err 168 } 169 return &enospcMockFile{fs: fs}, nil 170 } 171 172 func (fs *enospcMockFS) Link(oldname, newname string) error { 173 if err := fs.maybeENOSPC(); err != nil { 174 return err 175 } 176 return nil 177 } 178 179 func (fs *enospcMockFS) Remove(name string) error { 180 if err := fs.maybeENOSPC(); err != nil { 181 return err 182 } 183 return nil 184 } 185 186 func (fs *enospcMockFS) RemoveAll(name string) error { 187 if err := fs.maybeENOSPC(); err != nil { 188 return err 189 } 190 return nil 191 } 192 193 func (fs *enospcMockFS) Rename(oldname, newname string) error { 194 if err := fs.maybeENOSPC(); err != nil { 195 return err 196 } 197 return nil 198 } 199 200 func (fs *enospcMockFS) ReuseForWrite(oldname, newname string) (File, error) { 201 if err := fs.maybeENOSPC(); err != nil { 202 return nil, err 203 } 204 return &enospcMockFile{fs: fs}, nil 205 } 206 207 func (fs *enospcMockFS) MkdirAll(dir string, perm os.FileMode) error { 208 if err := fs.maybeENOSPC(); err != nil { 209 return err 210 } 211 return nil 212 } 213 214 func (fs *enospcMockFS) Lock(name string) (io.Closer, error) { 215 if err := fs.maybeENOSPC(); err != nil { 216 return nil, err 217 } 218 return nil, nil 219 } 220 221 type enospcMockFile struct { 222 fs *enospcMockFS 223 File 224 } 225 226 func (f *enospcMockFile) Write(b []byte) (int, error) { 227 228 if err := f.fs.maybeENOSPC(); err != nil { 229 n := len(b) 230 if f.fs.bytesWritten < n { 231 n = f.fs.bytesWritten 232 } 233 return n, err 234 } 235 return len(b), nil 236 } 237 238 func (f *enospcMockFile) Sync() error { 239 return f.fs.maybeENOSPC() 240 } 241 242 // BenchmarkOnDiskFull benchmarks the overhead of the OnDiskFull filesystem 243 // wrapper during a Write when there is no ENOSPC. 244 func BenchmarkOnDiskFull(b *testing.B) { 245 fs := OnDiskFull(NewMem(), func() {}) 246 247 f, err := fs.Create("foo") 248 require.NoError(b, err) 249 defer func() { require.NoError(b, f.Close()) }() 250 251 payload := []byte("hello world") 252 for i := 0; i < b.N; i++ { 253 _, err := f.Write(payload) 254 require.NoError(b, err) 255 } 256 }