Skip to content

Commit 68f24d4

Browse files
mikechuangmetafacebook-github-bot
authored andcommitted
Compactions discard values in dropped blob files (#15645)
Summary: Changes - Every compaction, automatic or manual, whose inputs may reference a blob file recorded by `best_efforts_blob_db_repair` runs `MissingBlobRepairIterator` in a new mode limited to recorded blob files. Lost values become deletions at their sequence number; wide entities keep surviving columns. - These compactions run locally and skip garbage for recorded blob files. Unrecorded missing blob files still fail with Corruption. Why? - After D131013277 records a dropped blob file, a compaction that drops one of its values still fails: garbage for a blob file absent from the Version is rejected ("unknown blob file"), and blob GC, filters and merges read the missing payload. Reusing the `CompactRangeOptions` repair from D128836898 lets compactions proceed and removes the dangling references over time. Changes visible to existing users - None while no dropped blob files are recorded: each compaction adds an O(1) check of its input Version's empty dropped set. Design doc - https://mdoc.internalmeta.com/doc/ffcd44f2-3147-4ced-aee4-c5623ccd6d8e Reviewed By: xingbowang Differential Revision: D131013279
1 parent eb12834 commit 68f24d4

8 files changed

Lines changed: 257 additions & 28 deletions

File tree

‎db/blob/db_blob_basic_test.cc‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,66 @@ TEST_F(DBBlobBasicTest, BestEffortsBlobDbRepairWithAtomicFlush) {
18971897
ASSERT_EQ(Get(1, "y"), "vy");
18981898
}
18991899

1900+
TEST_F(DBBlobBasicTest,
1901+
BestEffortsBlobDbRepairAutomaticCompactionDiscardsDroppedValues) {
1902+
Options options = MissingBlobRepairOptions();
1903+
uint64_t missing_blob_file = 0;
1904+
ASSERT_NO_FATAL_FAILURE(
1905+
RepairMissingNonOldestBlob(options, &missing_blob_file));
1906+
ASSERT_EQ(NumTableFilesAtLevel(1), 1);
1907+
1908+
// Reopen without repair. Overwriting "b" makes an automatic compaction drop
1909+
// its old value, which is in the dropped blob file.
1910+
options.disable_auto_compactions = false;
1911+
options.level0_file_num_compaction_trigger = 1;
1912+
Reopen(options);
1913+
ASSERT_OK(Put("b", "vb2"));
1914+
ASSERT_OK(Flush());
1915+
ASSERT_OK(dbfull()->TEST_WaitForCompact());
1916+
ASSERT_EQ(NumTableFilesAtLevel(0), 0);
1917+
uint64_t background_errors = 0;
1918+
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBackgroundErrors,
1919+
&background_errors));
1920+
ASSERT_EQ(background_errors, 0);
1921+
1922+
ASSERT_EQ(Get("a"), "va");
1923+
ASSERT_EQ(Get("b"), "vb2");
1924+
ASSERT_EQ(Get("c"), "vc");
1925+
ASSERT_EQ(Get("d"), "NOT_FOUND");
1926+
1927+
Reopen(options);
1928+
ASSERT_EQ(Get("b"), "vb2");
1929+
ASSERT_EQ(Get("d"), "NOT_FOUND");
1930+
}
1931+
1932+
TEST_F(DBBlobBasicTest, BestEffortsBlobDbRepairWithBlobGarbageCollection) {
1933+
Options options = MissingBlobRepairOptions();
1934+
uint64_t missing_blob_file = 0;
1935+
ASSERT_NO_FATAL_FAILURE(
1936+
RepairMissingNonOldestBlob(options, &missing_blob_file));
1937+
1938+
// Garbage collection relocates every blob, so it would read the dropped
1939+
// blob file if the compaction did not discard its values first.
1940+
options.enable_blob_garbage_collection = true;
1941+
options.blob_garbage_collection_age_cutoff = 1.0;
1942+
Reopen(options);
1943+
CompactRangeOptions compact_range_options;
1944+
compact_range_options.bottommost_level_compaction =
1945+
BottommostLevelCompaction::kForce;
1946+
ASSERT_OK(db_->CompactRange(compact_range_options, nullptr, nullptr));
1947+
ASSERT_EQ(Get("a"), "va");
1948+
ASSERT_EQ(Get("c"), "vc");
1949+
ASSERT_EQ(Get("b"), "NOT_FOUND");
1950+
ASSERT_EQ(Get("d"), "NOT_FOUND");
1951+
1952+
// The surviving values were relocated to a new blob file, so no SST file
1953+
// can reference the dropped blob file anymore.
1954+
ASSERT_TRUE(GetDroppedBlobFiles().empty());
1955+
Reopen(options);
1956+
ASSERT_TRUE(GetDroppedBlobFiles().empty());
1957+
ASSERT_EQ(Get("a"), "va");
1958+
}
1959+
19001960
TEST_F(DBBlobBasicTest, BestEffortsBlobDbRepairMissingOldestBlobWithForcedGc) {
19011961
// After repair drops the oldest blob file an SST file references, the
19021962
// oldest blob file left in the Version has no linked SST file.

‎db/blob/missing_blob_repair_iterator.h‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@
2020
namespace ROCKSDB_NAMESPACE {
2121

2222
// Replaces references absent from an immutable input Version with deletions
23-
// or entities containing the surviving columns. Input values must already be
23+
// or entities containing the surviving columns. With `only_dropped_blob_files`,
24+
// only references to blob files recorded as dropped in the input Version (see
25+
// VersionStorageInfo::GetDroppedBlobFiles()) are replaced; otherwise
26+
// references to any blob file absent from it are. Input values must already be
2427
// prepared, as they are for compaction input. Neither input is owned.
2528
class MissingBlobRepairIterator : public InternalIterator {
2629
public:
2730
MissingBlobRepairIterator(InternalIterator* input,
2831
const VersionStorageInfo* input_storage,
29-
size_t timestamp_size)
32+
size_t timestamp_size, bool only_dropped_blob_files)
3033
: input_(input),
3134
input_storage_(input_storage),
32-
timestamp_size_(timestamp_size) {
35+
timestamp_size_(timestamp_size),
36+
only_dropped_blob_files_(only_dropped_blob_files) {
3337
assert(input_);
3438
assert(input_storage_);
3539
Update();
@@ -161,6 +165,9 @@ class MissingBlobRepairIterator : public InternalIterator {
161165
if (index.IsInlined() || index.HasTTL() || index.IsSameFile()) {
162166
return false;
163167
}
168+
if (only_dropped_blob_files_) {
169+
return input_storage_->IsDroppedBlobFile(index.file_number());
170+
}
164171
const VersionStorageInfo::BlobFiles::const_iterator it =
165172
input_storage_->GetBlobFileMetaDataLB(index.file_number());
166173
return it == input_storage_->GetBlobFiles().end() ||
@@ -182,6 +189,20 @@ class MissingBlobRepairIterator : public InternalIterator {
182189
// changes the positions of surviving blob indexes, so rebuild those positions
183190
// before serialization. Only loss of every column becomes a deletion.
184191
Status RepairEntity(const ParsedInternalKey& parsed) {
192+
if (only_dropped_blob_files_) {
193+
// Most entities reference no dropped blob file. Find out from the blob
194+
// indexes alone before paying for a full deserialization.
195+
bool has_dropped = false;
196+
const Status s = WideColumnSerialization::ForEachBlobFileNumber(
197+
input_->value(), [this, &has_dropped](const BlobIndex& index) {
198+
has_dropped |= IsMissing(index);
199+
return Status::OK();
200+
});
201+
if (!s.ok() || !has_dropped) {
202+
return s;
203+
}
204+
}
205+
185206
columns_.clear();
186207
blob_columns_.clear();
187208
Status s = WideColumnSerialization::Deserialize(input_->value(), columns_,
@@ -277,6 +298,7 @@ class MissingBlobRepairIterator : public InternalIterator {
277298
WideColumns surviving_columns_;
278299
std::vector<std::pair<size_t, BlobIndex>> surviving_blobs_;
279300
Status status_;
301+
bool only_dropped_blob_files_;
280302
bool value_rewritten_ = false;
281303
bool repair_deletion_ = false;
282304
};

‎db/compaction/compaction.cc‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,38 @@ bool Compaction::DoesInputReferenceBlobFiles() const {
972972
return false;
973973
}
974974

975+
bool Compaction::RepairsMissingBlobReferences() const {
976+
if (skip_missing_blob_files_) {
977+
return true;
978+
}
979+
980+
assert(input_version_);
981+
const VersionStorageInfo* storage_info = input_version_->storage_info();
982+
assert(storage_info);
983+
984+
const std::vector<uint64_t>& dropped_blob_files =
985+
storage_info->GetDroppedBlobFiles();
986+
if (dropped_blob_files.empty()) {
987+
return false;
988+
}
989+
990+
// An SST file only references blob files at or above its oldest referenced
991+
// blob file.
992+
const uint64_t newest_dropped_blob_file = dropped_blob_files.back();
993+
for (const CompactionInputFiles& level_inputs : inputs_) {
994+
for (const FileMetaData* meta : level_inputs.files) {
995+
assert(meta);
996+
997+
if (meta->oldest_blob_file_number != kInvalidBlobFileNumber &&
998+
meta->oldest_blob_file_number <= newest_dropped_blob_file) {
999+
return true;
1000+
}
1001+
}
1002+
}
1003+
1004+
return false;
1005+
}
1006+
9751007
uint64_t Compaction::MaxInputFileNewestKeyTime(const InternalKey* start,
9761008
const InternalKey* end) const {
9771009
uint64_t newest_key_time = kUnknownNewestKeyTime;

‎db/compaction/compaction.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@ class Compaction {
362362
// PRE: input version has been set.
363363
bool DoesInputReferenceBlobFiles() const;
364364

365+
// Returns true iff this compaction replaces references to blob files missing
366+
// from its input Version (see MissingBlobRepairIterator): with
367+
// skip_missing_blob_files(), references to any such blob file; otherwise
368+
// references to blob files recorded as dropped in the input Version (see
369+
// VersionStorageInfo::GetDroppedBlobFiles()), when an input file might
370+
// reference one.
371+
//
372+
// PRE: input version has been set.
373+
bool RepairsMissingBlobReferences() const;
374+
365375
// test function to validate the functionality of IsBottommostLevel()
366376
// function -- determines if compaction with inputs and storage is bottommost
367377
static bool TEST_IsBottommostLevel(

‎db/compaction/compaction_iterator_test.cc‎

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,8 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
321321
InternalIterator* input = iter_.get();
322322
if (repair_enabled_) {
323323
repair_iter_ = std::make_unique<MissingBlobRepairIterator>(
324-
input, repair_storage_.get(), cmp_->timestamp_size());
324+
input, repair_storage_.get(), cmp_->timestamp_size(),
325+
repair_only_dropped_blob_files_);
325326
input = repair_iter_.get();
326327
}
327328
c_iter_.reset(new CompactionIterator(
@@ -338,6 +339,15 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
338339
c_iter_->SetMissingBlobRepairIterator(repair_iter_.get());
339340
}
340341

342+
// Repairs only references to `dropped_files`, which are recorded as dropped
343+
// in a Version that holds `healthy_files`.
344+
void EnableDroppedBlobRepair(const std::vector<uint64_t>& healthy_files,
345+
std::vector<uint64_t> dropped_files) {
346+
EnableMissingBlobRepair(healthy_files);
347+
repair_storage_->SetDroppedBlobFiles(std::move(dropped_files));
348+
repair_only_dropped_blob_files_ = true;
349+
}
350+
341351
void EnableMissingBlobRepair(const std::vector<uint64_t>& healthy_files) {
342352
repair_enabled_ = true;
343353
repair_storage_ = std::make_unique<VersionStorageInfo>(
@@ -415,6 +425,7 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
415425
const std::atomic<bool> kManualCompactionCanceledFalse_{false};
416426
FakeCompaction* compaction_proxy_;
417427
bool repair_enabled_ = false;
428+
bool repair_only_dropped_blob_files_ = false;
418429
};
419430

420431
TEST_P(CompactionIteratorTest, MergeBeforeFilterFullOrder) {
@@ -1088,7 +1099,8 @@ TEST_P(CompactionIteratorTest, MissingBlobRepairPreservesInputErrors) {
10881099
std::unique_ptr<InternalIterator> failed_input(
10891100
NewErrorInternalIterator(Status::IOError("Failed SST read")));
10901101
MissingBlobRepairIterator failed_repair(failed_input.get(),
1091-
repair_storage_.get(), 0);
1102+
repair_storage_.get(), 0,
1103+
/*only_dropped_blob_files=*/false);
10921104
failed_repair.SeekToFirst();
10931105
ASSERT_FALSE(failed_repair.Valid());
10941106
ASSERT_EQ(failed_repair.status().code(), Status::Code::kIOError);
@@ -1129,7 +1141,8 @@ TEST_P(CompactionIteratorTest, MissingBlobRepairPreservesInputErrors) {
11291141

11301142
FailingNextIterator input({test::KeyStr("a", 5, kTypeBlobIndex)},
11311143
{BlobIndexForFile(20)}, &icmp_);
1132-
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0);
1144+
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0,
1145+
/*only_dropped_blob_files=*/false);
11331146
repair.SeekToFirst();
11341147
ASSERT_OK(repair.status());
11351148
ASSERT_TRUE(repair.Valid());
@@ -1141,12 +1154,84 @@ TEST_P(CompactionIteratorTest, MissingBlobRepairPreservesInputErrors) {
11411154
ASSERT_FALSE(repair.IsRepairDeletion());
11421155
}
11431156

1157+
TEST_P(CompactionIteratorTest, MissingBlobRepairOnlyDroppedBlobFiles) {
1158+
// Blob file 10 is in the Version, 20 is recorded as dropped and 30 is
1159+
// missing without a record.
1160+
EnableMissingBlobRepair({10});
1161+
repair_storage_->SetDroppedBlobFiles({20});
1162+
VectorIterator input(
1163+
{test::KeyStr("a", 5, kTypeBlobIndex),
1164+
test::KeyStr("b", 4, kTypeBlobIndex),
1165+
test::KeyStr("c", 3, kTypeBlobIndex)},
1166+
{BlobIndexForFile(10), BlobIndexForFile(20), BlobIndexForFile(30)},
1167+
&icmp_);
1168+
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0,
1169+
/*only_dropped_blob_files=*/true);
1170+
repair.SeekToFirst();
1171+
ASSERT_TRUE(repair.Valid());
1172+
ASSERT_EQ(repair.key(), test::KeyStr("a", 5, kTypeBlobIndex));
1173+
ASSERT_FALSE(repair.IsRepairDeletion());
1174+
repair.Next();
1175+
ASSERT_TRUE(repair.Valid());
1176+
ASSERT_EQ(repair.key(), test::KeyStr("b", 4, kTypeDeletion));
1177+
ASSERT_TRUE(repair.IsRepairDeletion());
1178+
repair.Next();
1179+
ASSERT_TRUE(repair.Valid());
1180+
ASSERT_EQ(repair.key(), test::KeyStr("c", 3, kTypeBlobIndex));
1181+
ASSERT_EQ(repair.value(), BlobIndexForFile(30));
1182+
ASSERT_FALSE(repair.IsRepairDeletion());
1183+
repair.Next();
1184+
ASSERT_FALSE(repair.Valid());
1185+
ASSERT_OK(repair.status());
1186+
}
1187+
1188+
TEST_P(CompactionIteratorTest, MissingBlobRepairOnlyDroppedWideColumns) {
1189+
// Blob file 20 is in the Version, 30 is recorded as dropped and 40 is
1190+
// missing without a record.
1191+
EnableDroppedBlobRepair({20}, {30});
1192+
BlobIndex healthy;
1193+
BlobIndex dropped;
1194+
BlobIndex unrecorded;
1195+
ASSERT_OK(healthy.DecodeFrom(BlobIndexForFile(20)));
1196+
ASSERT_OK(dropped.DecodeFrom(BlobIndexForFile(30)));
1197+
ASSERT_OK(unrecorded.DecodeFrom(BlobIndexForFile(40)));
1198+
1199+
// Only the column in the dropped blob file is removed.
1200+
std::string mixed;
1201+
ASSERT_OK(WideColumnSerialization::SerializeV2(
1202+
WideColumns{{"", "x"}, {"c1", ""}, {"c2", ""}, {"c3", ""}},
1203+
{{1, dropped}, {2, healthy}, {3, unrecorded}}, mixed));
1204+
std::string mixed_repaired;
1205+
ASSERT_OK(WideColumnSerialization::SerializeV2(
1206+
WideColumns{{"", "x"}, {"c2", ""}, {"c3", ""}},
1207+
{{1, healthy}, {2, unrecorded}}, mixed_repaired));
1208+
// An entity without dropped columns is left as is.
1209+
std::string healthy_only;
1210+
ASSERT_OK(WideColumnSerialization::SerializeV2(WideColumns{{"c2", ""}},
1211+
{{0, healthy}}, healthy_only));
1212+
// An entity that loses every column becomes a deletion.
1213+
std::string dropped_only;
1214+
ASSERT_OK(WideColumnSerialization::SerializeV2(WideColumns{{"c1", ""}},
1215+
{{0, dropped}}, dropped_only));
1216+
1217+
RunTest({test::KeyStr("a", 5, kTypeWideColumnEntity),
1218+
test::KeyStr("b", 5, kTypeWideColumnEntity),
1219+
test::KeyStr("c", 5, kTypeWideColumnEntity),
1220+
test::KeyStr("c", 3, kTypeValue)},
1221+
{mixed, healthy_only, dropped_only, "older"},
1222+
{test::KeyStr("a", 5, kTypeWideColumnEntity),
1223+
test::KeyStr("b", 5, kTypeWideColumnEntity),
1224+
test::KeyStr("c", 5, kTypeDeletion)},
1225+
{mixed_repaired, healthy_only, ""});
1226+
}
1227+
11441228
TEST_P(CompactionIteratorTest, MissingBlobRepairSeekAndNextUseRewrittenKeys) {
11451229
EnableMissingBlobRepair({});
11461230
VectorIterator input(
11471231
{test::KeyStr("a", 5, kTypeBlobIndex), test::KeyStr("b", 4, kTypeValue)},
11481232
{BlobIndexForFile(10), "healthy"}, &icmp_);
1149-
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0);
1233+
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0,
1234+
/*only_dropped_blob_files=*/false);
11501235
const std::string deletion = test::KeyStr("a", 5, kTypeDeletion);
11511236
repair.Seek(deletion);
11521237
ASSERT_OK(repair.status());
@@ -1173,14 +1258,16 @@ TEST_P(CompactionIteratorTest,
11731258
{
11741259
std::unique_ptr<InternalIterator> input(
11751260
NewErrorInternalIterator(Status::IOError("Failed SST read")));
1176-
MissingBlobRepairIterator repair(input.get(), repair_storage_.get(), 0);
1261+
MissingBlobRepairIterator repair(input.get(), repair_storage_.get(), 0,
1262+
/*only_dropped_blob_files=*/false);
11771263
ASSERT_FALSE(repair.Valid());
11781264
}
11791265
VectorIterator input({test::KeyStr("a", 5, kTypeWideColumnEntity)},
11801266
{"invalid entity"});
11811267
input.SeekToFirst();
11821268
ASSERT_TRUE(input.Valid());
1183-
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0);
1269+
MissingBlobRepairIterator repair(&input, repair_storage_.get(), 0,
1270+
/*only_dropped_blob_files=*/false);
11841271
}
11851272

11861273
// It is possible that the output of the compaction iterator is empty even if
@@ -2321,7 +2408,8 @@ TEST_P(CompactionIteratorTsGcTest,
23212408
input.SeekToFirst();
23222409
ASSERT_TRUE(input.Valid());
23232410
MissingBlobRepairIterator repair(&input, repair_storage_.get(),
2324-
cmp_->timestamp_size());
2411+
cmp_->timestamp_size(),
2412+
/*only_dropped_blob_files=*/false);
23252413
}
23262414

23272415
TEST_P(CompactionIteratorTsGcTest, MissingBlobRepairRespectsHistoryTrimming) {
@@ -2336,7 +2424,8 @@ TEST_P(CompactionIteratorTsGcTest, MissingBlobRepairRespectsHistoryTrimming) {
23362424
PutFixed64(&trim_timestamp, 103);
23372425
HistoryTrimmingIterator trimmed(&input, cmp_, trim_timestamp);
23382426
MissingBlobRepairIterator repair(&trimmed, repair_storage_.get(),
2339-
cmp_->timestamp_size());
2427+
cmp_->timestamp_size(),
2428+
/*only_dropped_blob_files=*/false);
23402429
repair.SeekToFirst();
23412430
ASSERT_OK(repair.status());
23422431
ASSERT_TRUE(repair.Valid());

0 commit comments

Comments
 (0)