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 }