diff --git a/tidal-server/src/cluster/node.rs b/tidal-server/src/cluster/node.rs index 16363e2..1228a6c 100644 --- a/tidal-server/src/cluster/node.rs +++ b/tidal-server/src/cluster/node.rs @@ -996,12 +996,30 @@ impl ShardReplica { // install, the pull must target the DISCOVERED leader's shard from the // seeded artifact tail + 1 — NOT the boot topology leader, which for a // reseeded ex-leader is the node itself (a self-pull never converges). - // The plain self-heal still targets the boot topology leader for a - // non-install follower boot (leadership moved? term fencing + later - // election traffic rescue it, the existing m11p2 semantics). + // The plain self-heal targets the boot topology leader — but ONLY in the + // genuine topology era (durable term 0). Post-election that field is dead + // config, and pulling against it is actively harmful: + // + // `applied_seqno` is keyed per LEADER REGION, so a group retains a position + // for every leadership it has followed. Live tidaldb-0's group 1 held + // keys [[0, 13540659], [1, 13540652], [2, 13540661]] — current leader + // tidaldb-2 (key 2) fully converged at 13540661, alongside a STALE key 1 + // from when tidaldb-1 led the group. The old code pulled the topology + // leader's key regardless, i.e. key 1 from 13540652 + 1 = 13540653, which + // tidaldb-1's WAL had compacted below (earliest 13540657) → a permanent + // `snapshot-required` refusal → marker latch → `reseed_self_restart` loop. + // The comment here used to claim "term fencing + later election traffic + // rescue it"; they do not, because the refusal re-latches faster than the + // rescue converges. + // + // Post-election, convergence is driven by the heartbeat path + // (`note_leader_frontier_for_readiness`, which carries the CURRENT leader's + // frontier and works on an idle cluster since m12p5) and by the receiver's + // gap detection on real ship traffic — both keyed to the leader that is + // actually shipping, never a historical one. if let Some((discovered_shard, recovered_tail)) = install_target { transport.request_catchup(discovered_shard, recovered_tail + 1); - } else if !is_leader_at_boot { + } else if !is_leader_at_boot && hard_state.current_term == 0 { let leader_shard = shard_of_region(leader); // A node can NEVER pull its own stream from itself: only the node whose // `source_shard == S` serves shard S's stream (server.rs returns