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]

Reply via email to