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  }