fix(consensus): stop view change discarding committed ops - #3824
Open
krishvishal wants to merge 2 commits into
Open
fix(consensus): stop view change discarding committed ops#3824krishvishal wants to merge 2 commits into
krishvishal wants to merge 2 commits into
Conversation
krishvishal
force-pushed
the
vsr-dvc-headers
branch
from
August 5, 2026 19:13
da9f942 to
d4b4d30
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3824 +/- ##
============================================
+ Coverage 76.40% 76.43% +0.02%
Complexity 1046 1046
============================================
Files 1334 1335 +1
Lines 165228 167529 +2301
Branches 137583 139990 +2407
============================================
+ Hits 126242 128045 +1803
- Misses 35271 35657 +386
- Partials 3715 3827 +112
🚀 New features to boost your workflow:
|
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.
What this fixes
A committed, client-acknowledged op survived a primary crash only if the round-robin new primary happened to hold it.
DoViewChangecarried scalars only, so the new primary took the winner's op number without its log, rebuilt its pipeline from its own journal, hit the local gap, and truncated the range as "decided lost". The op was journaled on a quorum, committed, and already replied to; the next client op reused the number and collided with the stale entry on an up-to-date backup. The comment justifying the truncation cited a DVC merge that did not exist.DoViewChangenow carries the sender's uncommitted suffix, a nack bit and a present bit per entry, and the new primary merges the quorum's headers. Discarding requires a nack quorum, which quorum intersection makes unreachable for anything that may have committed. Otherwise the view does not start: unavailable and saying so beats available and short a reply.Changes
Independent defects on the same path.
Prepare identity.
PrepareHeader.checksumwas never set, soparentchained zeros and every merge comparison was0 == 0. Now sealed on both planes and verified at all four ingress points, not just metadata repair: a frame corrupted in flight was journaled as-is and re-served, which the interior-corruption boot refusal turns into an unbootable node. The WAL scan checks it and the parent chain, since a flippedcommitreaches recovery'smax(header.commit)watermark.viewis excluded, becauserestamp_prepare_viewrewrites it in place.Zero, enforced two ways. For prepares
CHECKSUM_UNSEALEDis 0 and means "skip", so existing WALs replay. For control headers a zero is aFrameChecksumMismatch: keying on "looks sealed" lets one flipped bit disable the layer by clearing the field that gates the check. Tolerant on disk, strict on the wire.Control frame integrity. Canonical selection took the first sender scanned and read a differing header as an implicit nack, so one bit flipped in that sender's suffix turned every honest sender's correct header into a nack against the garbage: a nack quorum on three replicas, above
commit_maxwhere the refusal does not fire. Control headers now carry a checksum over every byte past it, verified on the typed parse beforevalidatereads a field. Body verification keys on body presence, not on whetherchecksum_bodylooks sealed.Merge correctness. Canonical selection consults every canonical sender and treats disagreement as undecidable, since senders at the canonical
log_viewcannot legitimately disagree when a primary prepares one thing per op. Only a sender behind thatlog_viewmay nack implicitly, a constraint the comment stated and nothing tested. The decoder recomputes each identity and checks view and timestamp monotonicity and the hash chain. A tripwire that panicked on same-log_viewdisagreement is gone; it fired on remote bytes.Quorum sizing. Splitting the replication and view-change quorums to define the nack quorum surfaced a latent bug: at
replica_count4 both were 2, so they could be disjoint.log_viewtiming. Raised when the merge parked, before the merged head was installed, so a primary-elect superseded or crashed mid-repair held a durable claim to a view whose headers it never had, then carried its own stale head as sole canonical sender of the next view change.Parked-log roles. A backup's parked log is a verification reference, not a repair window. It doubled as the view's repair scope, so once traffic passed the view's opening head every repaired op was discarded unnamed and the backup never converged.
Latched quorum flag. Set on reaching a view-change quorum, not on deciding a log, so every non-Ready outcome was terminal: an
AwaitingRepairwaiting for moreDoViewChangemessages had guaranteed it would ignore them.Repair serving. Bounded by the local frontier again.
to_opcomes from a peer with onlyfrom_op <= to_opvalidated and the skip loop walks op by op with noawait, so on the single-threaded shard pump an unclampedu64::MAXends the shard.Divergence reconciliation.
Journal::truncate_fromdrops a diverging uncommitted suffix and leaves the snapshot watermark alone. Notdrain, which advances the watermark past what it removed: correct for a committed prefix, ruinous for a suffix, marking the ops that must stay refillable as evictable.Data at rest. Disk polls verify each batch against its
batch_checksumand fail closed. Consumer offset files gain a checksum, since the offset is a cursor reloaded unchanged on every restart and a flipped bit silently rewinds or skips a consumer. Bare pre-checksum files still load and upgrade on the next write, and the legacy server stays compatible both ways by reading the first eight bytes and stopping. Separable from the consensus work.