github.com/cockroachdb/pebble@v1.1.2/objstorage/objstorageprovider/provider_test.go (about) 1 // Copyright 2023 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 objstorageprovider 6 7 import ( 8 "context" 9 "fmt" 10 "math/rand" 11 "strings" 12 "sync" 13 "testing" 14 15 "github.com/cockroachdb/datadriven" 16 "github.com/cockroachdb/pebble/internal/base" 17 "github.com/cockroachdb/pebble/objstorage" 18 "github.com/cockroachdb/pebble/objstorage/remote" 19 "github.com/cockroachdb/pebble/vfs" 20 "github.com/stretchr/testify/require" 21 ) 22 23 func TestProvider(t *testing.T) { 24 datadriven.Walk(t, "testdata/provider", func(t *testing.T, path string) { 25 var log base.InMemLogger 26 fs := vfs.WithLogging(vfs.NewMem(), func(fmt string, args ...interface{}) { 27 log.Infof("<local fs> "+fmt, args...) 28 }) 29 sharedStore := remote.WithLogging(remote.NewInMem(), func(fmt string, args ...interface{}) { 30 log.Infof("<remote> "+fmt, args...) 31 }) 32 sharedFactory := remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ 33 "": sharedStore, 34 }) 35 tmpFileCounter := 0 36 37 providers := make(map[string]objstorage.Provider) 38 // We maintain both backings and backing handles to allow tests to use the 39 // backings after the handles have been closed. 40 backings := make(map[string]objstorage.RemoteObjectBacking) 41 backingHandles := make(map[string]objstorage.RemoteObjectBackingHandle) 42 var curProvider objstorage.Provider 43 readaheadConfig := DefaultReadaheadConfig 44 datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { 45 readaheadConfig = DefaultReadaheadConfig 46 scanArgs := func(desc string, args ...interface{}) { 47 t.Helper() 48 if len(d.CmdArgs) != len(args) { 49 d.Fatalf(t, "usage: %s %s", d.Cmd, desc) 50 } 51 for i := range args { 52 _, err := fmt.Sscan(d.CmdArgs[i].String(), args[i]) 53 if err != nil { 54 d.Fatalf(t, "%s: error parsing argument '%s'", d.Cmd, d.CmdArgs[i]) 55 } 56 } 57 } 58 ctx := context.Background() 59 60 log.Reset() 61 switch d.Cmd { 62 case "open": 63 var fsDir string 64 var creatorID objstorage.CreatorID 65 scanArgs("<fs-dir> <remote-creator-id>", &fsDir, &creatorID) 66 67 st := DefaultSettings(fs, fsDir) 68 if creatorID != 0 { 69 st.Remote.StorageFactory = sharedFactory 70 st.Remote.CreateOnShared = remote.CreateOnSharedAll 71 st.Remote.CreateOnSharedLocator = "" 72 } 73 st.Local.ReadaheadConfigFn = func() ReadaheadConfig { 74 return readaheadConfig 75 } 76 require.NoError(t, fs.MkdirAll(fsDir, 0755)) 77 p, err := Open(st) 78 require.NoError(t, err) 79 if creatorID != 0 { 80 require.NoError(t, p.SetCreatorID(creatorID)) 81 } 82 // Checking refs on open affects the test output. We don't want tests to 83 // only pass when the `invariants` tag is used, so unconditionally 84 // enable ref checking on open. 85 p.(*provider).remote.shared.checkRefsOnOpen = true 86 providers[fsDir] = p 87 curProvider = p 88 89 return log.String() 90 91 case "switch": 92 var fsDir string 93 scanArgs("<fs-dir>", &fsDir) 94 curProvider = providers[fsDir] 95 if curProvider == nil { 96 t.Fatalf("unknown provider %s", fsDir) 97 } 98 99 return "" 100 101 case "close": 102 require.NoError(t, curProvider.Sync()) 103 require.NoError(t, curProvider.Close()) 104 delete(providers, curProvider.(*provider).st.FSDirName) 105 curProvider = nil 106 107 return log.String() 108 109 case "create": 110 opts := objstorage.CreateOptions{ 111 SharedCleanupMethod: objstorage.SharedRefTracking, 112 } 113 if len(d.CmdArgs) == 5 && d.CmdArgs[4].Key == "no-ref-tracking" { 114 d.CmdArgs = d.CmdArgs[:4] 115 opts.SharedCleanupMethod = objstorage.SharedNoCleanup 116 } 117 var fileNum base.FileNum 118 var typ string 119 var salt, size int 120 scanArgs("<file-num> <local|shared> <salt> <size> [no-ref-tracking]", &fileNum, &typ, &salt, &size) 121 switch typ { 122 case "local": 123 case "shared": 124 opts.PreferSharedStorage = true 125 default: 126 d.Fatalf(t, "'%s' should be 'local' or 'shared'", typ) 127 } 128 w, _, err := curProvider.Create(ctx, base.FileTypeTable, fileNum.DiskFileNum(), opts) 129 if err != nil { 130 return err.Error() 131 } 132 data := make([]byte, size) 133 // TODO(radu): write in chunks? 134 genData(byte(salt), 0, data) 135 require.NoError(t, w.Write(data)) 136 require.NoError(t, w.Finish()) 137 138 return log.String() 139 140 case "link-or-copy": 141 opts := objstorage.CreateOptions{ 142 SharedCleanupMethod: objstorage.SharedRefTracking, 143 } 144 if len(d.CmdArgs) == 5 && d.CmdArgs[4].Key == "no-ref-tracking" { 145 d.CmdArgs = d.CmdArgs[:4] 146 opts.SharedCleanupMethod = objstorage.SharedNoCleanup 147 } 148 var fileNum base.FileNum 149 var typ string 150 var salt, size int 151 scanArgs("<file-num> <local|shared> <salt> <size> [no-ref-tracking]", &fileNum, &typ, &salt, &size) 152 switch typ { 153 case "local": 154 case "shared": 155 opts.PreferSharedStorage = true 156 default: 157 d.Fatalf(t, "'%s' should be 'local' or 'shared'", typ) 158 } 159 160 tmpFileCounter++ 161 tmpFilename := fmt.Sprintf("temp-file-%d", tmpFileCounter) 162 f, err := fs.Create(tmpFilename) 163 require.NoError(t, err) 164 data := make([]byte, size) 165 genData(byte(salt), 0, data) 166 n, err := f.Write(data) 167 require.Equal(t, len(data), n) 168 require.NoError(t, err) 169 require.NoError(t, f.Close()) 170 171 _, err = curProvider.LinkOrCopyFromLocal( 172 ctx, fs, tmpFilename, base.FileTypeTable, fileNum.DiskFileNum(), opts, 173 ) 174 require.NoError(t, err) 175 return log.String() 176 177 case "read": 178 forCompaction := d.HasArg("for-compaction") 179 if arg, ok := d.Arg("readahead"); ok { 180 var mode ReadaheadMode 181 switch arg.Vals[0] { 182 case "off": 183 mode = NoReadahead 184 case "sys-readahead": 185 mode = SysReadahead 186 case "fadvise-sequential": 187 mode = FadviseSequential 188 default: 189 d.Fatalf(t, "unknown readahead mode %s", arg.Vals[0]) 190 } 191 if forCompaction { 192 readaheadConfig.Informed = mode 193 } else { 194 readaheadConfig.Speculative = mode 195 } 196 } 197 198 d.CmdArgs = d.CmdArgs[:1] 199 var fileNum base.FileNum 200 scanArgs("<file-num> [for-compaction] [readahead|speculative-overhead=off|sys-readahead|fadvise-sequential]", &fileNum) 201 r, err := curProvider.OpenForReading(ctx, base.FileTypeTable, fileNum.DiskFileNum(), objstorage.OpenOptions{}) 202 if err != nil { 203 return err.Error() 204 } 205 var rh objstorage.ReadHandle 206 // Test both ways of getting a handle. 207 if rand.Intn(2) == 0 { 208 rh = r.NewReadHandle(ctx) 209 } else { 210 var prealloc PreallocatedReadHandle 211 rh = UsePreallocatedReadHandle(ctx, r, &prealloc) 212 } 213 if forCompaction { 214 rh.SetupForCompaction() 215 } 216 log.Infof("size: %d", r.Size()) 217 for _, l := range strings.Split(d.Input, "\n") { 218 var offset, size int 219 fmt.Sscanf(l, "%d %d", &offset, &size) 220 data := make([]byte, size) 221 err := rh.ReadAt(ctx, data, int64(offset)) 222 if err != nil { 223 log.Infof("%d %d: %v", offset, size, err) 224 } else { 225 salt := checkData(t, offset, data) 226 log.Infof("%d %d: ok (salt %d)", offset, size, salt) 227 } 228 } 229 require.NoError(t, rh.Close()) 230 require.NoError(t, r.Close()) 231 return log.String() 232 233 case "remove": 234 var fileNum base.FileNum 235 scanArgs("<file-num>", &fileNum) 236 if err := curProvider.Remove(base.FileTypeTable, fileNum.DiskFileNum()); err != nil { 237 return err.Error() 238 } 239 return log.String() 240 241 case "list": 242 for _, meta := range curProvider.List() { 243 log.Infof("%s -> %s", meta.DiskFileNum, curProvider.Path(meta)) 244 } 245 return log.String() 246 247 case "save-backing": 248 var key string 249 var fileNum base.FileNum 250 scanArgs("<key> <file-num>", &key, &fileNum) 251 meta, err := curProvider.Lookup(base.FileTypeTable, fileNum.DiskFileNum()) 252 require.NoError(t, err) 253 handle, err := curProvider.RemoteObjectBacking(&meta) 254 if err != nil { 255 return err.Error() 256 } 257 backing, err := handle.Get() 258 require.NoError(t, err) 259 backings[key] = backing 260 backingHandles[key] = handle 261 return log.String() 262 263 case "close-backing": 264 var key string 265 scanArgs("<key>", &key) 266 backingHandles[key].Close() 267 return "" 268 269 case "attach": 270 lines := strings.Split(d.Input, "\n") 271 if len(lines) == 0 { 272 d.Fatalf(t, "at least one row expected; format: <key> <file-num>") 273 } 274 var objs []objstorage.RemoteObjectToAttach 275 for _, l := range lines { 276 var key string 277 var fileNum base.FileNum 278 _, err := fmt.Sscan(l, &key, &fileNum) 279 require.NoError(t, err) 280 b, ok := backings[key] 281 if !ok { 282 d.Fatalf(t, "unknown backing key %q", key) 283 } 284 objs = append(objs, objstorage.RemoteObjectToAttach{ 285 FileType: base.FileTypeTable, 286 FileNum: fileNum.DiskFileNum(), 287 Backing: b, 288 }) 289 } 290 metas, err := curProvider.AttachRemoteObjects(objs) 291 if err != nil { 292 return log.String() + "error: " + err.Error() 293 } 294 for _, meta := range metas { 295 log.Infof("%s -> %s", meta.DiskFileNum, curProvider.Path(meta)) 296 } 297 return log.String() 298 299 default: 300 d.Fatalf(t, "unknown command %s", d.Cmd) 301 return "" 302 } 303 }) 304 }) 305 } 306 307 func TestSharedMultipleLocators(t *testing.T) { 308 ctx := context.Background() 309 stores := map[remote.Locator]remote.Storage{ 310 "foo": remote.NewInMem(), 311 "bar": remote.NewInMem(), 312 } 313 sharedFactory := remote.MakeSimpleFactory(stores) 314 315 st1 := DefaultSettings(vfs.NewMem(), "") 316 st1.Remote.StorageFactory = sharedFactory 317 st1.Remote.CreateOnShared = remote.CreateOnSharedAll 318 st1.Remote.CreateOnSharedLocator = "foo" 319 p1, err := Open(st1) 320 require.NoError(t, err) 321 require.NoError(t, p1.SetCreatorID(1)) 322 323 st2 := DefaultSettings(vfs.NewMem(), "") 324 st2.Remote.StorageFactory = sharedFactory 325 st2.Remote.CreateOnShared = remote.CreateOnSharedAll 326 st2.Remote.CreateOnSharedLocator = "bar" 327 p2, err := Open(st2) 328 require.NoError(t, err) 329 require.NoError(t, p2.SetCreatorID(2)) 330 331 file1 := base.FileNum(1).DiskFileNum() 332 file2 := base.FileNum(2).DiskFileNum() 333 334 for i, provider := range []objstorage.Provider{p1, p2} { 335 w, _, err := provider.Create(ctx, base.FileTypeTable, file1, objstorage.CreateOptions{ 336 PreferSharedStorage: true, 337 }) 338 require.NoError(t, err) 339 data := make([]byte, 100) 340 genData(byte(i), 0, data) 341 require.NoError(t, w.Write(data)) 342 require.NoError(t, w.Finish()) 343 } 344 345 // checkObjects reads the given object and verifies the data matches the salt. 346 checkObject := func(p objstorage.Provider, fileNum base.DiskFileNum, salt byte) { 347 t.Helper() 348 r, err := p.OpenForReading(ctx, base.FileTypeTable, fileNum, objstorage.OpenOptions{}) 349 require.NoError(t, err) 350 data := make([]byte, r.Size()) 351 require.NoError(t, r.ReadAt(ctx, data, 0)) 352 r.Close() 353 require.Equal(t, salt, checkData(t, 0, data)) 354 } 355 356 // Now attach p1's object (in the "foo" store) to p2. 357 meta1, err := p1.Lookup(base.FileTypeTable, file1) 358 require.NoError(t, err) 359 h1, err := p1.RemoteObjectBacking(&meta1) 360 require.NoError(t, err) 361 b1, err := h1.Get() 362 require.NoError(t, err) 363 364 _, err = p2.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ 365 FileNum: file2, 366 FileType: base.FileTypeTable, 367 Backing: b1, 368 }}) 369 require.NoError(t, err) 370 // Close the handle from which we obtained b1. 371 h1.Close() 372 checkObject(p2, file2, 0) 373 374 // Now attach p2's object (in the "bar" store) to p1. 375 meta2, err := p2.Lookup(base.FileTypeTable, file1) 376 require.NoError(t, err) 377 h2, err := p2.RemoteObjectBacking(&meta2) 378 require.NoError(t, err) 379 b2, err := h2.Get() 380 require.NoError(t, err) 381 _, err = p1.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ 382 FileNum: file2, 383 FileType: base.FileTypeTable, 384 Backing: b2, 385 }}) 386 require.NoError(t, err) 387 // Close the handle from which we obtained b2. 388 h2.Close() 389 checkObject(p1, file2, 1) 390 391 // Check that the object still works after close/reopen. 392 require.NoError(t, p1.Close()) 393 p1, err = Open(st1) 394 require.NoError(t, err) 395 checkObject(p1, file2, 1) 396 require.NoError(t, p1.Close()) 397 398 require.NoError(t, p2.Close()) 399 400 // Try to attach an object to a provider that doesn't recognize the locator. 401 st3 := DefaultSettings(vfs.NewMem(), "") 402 st3.Remote.StorageFactory = remote.MakeSimpleFactory(nil) 403 p3, err := Open(st3) 404 require.NoError(t, err) 405 require.NoError(t, p3.SetCreatorID(3)) 406 _, err = p3.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ 407 FileNum: file2, 408 FileType: base.FileTypeTable, 409 Backing: b2, 410 }}) 411 require.Error(t, err) 412 require.NoError(t, p3.Close()) 413 } 414 415 func TestAttachCustomObject(t *testing.T) { 416 ctx := context.Background() 417 storage := remote.NewInMem() 418 sharedFactory := remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ 419 "foo": storage, 420 }) 421 422 st1 := DefaultSettings(vfs.NewMem(), "") 423 st1.Remote.StorageFactory = sharedFactory 424 p1, err := Open(st1) 425 require.NoError(t, err) 426 defer p1.Close() 427 require.NoError(t, p1.SetCreatorID(1)) 428 429 w, err := storage.CreateObject("some-obj-name") 430 require.NoError(t, err) 431 data := make([]byte, 100) 432 genData(123, 0, data) 433 _, err = w.Write(data) 434 require.NoError(t, err) 435 require.NoError(t, w.Close()) 436 437 backing, err := p1.CreateExternalObjectBacking("foo", "some-obj-name") 438 require.NoError(t, err) 439 440 _, err = p1.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ 441 FileNum: base.FileNum(1).DiskFileNum(), 442 FileType: base.FileTypeTable, 443 Backing: backing, 444 }}) 445 require.NoError(t, err) 446 447 // Verify the provider can read the object. 448 r, err := p1.OpenForReading(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.OpenOptions{}) 449 require.NoError(t, err) 450 require.Equal(t, int64(len(data)), r.Size()) 451 buf := make([]byte, r.Size()) 452 require.NoError(t, r.ReadAt(ctx, buf, 0)) 453 require.Equal(t, byte(123), checkData(t, 0, buf)) 454 require.NoError(t, r.Close()) 455 456 // Verify that we can extract a correct backing from this provider and attach 457 // the object to another provider. 458 meta, err := p1.Lookup(base.FileTypeTable, base.FileNum(1).DiskFileNum()) 459 require.NoError(t, err) 460 handle, err := p1.RemoteObjectBacking(&meta) 461 require.NoError(t, err) 462 defer handle.Close() 463 backing, err = handle.Get() 464 require.NoError(t, err) 465 466 st2 := DefaultSettings(vfs.NewMem(), "") 467 st2.Remote.StorageFactory = sharedFactory 468 p2, err := Open(st2) 469 require.NoError(t, err) 470 defer p2.Close() 471 require.NoError(t, p2.SetCreatorID(2)) 472 473 _, err = p2.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ 474 FileNum: base.FileNum(10).DiskFileNum(), 475 FileType: base.FileTypeTable, 476 Backing: backing, 477 }}) 478 require.NoError(t, err) 479 480 // Verify the provider can read the object. 481 r, err = p2.OpenForReading(ctx, base.FileTypeTable, base.FileNum(10).DiskFileNum(), objstorage.OpenOptions{}) 482 require.NoError(t, err) 483 require.Equal(t, int64(len(data)), r.Size()) 484 buf = make([]byte, r.Size()) 485 require.NoError(t, r.ReadAt(ctx, buf, 0)) 486 require.Equal(t, byte(123), checkData(t, 0, buf)) 487 require.NoError(t, r.Close()) 488 } 489 490 func TestNotExistError(t *testing.T) { 491 fs := vfs.NewMem() 492 st := DefaultSettings(fs, "") 493 sharedStorage := remote.NewInMem() 494 st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ 495 "": sharedStorage, 496 }) 497 st.Remote.CreateOnShared = remote.CreateOnSharedAll 498 st.Remote.CreateOnSharedLocator = "" 499 provider, err := Open(st) 500 require.NoError(t, err) 501 require.NoError(t, provider.SetCreatorID(1)) 502 503 for i, shared := range []bool{false, true} { 504 fileNum := base.FileNum(1 + i).DiskFileNum() 505 name := "local" 506 if shared { 507 name = "remote" 508 } 509 t.Run(name, func(t *testing.T) { 510 // Removing or opening an object that the provider doesn't know anything 511 // about should return a not-exist error. 512 err := provider.Remove(base.FileTypeTable, fileNum) 513 require.True(t, provider.IsNotExistError(err)) 514 _, err = provider.OpenForReading(context.Background(), base.FileTypeTable, fileNum, objstorage.OpenOptions{}) 515 require.True(t, provider.IsNotExistError(err)) 516 517 w, _, err := provider.Create(context.Background(), base.FileTypeTable, fileNum, objstorage.CreateOptions{ 518 PreferSharedStorage: shared, 519 }) 520 require.NoError(t, err) 521 require.NoError(t, w.Write([]byte("foo"))) 522 require.NoError(t, w.Finish()) 523 524 // Remove the underlying file or object. 525 if !shared { 526 require.NoError(t, fs.Remove(base.MakeFilename(base.FileTypeTable, fileNum))) 527 } else { 528 meta, err := provider.Lookup(base.FileTypeTable, fileNum) 529 require.NoError(t, err) 530 require.NoError(t, sharedStorage.Delete(remoteObjectName(meta))) 531 } 532 533 _, err = provider.OpenForReading(context.Background(), base.FileTypeTable, fileNum, objstorage.OpenOptions{}) 534 require.True(t, provider.IsNotExistError(err)) 535 536 // It's acceptable for Remove to return a not-exist error, or no error at all. 537 if err := provider.Remove(base.FileTypeTable, fileNum); err != nil { 538 require.True(t, provider.IsNotExistError(err)) 539 } 540 }) 541 } 542 } 543 544 // genData generates object data that can be checked later with checkData. 545 func genData(salt byte, offset int, p []byte) { 546 for i := range p { 547 p[i] = salt ^ xor(offset+i) 548 } 549 } 550 551 func checkData(t *testing.T, offset int, p []byte) (salt byte) { 552 t.Helper() 553 salt = p[0] ^ xor(offset) 554 for i := range p { 555 if p[i]^xor(offset+i) != salt { 556 t.Fatalf("invalid data") 557 } 558 } 559 return salt 560 } 561 562 // xor returns the XOR of all bytes representing the integer. 563 func xor(n int) byte { 564 v := uint64(n) 565 v ^= v >> 32 566 v ^= v >> 16 567 v ^= v >> 8 568 return byte(v) 569 } 570 571 // TestParallelSync checks that multiple goroutines can create and delete 572 // objects and sync in parallel. 573 func TestParallelSync(t *testing.T) { 574 for _, shared := range []bool{false, true} { 575 name := "local" 576 if shared { 577 name = "shared" 578 } 579 t.Run(name, func(t *testing.T) { 580 st := DefaultSettings(vfs.NewMem(), "") 581 st.Remote.StorageFactory = remote.MakeSimpleFactory(map[remote.Locator]remote.Storage{ 582 "": remote.NewInMem(), 583 }) 584 585 st.Remote.CreateOnShared = remote.CreateOnSharedAll 586 st.Remote.CreateOnSharedLocator = "" 587 p, err := Open(st) 588 require.NoError(t, err) 589 require.NoError(t, p.SetCreatorID(1)) 590 591 const numGoroutines = 4 592 const numOps = 100 593 var wg sync.WaitGroup 594 for n := 0; n < numGoroutines; n++ { 595 wg.Add(1) 596 go func(startNum int, shared bool) { 597 defer wg.Done() 598 rng := rand.New(rand.NewSource(int64(startNum))) 599 for i := 0; i < numOps; i++ { 600 num := base.FileNum(startNum + i).DiskFileNum() 601 w, _, err := p.Create(context.Background(), base.FileTypeTable, num, objstorage.CreateOptions{ 602 PreferSharedStorage: shared, 603 }) 604 if err != nil { 605 panic(err) 606 } 607 if err := w.Finish(); err != nil { 608 panic(err) 609 } 610 if rng.Intn(2) == 0 { 611 if err := p.Sync(); err != nil { 612 panic(err) 613 } 614 } 615 if err := p.Remove(base.FileTypeTable, num); err != nil { 616 panic(err) 617 } 618 if rng.Intn(2) == 0 { 619 if err := p.Sync(); err != nil { 620 panic(err) 621 } 622 } 623 } 624 }(numOps*n, shared) 625 } 626 wg.Wait() 627 }) 628 } 629 }