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