fix(controller): refuse replicated WAL writes before this boot's recovery (bug-1920)
A restarted standby accepts the primary's connection during its startup delay, before persist_recover runs. Catch-up entries written into the un-recovered persist_t opened seg-...0001 with O_TRUNC and rewrote it, and after recovery the stale fd kept filing new LSNs under that segment name. The primary's filename-ordered streamer then skipped them, so a standby needing that range never caught up and commits stalled. persist_wal_write_replicated now returns -EBUSY until recovery completes (the bug-1416 recovery_complete gate). ha_apply_wal_catchup_entry and ha_handle_snapshot_chunk drop pre-recovery traffic without ACK or NACK. Catch-up streams from min(NACK-derived join position, standby report), and the streamer warns when a non-empty range yields no entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f01bb7de34
commit
762c6f1883
6 changed files with 242 additions and 2 deletions
|
|
@ -3658,6 +3658,21 @@ int ha_apply_wal_catchup_entry(ha_state_t *ha, persist_t *p,
|
|||
if (!ha || !p || !entry)
|
||||
return -EINVAL;
|
||||
|
||||
/* bug-1920: before this boot's persist_recover the local log is not
|
||||
* loaded: persisting would corrupt the on-disk segment layout (see
|
||||
* persist_wal_write_replicated), and a NACK would walk the primary's
|
||||
* next_index back past entries this node really holds. Drop silently --
|
||||
* no persist, no ACK, no NACK; the first state report after recovery
|
||||
* carries the real position and catch-up resumes from there. */
|
||||
if (!persist_recovery_complete(p)) {
|
||||
static _Atomic int s_logged;
|
||||
if (!atomic_exchange_explicit(&s_logged, 1, memory_order_relaxed))
|
||||
INFO("ha_apply_wal_catchup_entry: replication received before "
|
||||
"this boot's WAL recovery completed — dropping until "
|
||||
"recovered (bug-1920)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── ha-term-safety task 3.2: term-carrying AppendEntries → library route ──
|
||||
* A term-safe AppendEntries (one sent by repl_send_append_entries / the
|
||||
* library send path) carries a non-zero entry term stamp. Route it through
|
||||
|
|
@ -3965,6 +3980,12 @@ int ha_handle_snapshot_chunk(ha_state_t *ha, persist_t *p,
|
|||
if (!ha || !p || !snap_dir || !chunk)
|
||||
return -EINVAL;
|
||||
|
||||
/* bug-1920: same pre-recovery drop as ha_apply_wal_catchup_entry -- a
|
||||
* snapshot installed before persist_recover would be clobbered or mixed
|
||||
* with the replay. The transfer restarts from chunk 0 after recovery. */
|
||||
if (!persist_recovery_complete(p))
|
||||
return 0;
|
||||
|
||||
char part_path[4096 + 16];
|
||||
snprintf(part_path, sizeof(part_path), "%s/snap.part", snap_dir);
|
||||
|
||||
|
|
@ -4679,7 +4700,14 @@ int ha_dispatch_repl_action_for_standby(ha_state_t *ha,
|
|||
* corrected position at least once. */
|
||||
if (c) {
|
||||
uint64_t join_lsn;
|
||||
if (repl_get_catchup_join_lsn(&c->persist.repl, ep, &join_lsn))
|
||||
/* bug-1920: never above the standby's own report. The NACK backoff
|
||||
* moves next_index down one per reject, so join_lsn can sit far above
|
||||
* where the standby's log really ends; streaming from there is
|
||||
* rejected (one step per heartbeat) or finds nothing to send. The
|
||||
* report is an upper bound on what the standby holds; a lower
|
||||
* join_lsn (a conflicted suffix the report cannot see) still wins. */
|
||||
if (repl_get_catchup_join_lsn(&c->persist.repl, ep, &join_lsn)
|
||||
&& join_lsn < standby_lsn)
|
||||
standby_lsn = join_lsn;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue