github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/vfs/disk_health_test.go (about) 1 // Copyright 2020 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/atomic" 11 "testing" 12 "time" 13 14 "github.com/cockroachdb/errors" 15 "github.com/stretchr/testify/require" 16 ) 17 18 type mockFile struct { 19 syncDuration time.Duration 20 } 21 22 func (m mockFile) Close() error { 23 return nil 24 } 25 26 func (m mockFile) Read(p []byte) (n int, err error) { 27 panic("unimplemented") 28 } 29 30 func (m mockFile) ReadAt(p []byte, off int64) (n int, err error) { 31 panic("unimplemented") 32 } 33 34 func (m mockFile) Write(p []byte) (n int, err error) { 35 time.Sleep(m.syncDuration) 36 return len(p), nil 37 } 38 39 func (m mockFile) Stat() (os.FileInfo, error) { 40 panic("unimplemented") 41 } 42 43 func (m mockFile) Sync() error { 44 time.Sleep(m.syncDuration) 45 return nil 46 } 47 48 var _ File = &mockFile{} 49 50 type mockFS struct { 51 create func(string) (File, error) 52 link func(string, string) error 53 list func(string) ([]string, error) 54 lock func(string) (io.Closer, error) 55 mkdirAll func(string, os.FileMode) error 56 open func(string, ...OpenOption) (File, error) 57 openDir func(string) (File, error) 58 pathBase func(string) string 59 pathJoin func(...string) string 60 pathDir func(string) string 61 remove func(string) error 62 removeAll func(string) error 63 rename func(string, string) error 64 reuseForWrite func(string, string) (File, error) 65 stat func(string) (os.FileInfo, error) 66 getDiskUsage func(string) (DiskUsage, error) 67 } 68 69 func (m mockFS) Create(name string) (File, error) { 70 if m.create == nil { 71 panic("unimplemented") 72 } 73 return m.create(name) 74 } 75 76 func (m mockFS) Link(oldname, newname string) error { 77 if m.link == nil { 78 panic("unimplemented") 79 } 80 return m.link(oldname, newname) 81 } 82 83 func (m mockFS) Open(name string, opts ...OpenOption) (File, error) { 84 if m.open == nil { 85 panic("unimplemented") 86 } 87 return m.open(name, opts...) 88 } 89 90 func (m mockFS) OpenDir(name string) (File, error) { 91 if m.openDir == nil { 92 panic("unimplemented") 93 } 94 return m.openDir(name) 95 } 96 97 func (m mockFS) Remove(name string) error { 98 if m.remove == nil { 99 panic("unimplemented") 100 } 101 return m.remove(name) 102 } 103 104 func (m mockFS) RemoveAll(name string) error { 105 if m.removeAll == nil { 106 panic("unimplemented") 107 } 108 return m.removeAll(name) 109 } 110 111 func (m mockFS) Rename(oldname, newname string) error { 112 if m.rename == nil { 113 panic("unimplemented") 114 } 115 return m.rename(oldname, newname) 116 } 117 118 func (m mockFS) ReuseForWrite(oldname, newname string) (File, error) { 119 if m.reuseForWrite == nil { 120 panic("unimplemented") 121 } 122 return m.reuseForWrite(oldname, newname) 123 } 124 125 func (m mockFS) MkdirAll(dir string, perm os.FileMode) error { 126 if m.mkdirAll == nil { 127 panic("unimplemented") 128 } 129 return m.mkdirAll(dir, perm) 130 } 131 132 func (m mockFS) Lock(name string) (io.Closer, error) { 133 if m.lock == nil { 134 panic("unimplemented") 135 } 136 return m.lock(name) 137 } 138 139 func (m mockFS) List(dir string) ([]string, error) { 140 if m.list == nil { 141 panic("unimplemented") 142 } 143 return m.list(dir) 144 } 145 146 func (m mockFS) Stat(name string) (os.FileInfo, error) { 147 if m.stat == nil { 148 panic("unimplemented") 149 } 150 return m.stat(name) 151 } 152 153 func (m mockFS) PathBase(path string) string { 154 if m.pathBase == nil { 155 panic("unimplemented") 156 } 157 return m.pathBase(path) 158 } 159 160 func (m mockFS) PathJoin(elem ...string) string { 161 if m.pathJoin == nil { 162 panic("unimplemented") 163 } 164 return m.pathJoin(elem...) 165 } 166 167 func (m mockFS) PathDir(path string) string { 168 if m.pathDir == nil { 169 panic("unimplemented") 170 } 171 return m.pathDir(path) 172 } 173 174 func (m mockFS) GetDiskUsage(path string) (DiskUsage, error) { 175 if m.getDiskUsage == nil { 176 panic("unimplemented") 177 } 178 return m.getDiskUsage(path) 179 } 180 181 var _ FS = &mockFS{} 182 183 func TestDiskHealthChecking_Sync(t *testing.T) { 184 diskSlow := make(chan time.Duration, 100) 185 slowThreshold := 1 * time.Second 186 mockFS := &mockFS{create: func(name string) (File, error) { 187 return mockFile{syncDuration: 3 * time.Second}, nil 188 }} 189 fs, closer := WithDiskHealthChecks(mockFS, slowThreshold, 190 func(s string, duration time.Duration) { 191 diskSlow <- duration 192 }) 193 defer closer.Close() 194 dhFile, _ := fs.Create("test") 195 defer dhFile.Close() 196 197 dhFile.Sync() 198 199 select { 200 case d := <-diskSlow: 201 if d.Seconds() < slowThreshold.Seconds() { 202 t.Fatalf("expected %0.1f to be greater than threshold %0.1f", d.Seconds(), slowThreshold.Seconds()) 203 } 204 case <-time.After(5 * time.Second): 205 t.Fatal("disk stall detector did not detect slow disk operation") 206 } 207 } 208 209 var ( 210 errInjected = errors.New("injected error") 211 ) 212 213 func filesystemOpsMockFS(sleepDur time.Duration) *mockFS { 214 return &mockFS{ 215 create: func(name string) (File, error) { 216 time.Sleep(sleepDur) 217 return nil, errInjected 218 }, 219 link: func(oldname, newname string) error { 220 time.Sleep(sleepDur) 221 return errInjected 222 }, 223 mkdirAll: func(string, os.FileMode) error { 224 time.Sleep(sleepDur) 225 return errInjected 226 }, 227 remove: func(name string) error { 228 time.Sleep(sleepDur) 229 return errInjected 230 }, 231 removeAll: func(name string) error { 232 time.Sleep(sleepDur) 233 return errInjected 234 }, 235 rename: func(oldname, newname string) error { 236 time.Sleep(sleepDur) 237 return errInjected 238 }, 239 reuseForWrite: func(oldname, newname string) (File, error) { 240 time.Sleep(sleepDur) 241 return nil, errInjected 242 }, 243 } 244 } 245 246 func stallFilesystemOperations(fs FS) map[string]func() { 247 return map[string]func(){ 248 "create": func() { _, _ = fs.Create("foo") }, 249 "link": func() { _ = fs.Link("foo", "bar") }, 250 "mkdir-all": func() { _ = fs.MkdirAll("foo", os.ModePerm) }, 251 "remove": func() { _ = fs.Remove("foo") }, 252 "remove-all": func() { _ = fs.RemoveAll("foo") }, 253 "rename": func() { _ = fs.Rename("foo", "bar") }, 254 "reuse-for-write": func() { _, _ = fs.ReuseForWrite("foo", "bar") }, 255 } 256 } 257 258 func TestDiskHealthChecking_Filesystem(t *testing.T) { 259 const sleepDur = 50 * time.Millisecond 260 const stallThreshold = 10 * time.Millisecond 261 262 // Wrap with disk-health checking, counting each stall on stallCount. 263 var stallCount uint64 264 fs, closer := WithDiskHealthChecks(filesystemOpsMockFS(sleepDur), stallThreshold, 265 func(name string, dur time.Duration) { 266 atomic.AddUint64(&stallCount, 1) 267 }) 268 defer closer.Close() 269 fs.(*diskHealthCheckingFS).tickInterval = 5 * time.Millisecond 270 ops := stallFilesystemOperations(fs) 271 for name, op := range ops { 272 t.Run(name, func(t *testing.T) { 273 before := atomic.LoadUint64(&stallCount) 274 op() 275 after := atomic.LoadUint64(&stallCount) 276 require.Greater(t, int(after-before), 0) 277 }) 278 } 279 } 280 281 // TestDiskHealthChecking_Filesystem_Close tests the behavior of repeatedly 282 // closing and reusing a filesystem wrapped by WithDiskHealthChecks. This is a 283 // permitted usage because it allows (*bitalostable.Options).EnsureDefaults to wrap 284 // with disk-health checking by default, and to clean up the long-running 285 // goroutine on (*bitalostable.DB).Close, while still allowing the FS to be used 286 // multiple times. 287 func TestDiskHealthChecking_Filesystem_Close(t *testing.T) { 288 const stallThreshold = 10 * time.Millisecond 289 mockFS := &mockFS{ 290 create: func(name string) (File, error) { 291 time.Sleep(50 * time.Millisecond) 292 return &mockFile{}, nil 293 }, 294 } 295 296 stalled := map[string]time.Duration{} 297 fs, closer := WithDiskHealthChecks(mockFS, stallThreshold, 298 func(name string, dur time.Duration) { stalled[name] = dur }) 299 fs.(*diskHealthCheckingFS).tickInterval = 5 * time.Millisecond 300 301 files := []string{"foo", "bar", "bax"} 302 for _, filename := range files { 303 // Create will stall, and the detector should write to the stalled map 304 // with the filename. 305 _, _ = fs.Create(filename) 306 // Invoke the closer. This will cause the long-running goroutine to 307 // exit, but the fs should still be usable and should still detect 308 // subsequent stalls on the next iteration. 309 require.NoError(t, closer.Close()) 310 require.Contains(t, stalled, filename) 311 } 312 }