fix(controller): bug-1433 async-thread fdatasync block + bug-1434 committed_lsn CAS race
bug-1433: raft_cb_become_leader runs synchronously off peer_ctrl_dispatch_to_pool, which hardcodes VTASK_LIVENESS for every inbound peer-control packet including vote replies. The bug-1431 round-2 fix (flusher_flush_once force-flush in persist_promotion_advance_committed) was reachable from that never-blocks task class -- same bug family as bug-1198. Removed the force-flush; round-3 (durable-capped only) plus the already-shipped bug-1432 immediate-first-heartbeat fix close the visibility-latency gap non-blockingly. TRACKED RESIDUAL: persist_apply_pump_run_sync is still reachable on the same VTASK_LIVENESS path and does bounded pread() -- lower risk (page-cache-hot) but strictly still blocking I/O; not safe to remove outright (reopens bug-1043-family job loss), needs its own follow-up (reclassify peer_ctrl_classify's winning-vote-reply case or dispatch promotion tail onto a fresh VTASK_LIVE_BLOCK vtask). bug-1434: persist_promotion_advance_committed and ha_standby_advance_committed_and_apply raced on persist_t.committed_lsn via an unlocked load-then-conditional-store during the promotion window (before ha->role flips to CTRL_PRIMARY) -- a losing racer could regress committed_lsn. Added persist_committed_lsn_advance_atomic (CAS retry loop, lock-free) and routed both call sites through it. Validated: 19/19 unit test suites, 6-round chaotic soak (0 loss/divergence) + 6-round sequential+leadership-coverage soak, both PASS. Also folds in raft-apply-decoupling and raft-ha-core-safety validate-impl phase bumps to "implemented" and clears config-upgrade-snapshot's now-satisfied entity-bearing-snapshots block.
This commit is contained in:
parent
ea0ac81c92
commit
40f510fb14
25 changed files with 1617 additions and 226 deletions
|
|
@ -65,19 +65,73 @@ void (*ha_quorum_transition_log_hook)(int lost, int connected,
|
|||
int required) = NULL;
|
||||
#endif
|
||||
|
||||
#ifndef XCE_TESTING
|
||||
#define HA_SELF_RESTART_RETRY_MS 5000
|
||||
|
||||
static int ha_self_restart_retry_fn(void *arg);
|
||||
|
||||
/* ha_self_restart_enter_fence_and_retry: the non-abort response to a failed
|
||||
* re-exec (2026-07-10, supersedes ha-convergence-recovery Req 1.4's original
|
||||
* "terminate fatally" text — see requirements.md's superseding note). Sets
|
||||
* the sticky, never-live-cleared ha->self_restart_pending flag (checked
|
||||
* unconditionally by raft_cb_become_leader / promotion_fence_timeout_fn
|
||||
* alongside apply_divergent, ha.c) so this node can never become primary
|
||||
* while it still carries its stale pre-demotion in-memory state, then
|
||||
* schedules a bounded periodic retry of the re-exec. This satisfies the same
|
||||
* safety property Req 1.4 protects — a divergent ex-primary never serves
|
||||
* authoritatively — via the project-wide fail-loud invariant (a controller
|
||||
* MUST NEVER self-terminate as a designed response to an operational
|
||||
* condition; CLAUDE.md "Process Lifecycle") instead of abort(): staying
|
||||
* alive and retrying self-heals automatically once the transient condition
|
||||
* (ENOMEM, EMFILE, a permissions change mid-upgrade) clears, whereas a dead
|
||||
* process needs manual admin recovery and degrades quorum meanwhile. */
|
||||
static void ha_self_restart_enter_fence_and_retry(cluster_t *c)
|
||||
{
|
||||
atomic_store_explicit(&c->ha.self_restart_pending, 1, memory_order_release);
|
||||
|
||||
if (c->delaymgr.running) {
|
||||
int rc = delay_task_ms(&c->delaymgr, HA_SELF_RESTART_RETRY_MS, 1,
|
||||
ha_self_restart_retry_fn, c);
|
||||
if (rc < 0)
|
||||
WARN("HA self-restart: retry scheduling failed: %d — node stays "
|
||||
"fenced (non-promotable) with no further automatic retry "
|
||||
"attempt", rc);
|
||||
} else {
|
||||
WARN("HA self-restart: delaymgr not running — node stays fenced "
|
||||
"(non-promotable) with no automatic retry");
|
||||
}
|
||||
}
|
||||
|
||||
/* ha_self_restart_retry_fn: delaymgr task body — bounded periodic retry of
|
||||
* the re-exec. A successful retry replaces the process image entirely (this
|
||||
* function never returns in that case); a repeated failure re-arms itself
|
||||
* via ha_self_restart's own failure path. */
|
||||
static int ha_self_restart_retry_fn(void *arg)
|
||||
{
|
||||
cluster_t *c = (cluster_t *)arg;
|
||||
if (!c)
|
||||
return -EINVAL;
|
||||
return ha_self_restart(c);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* ha_self_restart — the truncate-and-restart primitive's restart half.
|
||||
*
|
||||
* Production (Req 1.3, 1.4): flush logs, then execv() the saved argv so the
|
||||
* Production (Req 1.3): flush logs, then execv() the saved argv so the
|
||||
* controller rebuilds in-memory state purely from the surviving (committed)
|
||||
* WAL prefix via boot recovery. If exec cannot be performed, terminate
|
||||
* FATALLY (abort) — a demoted ex-primary must NEVER continue running with
|
||||
* potentially divergent state. NEVER returns in production.
|
||||
* WAL prefix via boot recovery. NEVER returns on success (execv replaces the
|
||||
* process image). If exec cannot be performed, do NOT abort (see
|
||||
* ha_self_restart_enter_fence_and_retry above) — enter a permanent
|
||||
* non-promotable fence and keep retrying periodically, per the project's
|
||||
* never-self-terminate invariant.
|
||||
*
|
||||
* Test (Req 1.6): under XCE_TESTING invoke ha_restart_hook(c) instead of
|
||||
* execv and return its result on the success path; a non-zero hook return
|
||||
* models an exec failure and routes to ha_fatal_hook in place of abort, so a
|
||||
* test binary can assert the FATAL path was taken without dying (Req 1.4).
|
||||
* models an exec failure and routes to ha_fatal_hook (retained as the test
|
||||
* seam name — it still marks "restart could not be performed", now backed by
|
||||
* the fence-and-retry response in production rather than abort), so a test
|
||||
* binary can assert that failure path was taken without dying.
|
||||
*/
|
||||
int ha_self_restart(cluster_t *c)
|
||||
{
|
||||
|
|
@ -88,20 +142,26 @@ int ha_self_restart(cluster_t *c)
|
|||
rc = ha_restart_hook(c);
|
||||
|
||||
if (rc != 0 || ha_restart_hook == NULL) {
|
||||
/* Restart could not be performed → FATAL contract (Req 1.4). */
|
||||
/* Restart could not be performed — same non-abort response as
|
||||
* production (Req 1.6 mirrors ha_self_restart_enter_fence_and_retry's
|
||||
* flag-setting without the real delaymgr retry, since a unit fixture
|
||||
* has no live delaymgr to retry on). */
|
||||
if (c)
|
||||
atomic_store_explicit(&c->ha.self_restart_pending, 1,
|
||||
memory_order_release);
|
||||
if (ha_fatal_hook != NULL)
|
||||
ha_fatal_hook("ha_self_restart: restart seam failed (FATAL)");
|
||||
ha_fatal_hook("ha_self_restart: restart seam failed — fenced "
|
||||
"non-promotable, staying alive");
|
||||
return rc != 0 ? rc : -1;
|
||||
}
|
||||
return 0;
|
||||
#else
|
||||
(void)c;
|
||||
|
||||
char **argv = controller_saved_argv();
|
||||
if (argv == NULL || argv[0] == NULL) {
|
||||
ERROR("HA self-restart: no saved argv — cannot re-exec (FATAL)");
|
||||
tlog_exit(); /* flush pending log buffer before dying */
|
||||
abort();
|
||||
ERROR("HA self-restart: no saved argv — cannot re-exec; entering "
|
||||
"non-promotable fence and retrying periodically (stay alive)");
|
||||
ha_self_restart_enter_fence_and_retry(c);
|
||||
return -1;
|
||||
}
|
||||
|
||||
INFO("HA self-restart: re-executing %s to rebuild from committed state",
|
||||
|
|
@ -110,12 +170,16 @@ int ha_self_restart(cluster_t *c)
|
|||
|
||||
execv(argv[0], argv);
|
||||
|
||||
/* execv only returns on failure — FATAL (Req 1.4): never continue serving
|
||||
* as a divergent ex-primary. */
|
||||
ERROR("HA self-restart: execv(%s) failed: %d — aborting (FATAL)",
|
||||
argv[0], errno);
|
||||
abort();
|
||||
return -1; /* unreachable */
|
||||
/* execv only returns on failure. A demoted ex-primary must never continue
|
||||
* serving with potentially divergent state (ha-convergence-recovery Req
|
||||
* 1.4) — but per the project-wide fail-loud invariant (never self-
|
||||
* terminate as a designed response to an operational condition), the
|
||||
* correct response is a permanent non-promotable fence that stays alive
|
||||
* and keeps retrying, not abort(). */
|
||||
ERROR("HA self-restart: execv(%s) failed: %d — entering non-promotable "
|
||||
"fence and retrying periodically (stay alive)", argv[0], errno);
|
||||
ha_self_restart_enter_fence_and_retry(c);
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue