Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/tasks/batch_write_task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,27 @@ KvError BatchWriteTask::Truncate(std::string_view trunc_pos)
FreePage(page_id);
}

// Also free the large-value segment pages. Without this the segment
// mapping stays populated and is serialized into the empty snapshot, so
// GC keeps the segment files forever (only DropTable would reclaim
// them). The partial-truncate path frees these via DelLargeValue.
if (cow_meta_.segment_mapper_ != nullptr)
{
PageMapper *seg_mapper = cow_meta_.segment_mapper_.get();
const auto &seg_tbl = seg_mapper->GetMapping()->mapping_tbl_;
for (PageId seg_id = 0; seg_id < seg_tbl.size(); ++seg_id)
{
auto val_type =
MappingSnapshot::GetValType(seg_tbl.Get(seg_id));
if (val_type == MappingSnapshot::ValType::Invalid ||
val_type == MappingSnapshot::ValType::PageId)
{
continue;
}
seg_mapper->FreePage(seg_id);
}
}

cow_meta_.root_id_ = MaxPageId;
cow_meta_.ttl_root_id_ = MaxPageId;
cow_meta_.next_expire_ts_ = 0;
Expand Down
64 changes: 64 additions & 0 deletions tests/large_value_gc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,23 @@ void WaitForGc(int ms = 800)
std::this_thread::sleep_for(chrono::milliseconds(ms));
}

template <typename Pred>
bool WaitForCondition(chrono::milliseconds timeout,
chrono::milliseconds step,
Pred &&pred)
{
auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline)
{
if (pred())
{
return true;
}
std::this_thread::sleep_for(step);
}
return pred();
}

} // namespace

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -313,6 +330,53 @@ TEST_CASE(
CleanupStore(opts);
}

// Full-partition Truncate must also retire the large-value segment files. Its
// fast path used to free only the data mapper, leaving the segment mapping
// populated so GC kept every segment file forever.
TEST_CASE("full truncate of a large-value partition retires every segment file",
"[large-value-gc][pinned]")
{
PinnedHarness h;
eloqstore::KvOptions opts =
MakePinnedOpts(h, /*file_amp=*/2, /*seg_amp=*/2);
eloqstore::EloqStore *store = InitStore(opts);

eloqstore::TableIdent tbl{"lvgc_truncate_all", 0};
const size_t seg = h.SegmentSize();

{
std::vector<PinnedBatchEntry> entries;
entries.push_back(
{"k_a", h.AllocateSegmentAligned(seg * 2), 0x2010, "meta-a"});
entries.push_back(
{"k_b", h.AllocateSegmentAligned(seg * 3), 0x2020, "meta-b"});
entries.push_back(
{"k_c", h.AllocateSegmentAligned(seg * 1), 0x2030, "meta-c"});
WritePinnedBatch(store, tbl, std::move(entries), /*ts=*/1);
}

SegmentFileInfo before = InspectSegmentFiles(opts, tbl);
REQUIRE(before.any);

{
eloqstore::TruncateRequest req;
req.SetArgs(tbl, std::string{}); // empty position -> full truncate
store->ExecSync(&req);
REQUIRE(req.Error() == eloqstore::KvError::NoError);
}

// Nudge so UpdateMeta/GC runs on the now-empty segment mapping, then poll
// until GC has retired the segment files.
WriteSmall(store, tbl, "nudge", "x", /*ts=*/2);
REQUIRE(WaitForCondition(chrono::seconds(10),
chrono::milliseconds(20),
[&]
{ return !InspectSegmentFiles(opts, tbl).any; }));

store->Stop();
CleanupStore(opts);
}

// ---------------------------------------------------------------------------
// Overwrite metadata-bearing pinned values many times. GC must reclaim the
// dead segment files (those whose segments are no longer referenced by the
Expand Down
Loading