spetz commented on code in PR #4073:
URL: https://github.com/apache/iggy/pull/4073#discussion_r3945308691


##########
core/simulator/src/lib.rs:
##########
@@ -1319,6 +1332,24 @@ impl Simulator {
         Some(partition.offsets())
     }
 
+    /// A replica's journaled partition-plane prepare header at `op`, or 
`None` when
+    /// it does not host the namespace or no longer holds the entry.
+    ///
+    /// Absence is ordinary, unlike on the metadata plane: the partition 
journal
+    /// evicts its committed prefix as it flushes to segments. The quiesce 
oracle
+    /// compares only the ops two replicas both still hold.
+    #[must_use]
+    pub(crate) fn partition_journaled_header(
+        &self,
+        replica_idx: usize,
+        namespace: IggyNamespace,
+        op: u64,
+    ) -> Option<PrepareHeader> {
+        let shard = self.replicas[replica_idx].partition_shard(namespace);
+        let partition = shard.plane.partitions().get_by_ns(&namespace)?;
+        partition.log.journal().inner.header_by_op(op)

Review Comment:
   `header_by_op` reads resident headers only. `evict_prefix` clears them on 
flush and moves the entries to the repair ring. After every replica flushes, 
`assert_partition_prefixes_agree` compares zero ops and the check is vacuous at 
quiescence.
   
   Use `repair_headers_in(1..=commit_min)` once per replica per namespace, 
since state_checker.rs:333 probes op by op and both lookups are linear. Add a 
post-flush comparison test.



##########
core/partitions/src/iggy_partition.rs:
##########
@@ -2081,10 +2081,27 @@ where
             .is_some_and(|&high_water| offset <= high_water)
     }
 
-    fn apply_consumer_offset_commit(
-        &self,
-        pending: PendingConsumerOffsetCommit,
-    ) -> Result<(), IggyError> {
+    /// Note a committed delete that found no offset to remove.
+    ///
+    /// Expected wherever the paired `AckLevel::NoAck` store never replicated, 
so a
+    /// diagnostic and not a fault. Still logged: on a replica that did serve 
the
+    /// store it is the first symptom of a lost apply.
+    fn log_absent_offset_delete(&self, kind: &str, id: u64) {

Review Comment:
   Patch coverage on this file is 20%. Add a unit test that commits 
DeleteConsumerOffset for an absent offset on a primary and asserts the 
partition stays unfenced.



##########
core/shard/src/lib.rs:
##########
@@ -5753,7 +5785,7 @@ where
         // one back: demanding one parks the view change forever on an op 
already
         // applied and durable in the snapshot.
         let repair_floor = journal.handle().snapshot_op();
-        let missing = first_op_not_covered(&pending, repair_floor, |op| {
+        let missing = first_op_not_covered(&pending, repair_floor, 
consensus.commit_min(), |op| {

Review Comment:
   This scan now reports a `missing_op` below `pending.commit_max`, but 
`pending_view_body_sources(missing_op)` (5829) only looks at DVC suffixes, 
which span `commit..=op` per sender. `index_of` returns None below that, the 
source list comes back empty, and the view change stalls until the timeout 
escalates. Every sender with `commit >= missing_op` holds or has compacted the 
op.
   
   For `missing_op < pending.commit_max`, select DVC senders with `commit >= 
missing_op`, most recent `log_view` first. A RangeEvicted from such a peer 
means this replica cannot serve the committed prefix, so let the view-change 
timeout escalate rather than arm a state transfer as primary-elect. Test 
against a real DVC quorum.



##########
core/consensus/src/impls.rs:
##########
@@ -1729,6 +1729,33 @@ impl<B: MessageBus, P: Pipeline<Entry = PipelineEntry>> 
VsrConsensus<B, P> {
         self.recovery_barrier.set(required_commit);
     }
 
+    /// Re-decide the barrier against a log head the cluster just settled.
+    ///
+    /// Boot arms it at the recovered journal head: those ops were acked 
before the
+    /// restart, so admitting writes before they re-commit rolls back committed
+    /// history. It otherwise clears only by `commit_max` passing it, which 
never
+    /// happens when a view change discards the suffix instead of 
re-committing it.
+    /// The boot re-pipeline already ran, so nothing re-prepares those ops,
+    /// `is_caught_up_primary` stays shut, and the primary drops the very 
requests
+    /// that would raise `commit_max`.
+    ///
+    /// Call this wherever the head is authoritatively re-decided: a merged 
log at
+    /// view start, an adopted `StartView`. `head` lowers the barrier when the 
view
+    /// truncated the suffix, keeps it when the suffix survived.
+    pub fn redecide_recovery_barrier(&self, head: u64) {
+        let barrier = self.recovery_barrier.get();
+        if barrier == 0 {
+            return;
+        }
+        let barrier = barrier.min(head);
+        self.recovery_barrier
+            .set(if barrier <= self.commit_max.get() {

Review Comment:
   Collapsing the barrier to 0 when `barrier <= commit_max` opens the HTTP read 
gate early. `await_recovery_barrier` (core/server/src/http/reads.rs:279) gates 
on `commit_min` because adoption advances `commit_max` before applying the 
suffix. With head 105 and `commit_min` 100, new readers see barrier 0 and serve 
state from before ops 101..=105 apply. `is_caught_up_primary` compares 
`commit_max >= barrier` and needs no zero.
   
   Set `barrier.min(head)` and drop the collapse. Update 
`given_a_discarded_suffix_when_adopting_a_view_should_lower_the_barrier` to 
expect 105 and assert `commit_max() >= recovery_barrier()`.



##########
core/consensus/src/plane_helpers.rs:
##########
@@ -482,15 +510,37 @@ where
 /// revalidates that the head is still this exact entry before popping and
 /// applying it. A driver dropped at an await strands nothing; a sibling driver
 /// that committed the op first fails the caller's revalidation and re-peeks.
+///
+/// Bounded below for the reason [`drain_committable_prefix`] is, and stalls 
rather
+/// than panicking for the same one: a shard pump's panic is swallowed by
+/// `compio::runtime::spawn`, while `tick_metadata` re-arms repair on the 
level.
 pub fn peek_committable_head<B, P>(consensus: &VsrConsensus<B, P>) -> 
Option<PrepareHeader>
 where
     B: MessageBus,
     P: Pipeline<Entry = PipelineEntry>,
 {
     let commit = consensus.commit_max();
-    consensus
+    let next = consensus.commit_min() + 1;
+    let head = consensus
         .pipeline_head_header()
-        .filter(|header| header.op <= commit)
+        .filter(|header| header.op <= commit)?;
+    if head.op != next {
+        // Unreachable in debug and the simulator; release reports and waits.

Review Comment:
   "Unreachable in debug and the simulator" is false on the partition plane. 
`IggyPartition::commit_journal` walks at most `COMMIT_WALK_OPS_MAX` (64) ops 
per call, and a promoted primary's pipeline is seeded from `merged.commit_max + 
1`. A primary-elect whose journal covers the merged log but whose apply lags by 
more than 64 ops passes the coverage scan, starts the view, and receives quorum 
acks for `commit_max + 1` before the sweep drains the backlog. 
`drain_committable_prefix` then sees `head_op > commit_min + 1` and the assert 
fires. Release logs an error per ack until the walk catches up.
   
   Finish the journal walk before the pipeline drain on partition promotion, or 
downgrade the asserts at 466 and 529 to a log.



##########
core/shard/src/lib.rs:
##########
@@ -5532,18 +5542,29 @@ where
             })
         };
         if let Some((peer, nonce, to_op)) = stalled {
-            // Primary-elect only. Its window starts at the merged log's commit
-            // point, which can sit below local `commit_min` (the headers 
inherited
-            // from senders behind the canonical log_view live there), so
-            // `commit_min + 1` would skip them. A backup's parked `StartView`
-            // suffix is only a verification reference; resuming from its 
commit
-            // point would restart at the view's opening head, not at the gap.
+            // Primary-elect only, and floored so a retry re-requests the 
window the
+            // initial arm did. A backup's parked `StartView` suffix is a
+            // verification reference: its commit point would restart at the 
view's
+            // opening head, not at the gap.
             let from_op = consensus
                 .is_primary_for_view(consensus.view())
-                .then(|| consensus.with_pending_view_log(|pending| 
pending.commit_max.max(1)))
+                .then(|| {
+                    consensus.with_pending_view_log(|pending| {
+                        merged_log_scan_floor(pending, consensus.commit_min())
+                    })
+                })
                 .flatten()
                 .unwrap_or_else(|| consensus.commit_min() + 1);
-            if from_op <= to_op {
+            if from_op > to_op {
+                // Satisfied. Leaving it armed wedges the replica: no 
`RepairDone`
+                // clears a window the walk is already past, and the `is_none` 
gate
+                // then blocks the session the ops above it need.
+                *self.metadata_repair.borrow_mut() = None;
+            } else {
+                // The quiet peer may be the thing that died, and nothing else
+                // re-targets a journal-repair session, so retrying it forever 
pins
+                // the walk while the rest of the cluster is serveable.
+                let peer = next_repair_peer(consensus.replica_count(), 
consensus.replica(), peer);

Review Comment:
   `session.peer` is never updated. The rotated value is a shadowed local, so 
every retry rotates from the original peer and the RepairDone continuation 
(line 5097) still sends to it.
   
   Rotation is also blind and immediate. A Normal backup rotates on the first 
stall to any replica, including one lagging below `from_op`. 
`on_request_prepares` (4683-4708) answers a range it never held with 
RangeEvicted and RepairDone, and the RangeEvicted arm (5106) arms a state 
transfer against that peer without checking `retained_from` against `commit_min 
+ 1`. On the primary-elect path the rotation leaves the 
`pending_view_body_sources` set and hits the same conversion mid view change.
   
   Write `session.peer`. Reuse `next_transfer_peer` (2518, prefers the primary) 
instead of adding `next_repair_peer`. Rotate only after a retry budget, as 
`tick_partitions` does (7148). Keep primary-elect rotation inside 
`pending_view_body_sources`.



-- 
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