krishvishal opened a new pull request, #3824: URL: https://github.com/apache/iggy/pull/3824
## What this fixes A committed, client-acknowledged op survived a primary crash only if the round-robin new primary happened to hold it. `DoViewChange` carried 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. `DoViewChange` now 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.checksum` was never set, so `parent` chained zeros and every merge comparison was `0 == 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 flipped `commit` reaches recovery's `max(header.commit)` watermark. `view` is excluded, because `restamp_prepare_view` rewrites it in place. **Zero, enforced two ways.** For prepares `CHECKSUM_UNSEALED` is 0 and means "skip", so existing WALs replay. For control headers a zero is a `FrameChecksumMismatch`: 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_max` where the refusal does not fire. Control headers now carry a checksum over every byte past it, verified on the typed parse before `validate` reads a field. Body verification keys on body presence, not on whether `checksum_body` looks sealed. **Merge correctness.** Canonical selection consults every canonical sender and treats disagreement as undecidable, since senders at the canonical `log_view` cannot legitimately disagree when a primary prepares one thing per op. Only a sender behind that `log_view` may 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_view` disagreement 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_count` 4 both were 2, so they could be disjoint. **`log_view` timing.** 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 `AwaitingRepair` waiting for more `DoViewChange` messages had guaranteed it would ignore them. **Repair serving.** Bounded by the local frontier again. `to_op` comes from a peer with only `from_op <= to_op` validated and the skip loop walks op by op with no `await`, so on the single-threaded shard pump an unclamped `u64::MAX` ends the shard. **Divergence reconciliation.** `Journal::truncate_from` drops a diverging uncommitted suffix and leaves the snapshot watermark alone. Not `drain`, 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_checksum` and 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. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
