forked from lei.chen/myxce
fix(repl): harden standby slot allocation against WAL-retention guard clobber (bug-1474)
repl_add_standby()'s "find first empty slot" scan treated any ep==NULL slot as available, with no awareness that a disconnected slot can still hold an unexpired bug-1147 WAL-retention guard (last_disconnected_lsn, set by repl_remove_standby, meant to be cleared only by repl_expire_disconnect_slots once snap_interval_s genuinely elapses). The scan could pick a guard-holding slot over a genuinely virgin one purely by lower index, then unconditionally memset it -- destroying the guard before trim_wal_segments/repl_trim_lsn could ever honor it. Confirmed via 2/2 reproducible live 3-node soak runs (config-upgrade- snapshot task 8.9's new --config-upgrade-wal-trim mode): a killed standby's still-needed WAL segment got trimmed out from under it. Now a two-pass scan: prefer a genuinely virgin slot; only fall back to reusing a guard-holding slot (oldest last_disconnect_time first, to discard the least remaining protection) when no virgin slot exists -- logged via WARN() so the discarded guard is operator-visible rather than silent. Defense-in-depth at the slot-allocation layer, independent of whatever triggers a given repl_add_standby call (a separate, not-fully-root-caused ha.c reconnect-sweep defect is the specific trigger seen in the soak, out of this fix's scope). Independently reviewed across two rounds: round 1 flagged the exhausted-fallback path silently discarding a guard with zero logging; remediated with a WARN() matching this file's existing repl_broadcast convention, empirically confirmed to fire only on the fallback path with correct content. Suite: 1028 -> 1030 tests, 0 failures (net of this fix; final combined total with bug-1472 landing concurrently: 1033 tests, 0 failures).
This commit is contained in:
parent
7547a8f14b
commit
c0b0c7d1de
2 changed files with 189 additions and 2 deletions
|
|
@ -93,15 +93,60 @@ int repl_add_standby(replicator_t *r, repl_ep_t ep, const char *host, uint32_t p
|
|||
return -ENOSPC;
|
||||
}
|
||||
|
||||
/* Find first empty slot */
|
||||
/* Slot allocation (bug-1474 hardening): prefer a genuinely virgin slot
|
||||
* over one still holding an unexpired bug-1147 disconnect guard
|
||||
* (last_disconnected_lsn != 0) for a DIFFERENT peer. repl_add_standby
|
||||
* cannot know whether this call is a legitimate new connection or a
|
||||
* duplicate/late-joiner reconnect for an already-connected peer (that
|
||||
* classification defect lives in ha.c, out of scope here) — but either
|
||||
* way, clobbering an unexpired guard is never correct: it silently
|
||||
* defeats repl_trim_lsn's WAL retention floor for whichever standby
|
||||
* the guard was protecting (see .wolf/buglog.json bug-1474).
|
||||
*
|
||||
* Pass 1: any slot with ep==NULL && last_disconnected_lsn==0 (virgin —
|
||||
* never used, or its guard already expired via
|
||||
* repl_expire_disconnect_slots, which fully zeroes the slot). Always
|
||||
* safe to take; first such slot by index.
|
||||
*
|
||||
* Pass 2 (fallback, only when Pass 1 finds nothing): among slots with
|
||||
* ep==NULL && last_disconnected_lsn!=0 (guard-holding), reuse the one
|
||||
* with the OLDEST last_disconnect_time. Deliberate policy: that guard
|
||||
* is closest to its own natural expiry (repl_expire_disconnect_slots'
|
||||
* snap_interval_s window), so reusing it discards the LEAST remaining
|
||||
* WAL-retention protection of any guard-holding slot. With
|
||||
* MAX_STANDBYS==3 it is possible — under a chaotic multi-disconnect
|
||||
* scenario with no virgin slot free — for every non-primary slot to be
|
||||
* guard-holding simultaneously; refusing this connect (-ENOSPC) would
|
||||
* trade a bounded, already-degraded WAL-retention risk for an outright
|
||||
* connection failure, which is worse for cluster availability. This
|
||||
* fallback does NOT touch the -ENOSPC path above, which remains
|
||||
* reserved for the genuinely-impossible case of more distinct live
|
||||
* peers than slots exist. */
|
||||
int idx = -1;
|
||||
for (int i = 0; i < MAX_STANDBYS; i++) {
|
||||
if (r->standbys[i].ep == NULL) {
|
||||
if (r->standbys[i].ep == NULL && r->standbys[i].last_disconnected_lsn == 0) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pass 1 found nothing -- about to fall back to Pass 2 (guard-holding
|
||||
* slot reuse) below. Tracked so the WARN below fires only for the
|
||||
* genuine fallback case, not the common virgin-slot path. */
|
||||
int used_fallback_slot = (idx < 0);
|
||||
|
||||
if (idx < 0) {
|
||||
for (int i = 0; i < MAX_STANDBYS; i++) {
|
||||
if (r->standbys[i].ep != NULL) {
|
||||
continue;
|
||||
}
|
||||
if (idx < 0 ||
|
||||
r->standbys[i].last_disconnect_time < r->standbys[idx].last_disconnect_time) {
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
/* Should not happen given nstandbys guard above, but be safe */
|
||||
mylock_unlock(&r->lock);
|
||||
|
|
@ -109,6 +154,17 @@ int repl_add_standby(replicator_t *r, repl_ep_t ep, const char *host, uint32_t p
|
|||
}
|
||||
|
||||
standby_conn_t *s = &r->standbys[idx];
|
||||
if (used_fallback_slot) {
|
||||
/* bug-1474 review finding: this silently destroys the slot's
|
||||
* WAL-retention guard below (via memset) -- log it loudly, mirroring
|
||||
* repl_broadcast's existing "degraded but continuing" WARN
|
||||
* convention (e.g. "standby %s:%u evicted", line ~314 above), so the
|
||||
* discarded protection is operator-visible instead of silent. */
|
||||
WARN("repl_add_standby: reusing guard-holding slot %d (host=%s:%u,"
|
||||
" last_disconnected_lsn=%lu) -- no virgin slot available,"
|
||||
" WAL-retention protection discarded",
|
||||
idx, s->host, s->port, (unsigned long)s->last_disconnected_lsn);
|
||||
}
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->ep = ep;
|
||||
s->port = port;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue