diff --git a/src/tasks/batch_write_task.cpp b/src/tasks/batch_write_task.cpp index d7093b51..e6f181d1 100644 --- a/src/tasks/batch_write_task.cpp +++ b/src/tasks/batch_write_task.cpp @@ -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; diff --git a/tests/large_value_gc.cpp b/tests/large_value_gc.cpp index 8b988fb0..a4118a31 100644 --- a/tests/large_value_gc.cpp +++ b/tests/large_value_gc.cpp @@ -250,6 +250,23 @@ void WaitForGc(int ms = 800) std::this_thread::sleep_for(chrono::milliseconds(ms)); } +template +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 // --------------------------------------------------------------------------- @@ -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 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