[Bugfix] Roll back failed OffsetAllocator evictions#2964
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a transactional eviction mechanism in the OffsetAllocatorStorageBackend. It adds a PendingEviction structure to hold evicted metadata and allocation handles, preventing their reuse until the master accepts the replica-removal notification. If the notification fails, the eviction is rolled back using the new RestorePreparedEviction method. A unit test has been added to verify this rollback behavior on notification failure. There are no review comments, so no feedback is provided.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
he-yufeng
left a comment
There was a problem hiding this comment.
Thanks for tightening this up — the core change is correct and closes a real consistency hole. Before this, if eviction_handler rejected the removal we'd already have erased the victim from shard.map and dropped its AllocationPtr, so the extent could be reused while the master still believed the local replica existed. Pinning the evicted ObjectEntry (and with it the refcounted AllocationPtr) in PendingEviction until the notify succeeds, then committing by dropping the last ref or restoring on rejection, is the right shape. The emplace_back(vkey, it->second) copy is safe precisely because AllocationPtr is a shared_ptr, so the subsequent shard.map.erase(it) doesn't free the extent. Totals stay symmetric (sub at prepare, add on restore, untouched on commit), and the fallback loop's switch to pending_eviction.objects.size() is fine since pending is always drained at the top of each turn.
Two things I'd like to resolve before approving.
-
The refactor drops the
eviction_handlernull check that both notify sites used to carry (if (eviction_on && eviction_handler && ...)), andNotifyAndCommitPreparedEvictioninvokeseviction_handler(evicted_keys)unconditionally. The interface declaresEvictionHandler eviction_handler = nullptrand documents it as optional, so an emptystd::functionis a legal argument. The sole production caller infile_storage.cppalways passes a live handler, so this isn't reachable today — but a test or future caller that runs witheviction_policy != NONEand no handler would now hitstd::bad_function_callinstead of the old "evict locally, skip master notify" behavior. Could you early-return inside the helper to preserve that, e.g.if (!eviction_handler) { pending.objects.clear(); return {}; }at the top? That keeps the null-handler path a local-only commit like before. -
RestorePreparedEvictionusesCHECK(map_inserted)/CHECK(fifo_inserted), which abort the process if the key or FIFO slot is already present. The notify call runs witheviction_mutex_released (EvictToMakeRoom unlocks on return, and the handler is invoked before restore re-locks), so there's a window where another writer could re-insert an evicted key or reuse itsfifo_seq. IsBatchOffloadserialized against a concurrentPut/offload of the same key such that this can't happen? If the invariant holds, a one-line comment on the CHECKs would make it self-documenting; if it doesn't, I'd rather reconcile (keep the live entry / re-seat under a fresh seq) than turn a recoverable master rejection into a hard abort.
Neither is a big change — happy to approve once the null-handler guard is back and the restore-under-concurrency question is settled.
|
@he-yufeng Addressed both points in
|
he-yufeng
left a comment
There was a problem hiding this comment.
Re-reviewed after the rollback-assumptions commit. The core is correct and the guard commit closes the robustness gap cleanly.
The pin-until-notify shape is right: EvictToMakeRoom moves each victim's ObjectEntry into PendingEviction before erasing it from shard.map, and since AllocationPtr is a shared_ptr the extent stays alive (not returned to the allocator) until NotifyAndCommitPreparedEviction either drops the last reference on success or hands the entry back to RestorePreparedEviction on rejection. Totals stay symmetric (sub at prepare, add on restore, untouched on commit), and RestorePreparedEviction copies total_size/fifo_seq into locals before the std::move, so there's no use-after-move.
I also checked the sequencing concern: every NotifyAndCommitPreparedEviction runs before the following allocate() (the (B) block ahead of (C), and again inside the fallback loop before each retry), so committed extents are freed in time and holding the pin doesn't starve the allocation.
On the CHECK in RestorePreparedEviction: it's safe because the offload path is genuinely single-writer. BatchOffload is only reached through FileStorage::OffloadObjects, which is only called from Heartbeat(), which only runs on the single heartbeat_thread_ (nothing else calls either, and the client-buffer GC thread never offloads). So no writer can recreate the key or reuse its FIFO sequence during the notify window and the CHECK can't fire in correct operation. Documenting the precondition inline and warning future maintainers to revisit the DCHECK_GE guards if concurrent offload is ever added is the right call.
LGTM.
LujhCoconut
left a comment
There was a problem hiding this comment.
A clean and correct fix. LGTM. OffsetAllocatorStorageBackend is still not production ready. Thanks for making this further.
…end-persist Resolve conflicts in mooncake-store/src/storage_backend.cpp against upstream kvcache-ai#2964 (roll back failed OffsetAllocator evictions): - Adopt upstream's PendingEviction + NotifyAndCommitPreparedEviction flow in BatchOffload: eviction victims stay pinned until the master accepts their removal, and failed notifications roll the metadata back via RestorePreparedEviction. - Move eviction tombstone recording from EvictToMakeRoom (victim selection time) to commit time via the new RecordEvictionTombstones, so rolled-back evictions never leave stale tombstones that Phase C would wrongly delete on recovery. - Take upstream's storage_backend_test.cpp verbatim: the branch's changes there were lambda-formatting churn only, and upstream adds rollback/post-loop-flush eviction coverage. Verified: storage_backend_test 72/72 passed.
Description
Fixes #2963.
OffsetAllocatorStorageBackendpreviously removed FIFO victims before notifying the master. If notification failed, the local replicas were lost while the master could retain staleLOCAL_DISKmetadata.This PR keeps victims and allocation handles pending until notification succeeds, restores the shard map, FIFO index, and counters on failure, and adds a regression test covering rollback plus retry order.
#2932 also touches OffsetAllocator for persistence, but does not address notification-failure rollback; this PR is limited to that correctness issue.
Module
mooncake-store)Type of Change
How Has This Been Tested?
Test commands:
Test results:
Checklist
./scripts/code_format.shpre-commit run --all-filesand all hooks pass (repository-wide hooks currently fail on unrelated existing files; touched-file hooks pass)AI Assistance Disclosure
Codex assisted with diagnosis, implementation, testing, and review.