Repository navigation
Conversation
Re-adopting a standby-restored segment held snapshot_mutex_ exclusively from the first validation to the last replica, so a remount stopped every operation on the master for as long as it took to walk all 1024 metadata shards. With a store of a few million objects that walk is seconds, and concurrent remounts ran one after another because each one held the lock for its whole duration. Nothing in the walk mutates anything: it reads each shard to find the replicas that live on the segment, and rebuilds the segment's allocator from the regions they occupy. ReMountSegment now prepares under the shared lock, where it blocks only the snapshot fork, and commits under the exclusive one. The commit stage publishes the segment and re-points the replicas the scan found by looking their keys up directly, so its critical section is bounded by the replica count on the segment rather than by the size of the store. Splitting the operation means the scan's findings can go stale: - A replica can disappear before the commit stage reaches it. The scan therefore records the key rather than a Replica*, and the commit stage looks the key up again and re-matches the replica against the region its imported buffer covers. A buffer nobody claims is dropped, which returns its region to the allocator that has just taken ownership of it. - An HA restore can rewrite the standby state the scan read. A generation counter, advanced by every restore, is compared across the gap; a remount that loses the race refuses and the client retries. The allocator the mount creates covers the whole segment as free space, so it must not be visible to an allocation before it is replaced by the rebuilt one. The mount and the swap happen in one segment-manager critical section inside the exclusive stage. What an HA restore leaves behind for such segments -- the segment list, the bytes still charged to them, the allocators keeping their buffers alive and which endpoints are unreadable -- was four MasterService members keyed three different ways. It becomes OrphanedSegments, one record per segment, with an interface that follows the segment's lifecycle: Reset on restore, Find while preparing a remount, Adopt when committing it, IsUnreadable for readers and ReleaseAll on shutdown. Reset is the only way to install orphans and always advances the generation, so the check the commit stage relies on cannot be bypassed. The placeholder allocators a restore creates for NoF replicas are kept apart from it, since no remount adopts them. Adopting a segment now releases everything still charged to it. Before, only the bytes of the replicas the remount found were released: the bytes of replicas that died while the segment was orphaned stayed on the segment's allocated-memory metric until the next restore or shutdown, and the leftover entry sent later remounts of the segment through the full scan again. The re-adopted allocator already counts the surviving replicas itself. The scan no longer copies the buffer descriptor of every memory replica in the store, only of those on the segments being re-adopted.
Aionw
requested review from
00fish0,
Icedcoco,
Libotry,
XucSh,
YiXR,
stmatengss and
ykwd
as code owners
September 23, 2026 08:44
5 of 32 tasks
This was referenced Oct 15, 2026
Open
Merged
10 of 12 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
After an HA promotion,
ReMountSegmentwalks all 1024 metadata shards to find the replicas that survived on a standby-restored segment, and until now it did that holdingsnapshot_mutex_exclusively, from the first validation to the last replica. Every data-plane operation on the master waits for the whole walk, which is seconds on a store with a few million objects, and concurrent remounts run one after another. This is the snapshot-lock half of the join-storm stall described in #3937 and RFC #4181 (scenario S1).What changes
Two stages.
ReMountSegmentnow prepares under the shared lock and commits under the exclusive one:PrepareRemount(shared): validates the request, scans the shards for surviving replicas and rebuilds each segment's allocator from the regions they occupy. None of this mutates anything, so it only blocks the snapshot fork.CommitRemount(exclusive): mounts the segment, swaps in the rebuilt allocator, re-points the replicas by key and ends the orphan. Its critical section is bounded by the number of replicas on the segment, not by the size of the store.Splitting the operation lets the scan's findings go stale, which the commit stage handles:
Replica*; the commit stage looks the key up again and re-matches the replica against the region of the buffer imported for it. A buffer nobody claims is dropped while the exclusive lock still covers the allocator its region returns to.UNAVAILABLE_IN_CURRENT_STATUSand the client retries.OrphanedSegments(include/ha/orphaned_segments.h). What an HA restore leaves behind for such segments — the segment list, the bytes still charged to them, the allocators keeping their buffers alive and which endpoints are unreadable — was fourMasterServicemembers keyed three different ways, stitched together by callers. It is now one record per segment, and the interface follows the segment's lifecycle:Reseton restore,Findwhile preparing a remount,Adoptwhen committing it,IsUnreadablefor readers,ReleaseAllon shutdown.Resetis the only way to install orphans and always advances the generation, so the check the commit stage depends on cannot be skipped by a future restore path.Behavior change: allocated-memory metrics. Adopting a segment now releases everything still charged to it. Before, only the bytes of the replicas the remount found were released, so the bytes of replicas that died while their segment was orphaned stayed on that segment's allocated-memory metric until the next restore or shutdown, and the leftover entry sent later remounts of the segment through the full scan again. The re-adopted allocator already counts the surviving replicas itself (
OffsetBufferAllocator::Restore,CachelibBufferAllocator::adoptImportedBuffer).Smaller changes:
unreadable_segments=<n>(a segment count) instead ofinvalid_endpoints=<n>(an alias count).Relation to open work
client_mutex_off this path and [Store] Serve Ping on a separate RPC server that clients discover on Connect #4183 movesPingonto its own RPC server; this PR shortens thesnapshot_mutex_hold. Neither alone stops the mass expiry. This is meant to land before [Store] Centralize client session lifecycle and resource ownership #4180, which will rebase onto it.restored_nof_allocators_andstandby_nof_allocator_keepalive_into one; the NoF quarantine stays outsideOrphanedSegments, which only tracks memory segments awaiting a remount.ReMountSegment's signature for a different purpose; the two will conflict textually in the same function.Size
+586/−315 lines outside tests. Most of the
master_service.cppchurn is the existingReMountSegmentbody moving into the stage helpers (net +271 in that file); the design context is in RFC #4181 and in this description. It merges cleanly withmainat e88aacf.Module
mooncake-transfer-engine)mooncake-store)mooncake-conductor)mooncake-reshard)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
How Has This Been Tested?
Test commands:
New tests:
orphaned_segments_test: the class's invariants — everyResetadvances the generation (identical contents included),Findrejects a request that contradicts its orphan or names two, unmounted orphans are unreadable by endpoint and by name,Adoptends the orphan, releases its allocator and returns its whole charge,Resetreturns the charges it discards.MasterServiceHATest.RemountReleasesTheChargeOfReplicasThatDiedOrphaned: restores two replicas, erases one before the remount, and checks the segment's allocated-memory metric ends at the survivor's size. It would read 2048 instead of 1024 under the old accounting.Not covered: no test drives the two stages across the lock gap (a replica erased between them, or a restore landing between them). Those paths are exercised only by review.
Test results:
Checklist
./scripts/code_format.shPre-commit note:
cmake-formatrewrapsdynamic_replication_lease_table_testinmooncake-store/tests/CMakeLists.txt, a line that is already onmainand not touched here; that rewrap is left out. Every other hook passes. No separate RFC was filed; see Size above.AI Assistance Disclosure
Claude Code wrote the implementation and tests under the submitter's direction; the submitter decided the design at each step (splitting the stages, keeping test seams out of production code, the shape of
OrphanedSegments, and taking the metrics behavior change). OpenAI Codex ran independent review passes over the diff.