mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #2979] [RFC]: [Store] Entity Ownership Decoupling, SegmentView, and Recoverable Lifecycle #770
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#770
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Icedcoco on GitHub (Jul 17, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/2979
Changes proposed
Summary
This RFC proposes a first-version redesign of Mooncake Store Master metadata ownership and resource lifecycle. The proposal separates logical object metadata, replica state, allocation handles, and client/segment liveness while keeping the implementation deliberately small:
Objectowns itsReplicainstances.Replicaowns oneAllocatedBuffer.AllocatedBufferpoints to its containingSegment.Segmentowns the allocator and publishes its lifecycle through an independentSegmentView.shared_ptrownership graph are introduced.Objecthas no lifecycle state. When its replica collection becomes empty, the object is removed from the object map and then destroyed.The design is intended to complement RFC #2953 rather than replace its client liveness semantics. RFC #2953 supplies the client-level recovery window; this RFC defines how that signal affects Segments, Replicas, allocation handles, metadata visibility, and HA promotion.
Motivation
The current Master implementation keeps resource ownership and lifecycle information spread across Object, Replica, Segment, allocator, task, and HA paths. This creates several practical risks:
The first version should solve these problems without introducing a general object registry, a new Replica state machine framework, or a new generation protocol for Objects.
Goals
Replica*.Non-goals
shared_ptrownership graph for core entities.Entity model
Object
ObjectretainsObjectKey, logical size, data type, tenant/group metadata, lease/pin/quota/accounting information, a per-object monotonic ReplicaId counter, andvector<Replica*>.Object does not retain client id or Segment id. It has no independent lifecycle state. A Replica is removed from the vector only after its buffer has been released. Once the vector is empty, the Object is first erased from the object map and then destroyed.
group_idis immutable while the Object exists, matching the current code's validation.sizemay change during destructive Upsert; cleanup and quota accounting must use the sizes recorded in each AllocatedBuffer rather than the current Object size.Replica
Each Replica retains its ObjectKey and a 32-bit ReplicaId. IDs are allocated monotonically per Object and are not reused while the Object exists. Copy and Move create a new target Replica with a new ID; the source ID does not change.
The first version keeps the current status values:
It does not add a generic Replica state-machine abstraction. Replica methods should own common getters, descriptor access, read/write eligibility checks, reference-count changes, and the small set of legal status changes. They do not write HA storage directly.
AllocatedBuffer
AllocatedBufferis a logical allocation handle, not ownership of the whole client memory region. It stores its containing Segment pointer, requested and reserved sizes, and the medium-specific allocation descriptor (offset, address, file locator, or equivalent).Its destruction synchronously returns the allocation to the local allocator and decrements
Segment.active_allocations. A separatereleased_flag is not required. Local release is expected to be infallible; an error is an invariant failure rather than an ordinary business result.Segment and SegmentView
Segmentowns the allocator and runtime connection object. Its frequently read lifecycle state is an atomic value. The fixed states are:DRAINEDis intentionally not a state; a drained Segment proceeds toUNMOUNTING.All durable Segment information is stored in an independent SegmentView store. SegmentView is not part of Object/Replica oplog or snapshot state and is the sole authority for Segment identity, medium, endpoint/connection information, lifecycle state, deadlines, reason, and connection generation.
The primary writes SegmentView first and publishes the new local atomic state only after the backend write succeeds. A failed write leaves the old local state in place and is retried. Ordinary GET/PUT/Copy/Move continue using the previous state; they do not receive an artificial retry result merely because the state publication backend is temporarily unavailable.
Segment lifecycle
The normal transitions are:
GRACEFUL_UNMOUNTINGnever silently returns toACTIVE. Reconnection keeps the Segment in graceful unmounting; cancelling that process requires an explicit future management event.SUSPECTEDrejects new reads, writes, allocation, Copy, Move, and task dispatch. Already-running tasks may finish and report their result so that Replica status, references, and allocation accounting converge.UNMOUNTINGrestores existing Object/Replica/task metadata but rejects new work. PROCESSING Replicas are allowed to return normally. The first version does not periodically cancel them or force cleanup after a timeout.UNMOUNTEDmeans the allocator has been destroyed, every AllocatedBuffer has been released,active_allocations == 0, and the final SegmentView state has been persisted. A future mount uses a new Segment UUID and is unrelated to the old Segment.Primitive Replica events and oplog
The system uses a finite set of primitive events:
The durable oplog mapping is:
REMOVE_DURABLE,FINALIZE, and reference-count changes are local cleanup steps and do not create another logical oplog entry.Object/Replica methods validate and mutate in-memory state. A shared Master mutation helper owns HA/non-HA persistence, ordered submission, retry policy, durable callbacks, and finalization. This avoids scattering oplog writes through Put, Copy, Move, Evict, and cleanup implementations.
Composite operations are expressed using primitive events:
Move must submit
REPLICA_COMPLETE(target)beforeREPLICA_REMOVE(source)to the same ordered writer. It need not wait for the first durable callback. The ordered writer guarantees that a durable remove implies that the preceding complete was accepted.Upsert is intentionally destructive in the first version. Regardless of size, old Replicas are marked removed and their remove events are submitted first; only then is the Object's logical size updated and a new processing Replica created. This permits a short interval with no readable Replica and accepts the corresponding data-loss risk on failure. The old Object structure is not deleted before old Replica cleanup completes.
HA recovery and promotion
SegmentView is a strict recovery prerequisite. Standby/promotion must not recover Object/Replica resources from a missing or incomplete SegmentView. When SegmentView cannot be read, the standby retries with backoff. If the old Master lease is lost during this period, the partial recovery is discarded and the process returns to standby instead of entering serving.
The promotion sequence is:
active_allocations.Allocation conflicts, capacity mismatches, or descriptor mismatches are treated as program/link errors in this first version. The affected Replica and the conflicting Replica are marked for removal and cleaned. The design relies on recorded SegmentId, ReplicaId, requested/reserved size, descriptor, and ordered replay to make such conflicts unreachable in normal operation.
The long-term direction is hot-standby promotion. Snapshot allocator internals are compatibility data only and should eventually be removed from snapshots.
Concurrency and pointer safety
Version one keeps metadata shard locks for Object/Replica lifecycle, status, reference counts, vector mutation, and object accounting. SegmentManager's lock protects Segment and allocator lifetime. The lock order is:
Async tasks do not retain entity pointers. They carry ObjectKey, ReplicaId, SegmentId, and task-specific descriptors, then re-lookup the entity under the appropriate lock.
Replica*is therefore not an asynchronous ownership mechanism.Relationship to RFC #2953
RFC #2953 proposes a useful client-level
Active -> Suspected -> Offlinerecovery window and a common serving-eligibility decision. This RFC keeps that concept, but places the resource lifecycle boundary at Segment and Replica:The two designs can coexist: a client liveness record can drive Segment state transitions, while SegmentView and Replica events define the exact resource cleanup and replay behavior.
Rollout
Open questions
These are implementation details, not blockers for the architecture:
Any feedback is appreciated!
Before submitting a new issue...
@github-actions[bot] commented on GitHub (Jul 17, 2026):
Thanks for opening this issue, @Icedcoco!
102317026A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@Aionw commented on GitHub (Jul 23, 2026):
Thanks for putting this RFC together. I like the overall direction, especially making entity lifetimes easier to reason about and avoiding long-lived entity pointers in asynchronous tasks. I have a few suggestions and questions.
1. Separate the two main concerns more clearly
The RFC currently covers both:
I think the proposal would be easier to review if these were presented as two clearly separated parts, even if they remain in the same RFC.
The first part could focus on the entity ownership graph, destruction order, identity, locking, and in-memory mutation semantics. The second part could focus on persistence, replay, fencing, and recovery. The connection between them would then be the stable identities and primitive mutation events.
2. Use explicit ownership types for owning relationships
For relationships that represent ownership, I would prefer value semantics or
std::unique_ptrrather than raw pointers. For example:I agree that a
shared_ptrownership graph may not be necessary. However, usingunique_ptrfor exclusive ownership would make the intended destruction order explicit and reduce the amount of lifetime reasoning required from callers.For non-owning relationships, scoped references or ID-based lookup may be more appropriate. It would be helpful if the RFC explicitly distinguished owning relationships, temporary borrows, and asynchronous references.
3. Clarify the locking and performance model for ID-based lookup
Using IDs in asynchronous tasks is safer than retaining entity pointers, but I think the RFC should clarify the expected locking model and its performance implications.
The current Copy, Move, and Promotion completion paths already acquire a metadata shard lock and resolve Replicas by ID, so ID lookup itself may not add much overhead. The larger concern would be an implementation that acquires the lock separately for every primitive event.
Ideally, one task completion would:
It would also be good to avoid requiring a global SegmentManager lookup for every Replica lookup. Some basic performance validation around lock contention, callback latency, and hot-key workloads would help confirm that the new abstraction does not introduce a regression.
4. Revisit the Segment and Replica state definitions
I am not yet fully convinced that the proposed state sets capture the right concepts.
For Segment, the current model appears to combine lifecycle, client or connection health, drain policy, and recovery readiness. For example,
SUSPECTEDmay belong to client/connection health rather than Segment lifecycle. Otherwise, a Segment could conceptually be bothDRAININGandSUSPECTED, but a single enum cannot represent that combination.Similarly,
GRACEFUL_UNMOUNTINGmay be a draining mode with a reason and deadline rather than a separate lifecycle state. On the other hand, removingDRAINEDchanges the current distinction between “drain completed” and “unmount started,” so it would be useful to explain whether this semantic change is intentional.For Replica, some retained states such as
INITIALIZED,REMOVED, andFAILEDdo not yet have clearly defined transition paths, while HA-related phases such as “write completed but not yet durable” or “removal requested but not yet durable” may need to be represented.A transition table for both Segment and Replica would make this much easier to evaluate. It could describe, for each state and transition:
Overall, I agree with the direction of the RFC. Clarifying these boundaries would make the ownership and recovery model easier to reason about and implement incrementally.
@Icedcoco commented on GitHub (Jul 24, 2026):
Hi @Aionw . Thanks for the detailed comments. I agree with the main concerns. The RFC is still in planning, so I’ll first clarify the intended direction here before rewriting the RFC text.
1. Splitting ownership and HA concerns
I agree the current RFC mixes two related but different layers:
I plan to keep them in the same RFC, but split the document into two clearly separated sections later. The first section will focus on Object/Replica/AllocatedBuffer/SegmentRuntime ownership, destruction order, borrow rules, lock scope, and async identity lookup. The second section will focus on lifecycle events, oplog replay, durable callbacks, Segment persistence, rollout, and rollback.
The connection between the two sections should be stable identities and primitive mutation events, not long-lived entity pointers in async tasks.
2. Ownership types
I agree owning relationships should be explicit. For Replica ownership,
unique_ptrmakes sense.The target direction is:
For the concrete C++ representation, I currently lean toward:
But high-frequency observation should still use raw non-owning pointers where the lifetime is guaranteed. For example,
AllocatedBuffercan keep a rawSegmentRuntime*plusSegmentIdfor validation/debug/recovery. The raw pointer is not ownership; it is a hot-path observer.The allocator model should eventually be cleaned up too. Instead of relying on
shared_ptr/weak_ptrto detect whether the allocator is still alive, the goal is to make the lifecycle explicit:So
shared_ptr/weak_ptrmay be a current implementation detail, but not the target ownership model.3. Locking and ID-based lookup
I agree with the locking concern. The intended model is not one lock acquisition per primitive event.
For one completion or mutation batch, the expected flow is:
Long operations must stay outside metadata/segment locks, including disk/network IO, etcd transactions, snapshot encoding/upload, and durable waits. If a path releases the lock and later resumes, it must re-check by stable identity rather than retaining an entity pointer.
So async tasks should carry identity, while scoped synchronous code may borrow raw pointers under the owning lock.
4. Segment and Replica states
I agree the RFC should clarify this with transition tables.
For Segment, I think
DRAINEDshould stay. It has a useful meaning distinct fromUNMOUNTING: drain is complete, but the segment has not been removed yet.I also agree
SUSPECTEDis better modeled as a health/admission overlay rather than a lifecycle state. So instead of encoding product states likeSUSPECTED_DRAINING, the intended model is closer to:For now,
SUSPECTEDonly needs to define behavior forOK,DRAINING,DRAINED, andGRACEFULLY_UNMOUNTING. Helper predicates such asCanAllocate,CanServeRead, andCanContinueCleanupcan hide the two-field checks from callers.For Replica, I do not intend to introduce a heavy state-machine framework. The state set should be small and monotonic:
FAILEDis not intended as a long-lived serving state; it should converge toREMOVED.Durability should not become extra Replica states. For example, “complete submitted but not durable” is a pending lifecycle event, not a separate
ReplicaStatus. The durable oplog event/callback drives the next action:For Segment persistence / DurableSegmentView, I do not want to commit to an independent keyspace too early. The first step should be persist-before-publish and reader/apply hardening. Whether to split out a separate DurableSegmentView should be decided later based on actual recovery ambiguity, write frequency, allocator rebuild behavior, and rollout data.
@github-actions[bot] commented on GitHub (Oct 22, 2026):
This issue has had no activity for 90 days and will be closed in 7 days if there is no further activity. Please comment or react if it should stay open.
@github-actions[bot] commented on GitHub (Oct 29, 2026):
Closing due to 3 months of inactivity. If this is still relevant, please comment and we can reopen.