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  }