github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/vfs/syncing_file_test.go (about)

     1  // Copyright 2019 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  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"testing"
    12  
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func TestSyncingFile(t *testing.T) {
    17  	const mb = 1 << 20
    18  
    19  	tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
    20  	require.NoError(t, err)
    21  
    22  	filename := tmpf.Name()
    23  	require.NoError(t, tmpf.Close())
    24  	defer os.Remove(filename)
    25  
    26  	f, err := Default.Create(filename)
    27  	require.NoError(t, err)
    28  
    29  	tf := &mockSyncToFile{File: f, canSyncTo: true}
    30  	sf := NewSyncingFile(tf, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */})
    31  	sf.(*syncingFile).fd = 1
    32  	testCases := []struct {
    33  		n              int64
    34  		expectedSyncTo int64
    35  	}{
    36  		{mb, -1},
    37  		{mb, mb},
    38  		{4 << 10, mb},
    39  		{4 << 10, mb + 8<<10},
    40  		{8 << 10, mb + 16<<10},
    41  		{16 << 10, mb + 32<<10},
    42  	}
    43  	for i, c := range testCases {
    44  		_, err := sf.Write(make([]byte, c.n))
    45  		require.NoError(t, err)
    46  
    47  		syncTo := sf.(*syncingFile).syncOffset.Load()
    48  		if c.expectedSyncTo != syncTo {
    49  			t.Fatalf("%d: expected sync to %d, but found %d", i, c.expectedSyncTo, syncTo)
    50  		}
    51  	}
    52  }
    53  
    54  func TestSyncingFileClose(t *testing.T) {
    55  	testCases := []struct {
    56  		canSyncTo bool
    57  		expected  string
    58  	}{
    59  		{true, `sync-to(1048576): test [false,<nil>]
    60  sync-to(2097152): test [false,<nil>]
    61  sync-to(3145728): test [false,<nil>]
    62  pre-close: test [offset=4194304 sync-offset=3145728]
    63  sync-data: test [<nil>]
    64  close: test [<nil>]
    65  `},
    66  		// When SyncTo is not being used, the last sync call ends up syncing all
    67  		// of the data causing syncingFile.Close to elide the sync.
    68  		{false, `sync-to(1048576): test [true,<nil>]
    69  sync-to(3145728): test [true,<nil>]
    70  pre-close: test [offset=4194304 sync-offset=4194304]
    71  close: test [<nil>]
    72  `},
    73  	}
    74  	for _, c := range testCases {
    75  		t.Run(fmt.Sprintf("canSyncTo=%t", c.canSyncTo), func(t *testing.T) {
    76  			tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
    77  			require.NoError(t, err)
    78  
    79  			filename := tmpf.Name()
    80  			require.NoError(t, tmpf.Close())
    81  			defer os.Remove(filename)
    82  
    83  			f, err := Default.Create(filename)
    84  			require.NoError(t, err)
    85  
    86  			var buf bytes.Buffer
    87  			tf := &mockSyncToFile{File: f, canSyncTo: c.canSyncTo}
    88  			lf := &vfsTestFSFile{tf, "test", &buf}
    89  			s := NewSyncingFile(lf, SyncingFileOptions{BytesPerSync: 8 << 10 /* 8 KB */}).(*syncingFile)
    90  
    91  			write := func(n int64) {
    92  				t.Helper()
    93  				_, err := s.Write(make([]byte, n))
    94  				require.NoError(t, err)
    95  			}
    96  
    97  			const mb = 1 << 20
    98  			write(2 * mb)
    99  			write(mb)
   100  			write(mb)
   101  
   102  			fmt.Fprintf(&buf, "pre-close: %s [offset=%d sync-offset=%d]\n",
   103  				lf.name, s.offset.Load(), s.syncOffset.Load())
   104  			require.NoError(t, s.Close())
   105  
   106  			if s := buf.String(); c.expected != s {
   107  				t.Fatalf("expected\n%s\nbut found\n%s", c.expected, s)
   108  			}
   109  		})
   110  	}
   111  }
   112  
   113  type mockSyncToFile struct {
   114  	File
   115  	canSyncTo bool
   116  }
   117  
   118  func (f *mockSyncToFile) SyncTo(length int64) (fullSync bool, err error) {
   119  	if !f.canSyncTo {
   120  		if err = f.File.SyncData(); err != nil {
   121  			return false, err
   122  		}
   123  		return true, nil
   124  	}
   125  	// f.canSyncTo = true
   126  	if _, err = f.File.SyncTo(length); err != nil {
   127  		return false, err
   128  	}
   129  	// NB: If the underlying file performed a full sync, lie.
   130  	return false, nil
   131  }
   132  
   133  func TestSyncingFileNoSyncOnClose(t *testing.T) {
   134  	testCases := []struct {
   135  		useSyncTo    bool
   136  		expectBefore int64
   137  		expectAfter  int64
   138  	}{
   139  		{false, 2 << 20, 3<<20 + 128},
   140  		{true, 2 << 20, 3<<20 + 128},
   141  	}
   142  
   143  	for _, c := range testCases {
   144  		t.Run(fmt.Sprintf("useSyncTo=%v", c.useSyncTo), func(t *testing.T) {
   145  			tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
   146  			require.NoError(t, err)
   147  
   148  			filename := tmpf.Name()
   149  			require.NoError(t, tmpf.Close())
   150  			defer os.Remove(filename)
   151  
   152  			f, err := Default.Create(filename)
   153  			require.NoError(t, err)
   154  
   155  			tf := &mockSyncToFile{f, c.useSyncTo}
   156  			s := NewSyncingFile(tf, SyncingFileOptions{NoSyncOnClose: true, BytesPerSync: 8 << 10}).(*syncingFile)
   157  
   158  			write := func(n int64) {
   159  				t.Helper()
   160  				_, err := s.Write(make([]byte, n))
   161  				require.NoError(t, err)
   162  			}
   163  
   164  			const mb = 1 << 20
   165  			write(2 * mb) // Sync first 2MB
   166  			write(mb)     // No sync because syncToOffset = 3M-1M = 2M
   167  			write(128)    // No sync for the same reason
   168  
   169  			syncToBefore := s.syncOffset.Load()
   170  			require.NoError(t, s.Close())
   171  			syncToAfter := s.syncOffset.Load()
   172  
   173  			// If we're not able to non-blockingly sync using sync-to,
   174  			// NoSyncOnClose should elide the sync.
   175  			if !c.useSyncTo {
   176  				if syncToBefore != c.expectBefore || syncToAfter != c.expectAfter {
   177  					t.Fatalf("Expected syncTo before and after closing are %d %d but found %d %d",
   178  						c.expectBefore, c.expectAfter, syncToBefore, syncToAfter)
   179  				}
   180  			}
   181  		})
   182  	}
   183  }
   184  
   185  func BenchmarkSyncWrite(b *testing.B) {
   186  	const targetSize = 16 << 20
   187  
   188  	var wsizes []int
   189  	if testing.Verbose() {
   190  		wsizes = []int{64, 512, 1 << 10, 2 << 10, 4 << 10, 8 << 10, 16 << 10, 32 << 10}
   191  	} else {
   192  		wsizes = []int{64}
   193  	}
   194  
   195  	run := func(b *testing.B, wsize int, newFile func(string) File) {
   196  		tmpf, err := os.CreateTemp("", "pebble-db-syncing-file-")
   197  		if err != nil {
   198  			b.Fatal(err)
   199  		}
   200  		filename := tmpf.Name()
   201  		_ = tmpf.Close()
   202  		defer os.Remove(filename)
   203  
   204  		var f File
   205  		var size int
   206  		buf := make([]byte, wsize)
   207  
   208  		b.SetBytes(int64(len(buf)))
   209  		b.ResetTimer()
   210  		for i := 0; i < b.N; i++ {
   211  			if f == nil {
   212  				b.StopTimer()
   213  				f = newFile(filename)
   214  				size = 0
   215  				b.StartTimer()
   216  			}
   217  			if _, err := f.Write(buf); err != nil {
   218  				b.Fatal(err)
   219  			}
   220  			if err := f.Sync(); err != nil {
   221  				b.Fatal(err)
   222  			}
   223  			size += len(buf)
   224  			if size >= targetSize {
   225  				_ = f.Close()
   226  				f = nil
   227  			}
   228  		}
   229  		b.StopTimer()
   230  	}
   231  
   232  	b.Run("no-prealloc", func(b *testing.B) {
   233  		for _, wsize := range wsizes {
   234  			b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) {
   235  				run(b, wsize, func(filename string) File {
   236  					_ = os.Remove(filename)
   237  					t, err := os.Create(filename)
   238  					if err != nil {
   239  						b.Fatal(err)
   240  					}
   241  					return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 0})
   242  				})
   243  			})
   244  		}
   245  	})
   246  
   247  	b.Run("prealloc-4MB", func(b *testing.B) {
   248  		for _, wsize := range wsizes {
   249  			b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) {
   250  				run(b, wsize, func(filename string) File {
   251  					_ = os.Remove(filename)
   252  					t, err := os.Create(filename)
   253  					if err != nil {
   254  						b.Fatal(err)
   255  					}
   256  					return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 4 << 20})
   257  				})
   258  			})
   259  		}
   260  	})
   261  
   262  	b.Run("reuse", func(b *testing.B) {
   263  		for _, wsize := range wsizes {
   264  			b.Run(fmt.Sprintf("wsize=%d", wsize), func(b *testing.B) {
   265  				init := true
   266  				run(b, wsize, func(filename string) File {
   267  					if init {
   268  						init = false
   269  
   270  						t, err := os.OpenFile(filename, os.O_RDWR, 0755)
   271  						if err != nil {
   272  							b.Fatal(err)
   273  						}
   274  						if _, err := t.Write(make([]byte, targetSize)); err != nil {
   275  							b.Fatal(err)
   276  						}
   277  						if err := t.Sync(); err != nil {
   278  							b.Fatal(err)
   279  						}
   280  						t.Close()
   281  					}
   282  
   283  					t, err := os.OpenFile(filename, os.O_RDWR, 0755)
   284  					if err != nil {
   285  						b.Fatal(err)
   286  					}
   287  					return NewSyncingFile(wrapOSFile(t), SyncingFileOptions{PreallocateSize: 0})
   288  				})
   289  			})
   290  		}
   291  	})
   292  }