@@ -509,7 +509,6 @@ TEST_F(BlobDBTest, SstFileManager) {
509509
510510 BlobDBOptions bdb_options;
511511 bdb_options.enable_garbage_collection = true ;
512- bdb_options.garbage_collection_cutoff = 1.0 ;
513512 Options db_options;
514513
515514 int files_scheduled_to_delete = 0 ;
@@ -527,20 +526,31 @@ TEST_F(BlobDBTest, SstFileManager) {
527526
528527 Open (bdb_options, db_options);
529528
530- // Create one obselete file and clean it.
529+ // Create 4 blob files. With GC cutoff of 0.25, the oldest file (file 1)
530+ // will be in the GC zone: floor(0.25 * 4) = 1.
531531 ASSERT_OK (blob_db_->Put (WriteOptions (), " foo" , " bar" ));
532532 auto blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
533533 ASSERT_EQ (1 , blob_files.size ());
534534 std::shared_ptr<BlobFile> bfile = blob_files[0 ];
535535 ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (bfile));
536+
537+ // Create 3 more blob files (files 2-4, outside GC zone).
538+ for (int i = 1 ; i < 4 ; i++) {
539+ ASSERT_OK (blob_db_->Put (WriteOptions (), " key" + std::to_string (i), " val" ));
540+ blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
541+ ASSERT_EQ (static_cast <size_t >(i + 1 ), blob_files.size ());
542+ ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (blob_files[i]));
543+ }
544+
536545 ASSERT_OK (blob_db_->CompactRange (CompactRangeOptions (), nullptr , nullptr ));
537546 blob_db_impl ()->TEST_DeleteObsoleteFiles ();
538547
539548 // Even if SSTFileManager is not set, DB is creating a dummy one.
540549 ASSERT_EQ (1 , files_scheduled_to_delete);
541550 Destroy ();
542551 // Make sure that DestroyBlobDB() also goes through delete scheduler.
543- ASSERT_EQ (2 , files_scheduled_to_delete);
552+ // Remaining files: 3 original (files 2-4) + 1 GC output file = 4 files.
553+ ASSERT_EQ (5 , files_scheduled_to_delete);
544554 SyncPoint::GetInstance ()->DisableProcessing ();
545555 sfm->WaitForEmptyTrash ();
546556}
@@ -606,20 +616,27 @@ TEST_F(BlobDBTest, SstFileManagerRestart) {
606616TEST_F (BlobDBTest, SnapshotAndGarbageCollection) {
607617 BlobDBOptions bdb_options;
608618 bdb_options.enable_garbage_collection = true ;
609- bdb_options.garbage_collection_cutoff = 1.0 ;
610619 bdb_options.disable_background_tasks = true ;
611620
612621 Options options;
613622 options.disable_auto_compactions = true ;
614623
615- // i = when to take snapshot
624+ // This test verifies that snapshots protect blob files from deletion during
625+ // garbage collection. With fixed GC cutoff of 0.25 and 8 immutable files,
626+ // floor(0.25 * 8) = 2 files are in the GC zone (files 1 and 2).
627+ //
628+ // We run 4 iterations with different snapshot timing:
629+ // i=0: snapshot after key1 (before key2) - protects file 1
630+ // i=1: snapshot after key2 (before key3) - protects files 1 and 2
631+ // i=2: snapshot after key9 (after all keys) - no protection needed
632+ // i=3: snapshot after Delete(key2) - no protection needed
616633 for (int i = 0 ; i < 4 ; i++) {
617634 Destroy ();
618635 Open (bdb_options, options);
619636
620637 const Snapshot* snapshot = nullptr ;
621638
622- // First file
639+ // Create first blob file (will be in GC zone).
623640 ASSERT_OK (Put (" key1" , " value" ));
624641 if (i == 0 ) {
625642 snapshot = blob_db_->GetSnapshot ();
@@ -629,47 +646,75 @@ TEST_F(BlobDBTest, SnapshotAndGarbageCollection) {
629646 ASSERT_EQ (1 , blob_files.size ());
630647 ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (blob_files[0 ]));
631648
632- // Second file
649+ // Create second blob file (will be in GC zone). We track this file
650+ // to verify it becomes obsolete after GC relocates its blob.
633651 ASSERT_OK (Put (" key2" , " value" ));
634652 if (i == 1 ) {
635653 snapshot = blob_db_->GetSnapshot ();
636654 }
637655
638656 blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
639657 ASSERT_EQ (2 , blob_files.size ());
640- auto bfile = blob_files[1 ];
641- ASSERT_FALSE (bfile->Immutable ());
642- ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (bfile));
658+ auto gc_target_file = blob_files[1 ];
659+ ASSERT_FALSE (gc_target_file->Immutable ());
660+ ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (gc_target_file));
661+
662+ // Create files 3-8, all closed (these are outside GC zone).
663+ for (int j = 3 ; j <= 8 ; j++) {
664+ ASSERT_OK (Put (" key" + std::to_string (j), " value" ));
665+ blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
666+ ASSERT_EQ (static_cast <size_t >(j), blob_files.size ());
667+ ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (blob_files[j - 1 ]));
668+ }
643669
644- // Third file
645- ASSERT_OK (Put (" key3" , " value" ));
670+ // Create file 9 but leave it open (mutable). Only immutable files are
671+ // counted for GC cutoff calculation.
672+ ASSERT_OK (Put (" key9" , " value" ));
646673 if (i == 2 ) {
647674 snapshot = blob_db_->GetSnapshot ();
648675 }
649676
677+ // Verify we have 9 total files (8 immutable + 1 mutable).
678+ blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
679+ ASSERT_EQ (9 , blob_files.size ());
680+
681+ // Trigger GC via compaction. Blobs in files 1 and 2 will be relocated
682+ // to a new GC output file.
650683 ASSERT_OK (blob_db_->CompactRange (CompactRangeOptions (), nullptr , nullptr ));
651- ASSERT_TRUE (bfile->Obsolete ());
684+
685+ // Verify gc_target_file (file 2) is now obsolete.
686+ ASSERT_TRUE (gc_target_file->Obsolete ());
687+ // Verify the obsolete sequence matches the latest sequence number.
652688 ASSERT_EQ (blob_db_->GetLatestSequenceNumber (),
653- bfile ->GetObsoleteSequence ());
689+ gc_target_file ->GetObsoleteSequence ());
654690
655691 Delete (" key2" );
656692 if (i == 3 ) {
657693 snapshot = blob_db_->GetSnapshot ();
658694 }
659695
660- ASSERT_EQ (4 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
696+ // Verify we now have 10 files (9 original + 1 GC output file).
697+ // Files 1 and 2 are obsolete but not yet deleted.
698+ ASSERT_EQ (10 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
661699 blob_db_impl ()->TEST_DeleteObsoleteFiles ();
662700
663701 if (i >= 2 ) {
664- // The snapshot shouldn't see data in bfile
665- ASSERT_EQ (2 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
702+ // Snapshot was taken after all keys were written, so it sees the
703+ // post-compaction state where blob indexes point to the GC output file.
704+ // Obsolete files 1 and 2 can be deleted immediately.
705+ // Verify 8 files remain (10 - 2 obsolete files deleted).
706+ ASSERT_EQ (8 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
666707 blob_db_->ReleaseSnapshot (snapshot);
667708 } else {
668- // The snapshot will see data in bfile, so the file shouldn't be deleted
669- ASSERT_EQ (4 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
709+ // Snapshot was taken before compaction completed, so it may still
710+ // reference blobs in the obsolete files. Files cannot be deleted.
711+ // Verify all 10 files still exist.
712+ ASSERT_EQ (10 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
670713 blob_db_->ReleaseSnapshot (snapshot);
671714 blob_db_impl ()->TEST_DeleteObsoleteFiles ();
672- ASSERT_EQ (2 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
715+ // After releasing the snapshot, obsolete files can be deleted.
716+ // Verify 8 files remain.
717+ ASSERT_EQ (8 , blob_db_impl ()->TEST_GetBlobFiles ().size ());
673718 }
674719 }
675720}
@@ -1154,7 +1199,6 @@ TEST_F(BlobDBTest, GarbageCollection) {
11541199 BlobDBOptions bdb_options;
11551200 bdb_options.blob_file_size = kBlobFileSize ;
11561201 bdb_options.enable_garbage_collection = true ;
1157- bdb_options.garbage_collection_cutoff = 0.25 ;
11581202 bdb_options.disable_background_tasks = true ;
11591203
11601204 Options options;
@@ -1238,8 +1282,9 @@ TEST_F(BlobDBTest, GarbageCollection) {
12381282
12391283 VerifyBaseDB (blob_value_versions);
12401284
1241- const uint64_t cutoff = static_cast <uint64_t >(
1242- bdb_options.garbage_collection_cutoff * kNumBlobFiles );
1285+ // GC cutoff is fixed at 0.25
1286+ constexpr double kGCCutoff = 0.25 ;
1287+ const uint64_t cutoff = static_cast <uint64_t >(kGCCutoff * kNumBlobFiles );
12431288 for (auto & pair : blob_index_versions) {
12441289 BlobIndexVersion& version = pair.second ;
12451290
@@ -1290,40 +1335,56 @@ TEST_F(BlobDBTest, GarbageCollection) {
12901335TEST_F (BlobDBTest, GarbageCollectionFailure) {
12911336 BlobDBOptions bdb_options;
12921337 bdb_options.enable_garbage_collection = true ;
1293- bdb_options.garbage_collection_cutoff = 1.0 ;
12941338 bdb_options.disable_background_tasks = true ;
12951339
12961340 Options db_options;
12971341 db_options.statistics = CreateDBStatistics ();
12981342
12991343 Open (bdb_options, db_options);
13001344
1301- // Write a couple of valid blobs.
1345+ // Create 4 blob files. With fixed GC cutoff of 0.25, the oldest file
1346+ // (floor(0.25 * 4) = 1) will be in the GC zone.
1347+ // The first file contains valid blobs for "foo" and "dead".
13021348 ASSERT_OK (Put (" foo" , " bar" ));
13031349 ASSERT_OK (Put (" dead" , " beef" ));
13041350
1305- // Write a fake blob reference into the base DB that points to a non-existing
1306- // blob file.
1351+ auto blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
1352+ ASSERT_EQ (blob_files.size (), 1 );
1353+ auto first_file = blob_files[0 ];
1354+ uint64_t first_file_number = first_file->BlobFileNumber ();
1355+ ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (first_file));
1356+
1357+ // Create 3 more blob files (files 2-4, outside GC zone).
1358+ for (int i = 1 ; i < 4 ; i++) {
1359+ ASSERT_OK (Put (" key" + std::to_string (i), " value" ));
1360+ blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
1361+ ASSERT_EQ (static_cast <size_t >(i + 1 ), blob_files.size ());
1362+ ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (blob_files[i]));
1363+ }
1364+
1365+ // Write a fake blob index that points to the first file (in GC zone)
1366+ // but with an invalid offset beyond the file size. This will cause
1367+ // GC to fail when it tries to read this blob.
13071368 std::string blob_index;
1308- BlobIndex::EncodeBlob (&blob_index, /* file_number */ 1000 , /* offset */ 1234 ,
1369+ BlobIndex::EncodeBlob (&blob_index, first_file_number , /* offset */ 999999 ,
13091370 /* size */ 5678 , kNoCompression );
13101371
13111372 WriteBatch batch;
13121373 ASSERT_OK (WriteBatchInternal::PutBlobIndex (
13131374 &batch, blob_db_->DefaultColumnFamily ()->GetID (), " key" , blob_index));
13141375 ASSERT_OK (blob_db_->GetRootDB ()->Write (WriteOptions (), &batch));
13151376
1316- auto blob_files = blob_db_impl ()->TEST_GetBlobFiles ();
1317- ASSERT_EQ (blob_files.size (), 1 );
1318- auto blob_file = blob_files[0 ];
1319- ASSERT_OK (blob_db_impl ()->TEST_CloseBlobFile (blob_file));
1320-
1377+ // Verify compaction fails with IO error due to invalid blob offset.
13211378 ASSERT_TRUE (blob_db_->CompactRange (CompactRangeOptions (), nullptr , nullptr )
13221379 .IsIOError ());
13231380
13241381 const Statistics* const statistics = db_options.statistics .get ();
13251382 assert (statistics);
13261383
1384+ // Verify GC statistics:
1385+ // - Relocated 2 keys ("foo" and "dead") with 7 bytes ("bar" + "beef")
1386+ // - Failed on "key" which has invalid blob offset
1387+ // - Created 1 new GC output file before failing
13271388 ASSERT_EQ (statistics->getTickerCount (BLOB_DB_GC_NUM_FILES ), 0 );
13281389 ASSERT_EQ (statistics->getTickerCount (BLOB_DB_GC_NUM_NEW_FILES ), 1 );
13291390 ASSERT_EQ (statistics->getTickerCount (BLOB_DB_GC_FAILURES ), 1 );
0 commit comments