numinnex commented on code in PR #4073:
URL: https://github.com/apache/iggy/pull/4073#discussion_r3955629873
##########
core/consensus/src/plane_helpers.rs:
##########
@@ -446,18 +456,35 @@ where
P: Pipeline<Entry = PipelineEntry>,
{
let commit = consensus.commit_max();
+ let commit_min = consensus.commit_min();
+ let replica = consensus.replica();
let mut drained = Vec::new();
consensus.with_pipeline_mut(|pipeline| {
+ let mut next = commit_min + 1;
while let Some(head_op) = pipeline.head().map(|entry| entry.header.op)
{
if head_op > commit {
break;
}
+ if head_op != next {
Review Comment:
Partition twin of the peek gate, and it defeats what
`iggy_partition.rs:4285` documents. Holding makes `drained.is_empty()` true, so
`commit_journal` falls through to `collect_committable_from_journal` ->
`committed_headers_from(commit_min+1, commit_max, 64)` (`journal.rs:876`),
which walks straight through the held head and advances past it with
`send_client_replies: false` (`iggy_partition.rs:4978`).
After that overshoot `on_ack` returns at `:4234` on every ack, so the
partition never ships a client reply again, and each stranded entry leaves a
`receiver.await` parked with no timeout (`shard/lib.rs:3853`). Same cap fix as
the peek site.
##########
core/consensus/src/plane_helpers.rs:
##########
@@ -482,15 +510,33 @@ 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 reported
+/// rather than asserted for the same one. Holding is safe: 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 {
Review Comment:
`head.op != next` also matches `head.op <= commit_min`, and that arm is
terminal. `commit_journal` (`metadata.rs:3644`) is bounded by `commit_max`
alone, never by the pipeline head, so after a backlog past
`COMMIT_WALK_OPS_MAX` it applies the seeded head out of the journal and
advances `commit_min` past it. `pop_committed_prepare`'s only caller
(`metadata.rs:2843`) sits inside this loop, so nothing ever pops: wire replies
are never built, `take_reply_sender()` neither fires nor drops, and
`is_caught_up_primary` keeps reporting healthy on `commit_min == commit_max`
until the pipeline fills and dispatch shuts.
Suggest capping both journal walks at `pipeline_head_op - 1`, with an absent
head meaning *no cap* — the pipeline is primary-only, so a naive
`saturating_sub(1)` on `head = 0` would freeze every backup's walk.
##########
core/shard/src/lib.rs:
##########
@@ -5550,9 +5592,12 @@ where
// from the evicted ring or the flushed segments.
let missing = {
let journal = partition.log.journal();
- first_op_not_covered(&pending, consensus.commit_min(), |op| {
- journal.inner.header_by_op(op)
- })
+ first_op_not_covered(
+ &pending,
+ consensus.commit_min(),
+ consensus.commit_min(),
Review Comment:
Both args are `consensus.commit_min()`, so `merged_log_scan_floor(p,
cm).max(repair_floor + 1)` is `cm + 1` for all inputs: `pending.commit_max`
never participates and master's `max(pending.commit_max, cm+1)` floor is gone.
That lands the scan inside the evicted window. `header_by_op`
(`journal.rs:760`) reads the resident vec only, while `commit_messages` evicts
`committed_prefix(commit_max)` — the cluster frontier, not `commit_min` — so a
primary-elect with an apply backlog reads `None` at `cm+1`, reports a false
`missing_op`, and parks in `ViewChange`. This site arms no repair and
`group_is_gap_stopped` needs `probe.normal`, so there is no fill path. It also
drops a structural bound: `op_head - commit_max <= 127` (`dvc_merge.rs:115`)
capped master's scan at 128 ops; this is 127 + an unbounded backlog of
whole-vec probes per tick, with `commit_min` frozen while parked.
Probing ring+resident and arming repair (or ceding primaryship) would fix
both halves; a `repair_retained_from()` floor alone would declare coverage for
ops the journal cannot serve.
##########
core/shard/src/lib.rs:
##########
@@ -5214,6 +5211,51 @@ where
// `RangeEvicted` again if the primary checkpointed mid
// transfer -- that reraises through the same path, and
each
// round lifts the local floor, so it converges.
+ //
+ // Never as primary-elect. A transfer replaces
snapshot-shaped
+ // state wholesale, and this replica has a merged log
parked
+ // against that state naming ops it has just been told it
+ // cannot serve; installing under it would start the view
over
+ // a log the new state no longer matches. The honest
answer is
+ // that another replica holds the committed prefix, so
leave
+ // the session for the stall rotation to re-target and let
the
+ // view-change timeout escalate if nobody can serve it.
+ if consensus.view_log_is_pending()
Review Comment:
Returning with the session intact lets the trailing `RepairDone` run. The
nothing-retained serve arm sends `RangeEvicted` and then `RepairDone(from_op -
1)` on the same nonce (`:4780`, `:4793`); with a primary-elect `from_op <=
commit_min + 1`, `commit_journal` moves nothing and `repair_chunk_walked(C, C,
<=C)` (`:10465`) is true, so `:5180` re-requests immediately — no tick gate, no
debounce, no attempt burn.
That is an unthrottled request/reply loop across both shard pumps until the
view-change timeout escalates. Pre-PR the `RangeEvicted` arm dropped the
session first, so the trailing `RepairDone` hit `let Some(session) ... else
return`. Suggest fencing or dropping the session in this arm, or gating the
re-request on real chunk progress.
##########
core/shard/src/lib.rs:
##########
@@ -5023,6 +5026,9 @@ where
if !in_scope {
return;
}
+ // The serving peer is answering. Clears the stall clock and the
budget
+ // so a window served in chunks cannot rotate off a healthy stream.
+ self.note_metadata_repair_progress();
Review Comment:
This now sits above the divergence return (`:5044`),
`verify_prepare_integrity` (`:5054`) and the dedup return (`:5064`); master had
it below all three (`be3663805:5060-5067`, "only SILENCE should age the
stream").
That makes rotation unreachable rather than merely deferred: a stall round
burns attempts 0->1, `1 > REPAIR_MAX_STALL_RETRIES` fails, the re-request draws
the same stored bytes back, and the first in-scope frame zeroes the budget
again — so the count oscillates 1->0 and never reaches 4. Deterministic on the
`disagrees` path, since the peer re-serves the same bytes. Suggest resetting
`idle_ticks` here but charging the budget only on an accepted frame.
--
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]