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  }