Conversation
|
All lanes are green now, including CTest at 25m. Getting here took four CI rounds and each one caught something real, so a short account for reviewers:
Neither of those would have been caught without actually running the suite, and I could not run it locally: this machine has no rdma-core and the bundled fmt headers do not compile under Apple clang, which reproduces on a clean upstream checkout too, so it is the environment and not this diff. All test validation above is from CI, and I have tried to keep each fix small enough that the reasoning is checkable from the diff alone. One pre-existing failure that is not this diff: The six migrated HA tests are the contract change reviewers should look at most carefully: bad entries are now skipped per object with a |
|
Coordination note: #3811 (Icedcoco's PR-I00 ReplicaID preservation) touches the same RestoreFromStandbySnapshot and the same replica construction sites, so whichever lands second will need a real merge, not a rebase-and-go. The designs compose cleanly in one direction: this PR's per-entry tolerant restore can carry ID preservation per object too (an invalid or duplicate ReplicaID becomes a rejected entry with a reason label, next to the other per-entry rejections, rather than a wholesale INVALID_PARAMS). The one semantic decision for reviewers is whether duplicate or invalid ReplicaIDs should reject just that object (this PR's model) or the whole restore (#3811's current model). Happy to rebase and integrate the ID-preserving constructors here once the sequencing is clear, or to leave that merge to whoever lands second. |
|
One addendum to the SSD EvictObject note above: #3771 was the production-side fix I found first (lease-expiry cleanup wiping restored objects), and #3813 (just opened) offers a test-side explanation for the same failure: the eviction worker racing the original-vs-restored comparison in the shared snapshot fixture. Both account for the observed signature; which one the maintainers treat as the real root is theirs to call, and it does not change anything in this diff either way. |
|
I recommend deferring this PR rather than merging the current implementation. The motivation is valid: one malformed standby descriptor should not discard the entire restored index. However, the current PR changes a central promotion path while the OpLog/Snapshot workflow is still experimental and not a committed production contract. There is no immediate production requirement to land this semantic change, so I would prefer to align it with the N07/N08 promotion and recovery work in roadmap #3808. The main design issue is the For the initial recovery contract, an overlap must be treated as ambiguity: discard every descriptor participating in the overlapping range (including transitive overlap groups), and keep only replicas that are independent of the conflict. If an object has no reliable replica left, discard that object. Do not discard unrelated objects merely because another object conflicts. There are also correctness issues that should be resolved independently of the policy:
This PR also does not implement the pre-promotion dry run or promotability predicate discussed in #3774, and the restore path still does not preserve Given the current development status and the absence of an urgent production hotfix requirement, I recommend keeping #3760 open as a tracked restore validation issue and carrying this work into the N07/N08 design. A follow-up should define:
The CI results are useful and currently green, but they do not establish these recovery semantics. I would not merge #3806 in its current form. |
…d on empty restores Reworked from review by Icedcoco on kvcache-ai#3806: - Overlap is no longer "latest replay wins": the promotion input comes from StandbyMetadataStore::Snapshot(), which enumerates unordered maps, so no replay order exists to pick a winner by. Every descriptor in a transitively overlapping group is discarded, replicas independent of the conflict are kept, and an object is dropped only when no reliable replica remains. - pending_ranges no longer records accepted.size() mid-loop: ranges join only after the object is accepted, so a rejected object cannot leave a range whose owner index a later object would reuse. - Segment accounting now accumulates per object and commits once the whole object validates, so a mid-construction rejection leaves no phantom bytes behind for later capacity checks or metrics. - A snapshot whose objects all fail to land returns INVALID_PARAMS instead of an empty-cluster success, except when every rejection is an object the live index already holds. Tests pin the new semantics in both suites: per-descriptor discard with independent-replica survival, transitive overlap groups, the stale-range owner regression, accounting rollback, all-rejected failure, and mixed good+bad tolerance. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
|
Thanks for the close read. I checked each point against the code before answering, and you are right on the substance. Reworked in ae0d633. On the policy, agreed, and stronger than you put it: the objects vector comes from StandbyMetadataStore::Snapshot(), which enumerates unordered maps, so the promotion context has no replay order at all. "Latest replay wins" was not implementable with the data on hand; sorting by address only made it look ordered. Overlap is now treated as ambiguity: every descriptor in a transitively overlapping group is discarded, replicas independent of the conflict are kept, and an object is dropped only when no reliable replica remains. The three correctness bugs were all real:
On the return contract: a snapshot whose objects all fail to land now returns INVALID_PARAMS instead of an empty-cluster success. The one deliberate carve-out is the all-duplicates case, where the live index already holds the state. The partial-restore serving gate stays with N07/N08 per #3808, and this PR does not try to own it. No dry run, no promotability predicate, no ReplicaID preservation here either; those remain with #3774/#3808 and #3811, and this PR is just the restore-tolerance stop-gap with honest conflict semantics. Tests now pin the new behavior in both suites: per-descriptor discard with independent-replica survival, transitive groups, the stale-range-owner regression, accounting rollback, all-rejected failure, and mixed good+bad tolerance. One note on evidence: my environment cannot build the repo (no rdma-core, and upstream main fails here with the same pre-existing header errors), so the execution evidence is CI plus a standalone harness that runs the sweep and accounting logic through the pinned scenarios. I will watch the Linux run and report back if anything moves. |
|
CI on ae0d633 is fully green: 21 lanes pass including CTest (39m26s), which runs both restored suites with the new ambiguity-discard pins. Nothing moved. |
|
Thanks for the rework. I agree with the ambiguity-discard policy for overlapping memory descriptors. I also want to clarify one point from my earlier review: I do not think a partial restore must always remain non-serving. For a cache, serving a smaller but internally correct index can be a valid availability tradeoff. Losing some cache entries is acceptable here. Returning a descriptor that refers to the wrong bytes is not. The remaining question is therefore not simply whether partial restore may serve. It is whether the discard decision becomes durable state, or remains a local decision made only by this promotion attempt. At the moment,
At that point an old key can be restored with a descriptor pointing at memory that now belongs to a different key. This is the failure mode I want to rule out. Data loss is acceptable; resurrection of discarded state and incorrect reads are not. I think conflict handling needs to be defined as a durable repair operation, not only as a restore-time filter. A preliminary contract could be:
The intended ordering would be:
This also means that discarded memory descriptors should not be imported into the remounted allocator. Their removal is a logical metadata repair, not a direct remote-memory free RPC; after the repair is durable, allocator recovery must treat only canonical live descriptors as occupied. I would avoid serializing a synthetic “conflict placeholder” descriptor. A placeholder can accidentally re-enter later snapshots or allocation recovery. The conflict evidence can remain in logs/metrics and in a temporary repair plan; the durable result should be only Also, The current PR already improves the local validation behavior substantially: it removes the unsupported “latest replay wins” assumption, discards transitive overlap groups, preserves independent replicas, and fixes the stale range-owner and accounting issues. Those changes are directionally correct. However, the durable repair protocol crosses the promotion supervisor, writer fencing, OpLog ordering, callback identity, remount allocation recovery, and failure handling. This is the reason I previously recommended deferring the feature: the local restore algorithm is only one part of the correctness contract. Since the OpLog/Snapshot path is still experimental and there is no urgent production requirement, I would prefer that we agree on this repair and pending-write contract first, then implement it with focused real-etcd tests:
I am happy to continue the design discussion, but I do not think the current restore-only change should merge before this convergence behavior is specified. |
|
issue: restoring |
|
@catyans Confirmed against the code, your scenario is real. Fixed in 3444ea0: reuse is now exact-endpoint only, anything else gets a fresh One caveat on verification: the store targets need Linux-only headers ( |
| restored_memory_segments.push_back(seg); | ||
| auto allocator = std::make_shared<DummyBufferAllocator>( | ||
| seg.segment_name, seg.transport_endpoint); | ||
| // Reuse the live standby allocator only on an exact endpoint |
There was a problem hiding this comment.
The restore function right now is too hudge and mix up multiple functions, I'd suggest to extract this into more small and single-purpose pieces.
|
Thanks, the exact-endpoint reuse and regression address my allocator concern. I am not approving yet because @Icedcoco's durable-repair concern remains unanswered: restore-local filtering may let a later promotion replay discarded descriptors. Please resolve that convergence contract before merge. |
|
@Icedcoco The convergence gap is real in the current patch, and your framing is the right one: the discard decision is local to this promotion attempt, nothing supersedes the old descriptors in the log, and a later promotion can replay them while the primary may already have reused the address. Restore-local filtering buys availability and correctness of the serving index, but it cannot stand in for the durable repair. Mapping your contract onto what the tree already has, so we specify against real primitives rather than in the abstract:
The contract as I would write it:
On split: this PR's local side (ambiguity discard, independent-replica preservation, the stale range-owner fix, commit-time accounting) is the restore half of that contract and stays. I will implement the durable repair half next, in this PR if you want the whole thing gated together, or as the immediate stacked follow-up so this one stops growing. Either way the merge waits until the repair path exists with the real-etcd tests you listed: no resurrection on later promotion, no old-key exposure after address reuse, accepted-not-durable means no serve, leadership loss during repair means no serve, and pending writes never allow premature reuse. One place I want your read before I build it: whether the repair batch should also carry a marker the standby store understands as "these descriptors were superseded by repair", so a standby that never saw this promotion can still drop them on snapshot, or whether canonical PUT_END/REMOVE ordering alone is enough given the durable-prefix fence. I lean toward the latter (the prefix fence already orders it), but you know the standby snapshot path better than I do. |
|
Thanks @he-yufeng. |
|
@Icedcoco @catyans The durable repair half is in 2426e80, following the contract as agreed (no marker record, catyans' read on ordering):
What I could not do here: the store targets need Linux-only headers, so there is no local build on this Mac; the CTest lane is the verifier, same as the earlier rounds. The real-etcd promotion/recovery tests from your list (no resurrection on later promotion, no old-key exposure after address reuse, accepted-not-durable means no serve, leadership loss during repair means no serve) I have not added yet: they need the etcd harness, and I would rather build them against your read of this shape first. |
2426e80 to
80f74c4
Compare
…d on empty restores Reworked from review by Icedcoco on kvcache-ai#3806: - Overlap is no longer "latest replay wins": the promotion input comes from StandbyMetadataStore::Snapshot(), which enumerates unordered maps, so no replay order exists to pick a winner by. Every descriptor in a transitively overlapping group is discarded, replicas independent of the conflict are kept, and an object is dropped only when no reliable replica remains. - pending_ranges no longer records accepted.size() mid-loop: ranges join only after the object is accepted, so a rejected object cannot leave a range whose owner index a later object would reuse. - Segment accounting now accumulates per object and commits once the whole object validates, so a mid-construction rejection leaves no phantom bytes behind for later capacity checks or metrics. - A snapshot whose objects all fail to land returns INVALID_PARAMS instead of an empty-cluster success, except when every rejection is an object the live index already holds. Tests pin the new semantics in both suites: per-descriptor discard with independent-replica survival, transitive overlap groups, the stale-range owner regression, accounting rollback, all-rejected failure, and mixed good+bad tolerance. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
|
Rebased onto the ReplicaID-preserving main (#3811). Two things worth noting for reviewers:
CTest on 4ee658b is the verifier as before. |
|
@Aionw addressed on the new head. The tolerant machinery is no longer one block: it now sits in three single-purpose methods off Both suites re-run against this head in a Linux container: master_service_test 71/71, master_service_ha_test 101/101. |
|
@Aionw A week on from addressing your review, re-verified: the branch is still MERGEABLE against current main (a8106d4), and the standby-restore area was not touched by the recent #3723/#3984 landings. The tolerant restore machinery now sits in three pieces as discussed (standby index restore, ambiguous-overlap discard, durable repair), with the HA and master-service tests covering the new paths. Anything else you want changed before this moves forward? |
|
Thanks for the thorough rework. The changes address the earlier review points well, especially per-object filtering, transitive overlap handling, range cleanup, accounting rollback, and the fail-closed result for fully rejected snapshots. I still have a few concerns that may be worth discussing before merging:
I do not intend these as objections to the improvements already made. The local restore behavior is substantially clearer now; I mainly want to confirm that the durability and repeated-promotion semantics are either covered here or clearly assigned to the follow-up work. |
|
@Icedcoco Thanks for the four points — two of them were real gaps and are now closed in 4b79c8a. Taking them in order. 1. Durability of conflict resolution. The overlap-derived discards are already durable; that was the "durable repair" half of the change and it is easy to miss in the diff. For every conflict-affected key, the restore writes a fenced OpLog record before completing: REMOVE for a fully dropped object, PUT_END carrying only the surviving descriptors for a partial one (master_service.cpp, the block right after The remaining hole you sensed is real but one category over: non-conflict rejections (unknown endpoint, invalid descriptor, capacity overflow) are log-and-metric only, so a replayed snapshot re-rejects them every round. That is safe but noisy, and I deliberately did not tombstone them here — 2. End-to-end coverage. Fair. This round adds an OpLog-enabled test that proves the repair channel end to end at the writer boundary: a partial conflict now produces a durable REMOVE for the dropped key and a canonical PUT_END for the survivor, read back from the batch storage; and a rejecting writer makes the whole restore fail closed. The full chain you describe (snapshot creation, OpLog catch-up, a second promotion, address reuse) belongs to the N07/N08 integration scope, and I would rather keep it there than bolt half of that harness onto this PR. 3. ReplicaID dependency. No ordering left: #3811 is already on main and is in this branch's ancestry, and the restore-only Replica constructor it added is what every construction path here uses. This PR is safe to merge independently on current main. 4. Terminal replica states. Now explicit. An object whose replicas all arrive REMOVED or FAILED (no overlap involved) is dropped as Verification: |
…with durable repair One bad entry or descriptor must not cost the whole index (kvcache-ai#3760). The legacy snapshot path now validates in three phases: cheap per-entry validation, ambiguity-discard overlap resolution, then construction for the survivors; segment-level structural corruption stays fail-fast. Conflict-derived discards become durable repair records (REMOVE for full drops, canonical PUT_END with only the survivors for partial ones) so a later promotion cannot replay them. Restored buffers keep their dummy allocator alive across later snapshots via exact-endpoint keepalive reuse. A snapshot where nothing lands still fails the restore, except when every object was already present. The bounded handoff path keeps its fail-fast chunk loop: its chunks stream from a metadata store the standby wrote from its own live index, so the contents are internally consistent by construction, and deferring overlap resolution across chunk installs would break the bounded-memory shape. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
…channel An object whose replicas all arrive REMOVED or FAILED without any overlap used to slip past the reliable-replica check, which only ran on objects with overlap discards, so the index retained metadata with zero readable copies. The check now covers every accepted object: terminal-only ones are rejected as no_reliable_replica and flow through the same durable REMOVE as overlap casualties, so a later promotion cannot replay them either. The repair block itself had no coverage: new HA tests pin the durable REMOVE plus canonical PUT_END reaching the OpLog writer on a partial conflict, the restore failing closed when the repair batch cannot go durable, and the terminal-only drop.
The client-liveness work on main makes a restored local-disk replica readable only while its owner holds a record. The bounded promotion path does this through record_for_known_owner; the legacy tolerant path's factored-out constructor dropped the wiring when it moved off the inline layout, so a restored local-disk object landed in the index unreadable (ExistKey false right after a successful restore). Thread the staged records through the legacy context and share one helper between both paths.
4b79c8a to
ae25fa2
Compare
|
Rebased onto current main (6cc1a10) and fixed one real interaction the rebase surfaced: the client-liveness work that landed in #2991 makes a restored local-disk replica readable only while its owner holds a liveness record, and the bounded promotion path wires that through ae25fa2 threads the staged owner records through the legacy context and shares one Re-verified locally on the rebased head: |
|
CI update: the CTest failure on the merged snapshot was |
4f98651 to
f74326e
Compare
…olerance-v2 # Conflicts: # mooncake-store/include/master_service.h
f74326e to
78a64c9
Compare
|
CI note on the
Rebase in the previous push brought the tree current; the two test binaries this PR actually affects ( |
Fixes #3760
What
Standby promotion no longer discards the whole metadata index because one entry fails validation.
RestoreFromStandbySnapshotruns in three phases: cheap per-entry validation (each failure skips that entry with its key and reason logged and counted on themooncake_ha_standby_restore_rejected_objects_total{reason}counter), overlap resolution, then construction for the survivors only. Only segment-level structural corruption (empty name/endpoint, zero capacity) stays fail-fast, since that is a malformed context rather than one bad object.Overlap is treated as ambiguity, not recency. The promotion input comes from
StandbyMetadataStore::Snapshot(), which enumerates unordered maps, so no replay order reaches the promotion context and neither side of an overlap can prove it is newer. Every descriptor in a transitively overlapping group is discarded, replicas independent of the conflict are kept, and an object is dropped only when no reliable replica remains. A snapshot whose objects all fail to land returns INVALID_PARAMS instead of reporting an empty-cluster success, with one carve-out: objects rejected because the live index already holds them are not losses.Review-driven fixes on top of that:
pending_rangesonly after the object is accepted, so a rejected object never leaves a range whose owner index a later accepted object would reuse.Out of scope here, by design: the pre-promotion dry run and promotability predicate (#3774), ReplicaID preservation (I00 dependency in the #3808 roadmap, covered by #3811), and the partial-restore serving gate. Those belong to the N07/N08 promotion contract work.
Tests
master_service_test.cppandmaster_service_ha_test.cpppin the new semantics: per-descriptor overlap discard with independent-replica survival, transitive overlap groups, the stale-range-owner regression, accounting rollback on construction rejection, all-rejected restores returning INVALID_PARAMS, and mixed good+bad tolerance. Existing duplicate, remount, and tenant-scoping pins are unchanged.Verification
Local build of this repo is not possible in this environment (no rdma-core, and upstream
mainfails to compile here with the same pre-existing header errors, so this is environmental, not the change). The overlap sweep and the accounting commit flow were exercised locally through a standalone harness running the same logic against the pinned scenarios (identical, contained, transitive, and adjacent ranges, dead-replica drop, capacity rollback). clang-format 20.1.8 and codespell are clean. CI runs the full Linux build and both suites.AI assistance
Prepared with AI assistance (Kimi K3). The ambiguity-discard semantics come from the review discussion on this PR; the submitter reviewed every changed line and can defend the change end-to-end.