feat(ha-single-leader-role): remove CTRL_PRIMARY_COMMITTING enum + remaining decls — compiler-enforced (task 5.2)

This commit is contained in:
Rongsong Shen 2026-06-13 14:41:42 +00:00
parent 7f974e766b
commit 93dfc02717
10 changed files with 44 additions and 55 deletions

View file

@ -59,7 +59,7 @@
- Observable completion: full controller suite green; `grep -rln "COMMITTING" src/controller/testsuite/` returns zero files - Observable completion: full controller suite green; `grep -rln "COMMITTING" src/controller/testsuite/` returns zero files
- _Requirements: 1.1, 1.2_ - _Requirements: 1.1, 1.2_
- [ ] 5.2 Remove the enum value and remaining declarations (compiler-enforced completeness) - [x] 5.2 Remove the enum value and remaining declarations (compiler-enforced completeness)
- Delete `CTRL_PRIMARY_COMMITTING = 4` from `ctrl_role_t` (ha.h:64); delete `HA_DEFAULT_COMMITTING_TIMEOUT_MS` (ha.h:131), `committing_timeout_ms` (ha.h:141), `repl_ack_count` (ha.h:180), `committing_timeout_ctx` (ha.h:187); delete the `.committing_timeout_ms` initializer (cluster.c:899); sweep remaining ha.h comments (214-230, 244-262, 373, 503-519, 607-614, 673-700, 791), the cluster.c ctrlhello comment (1402-1410), and the raft.c comment (src/lib/raft/raft.c:123) - Delete `CTRL_PRIMARY_COMMITTING = 4` from `ctrl_role_t` (ha.h:64); delete `HA_DEFAULT_COMMITTING_TIMEOUT_MS` (ha.h:131), `committing_timeout_ms` (ha.h:141), `repl_ack_count` (ha.h:180), `committing_timeout_ctx` (ha.h:187); delete the `.committing_timeout_ms` initializer (cluster.c:899); sweep remaining ha.h comments (214-230, 244-262, 373, 503-519, 607-614, 673-700, 791), the cluster.c ctrlhello comment (1402-1410), and the raft.c comment (src/lib/raft/raft.c:123)
- Fix everything the compiler then surfaces — this is the completeness gate for code references - Fix everything the compiler then surfaces — this is the completeness gate for code references
- Observable completion: clean build with zero warnings about the removed symbols; full controller testsuite + ASAN green via `/xce-build controller` - Observable completion: clean build with zero warnings about the removed symbols; full controller testsuite + ASAN green via `/xce-build controller`
@ -87,6 +87,7 @@
- **(3.1) Legacy path routes through BARE helpers, not `ha_promote_to_primary`**: `ha_elect_evaluate_and_signal`'s PRIMARY branch calls `ha_register_persist_hooks` + `ha_start_primary_heartbeat` + `ha_arm_own_term_gate` directly (matching the raft path), NOT `ha_promote_to_primary` — because the latter calls `ha_do_promote` (term increment) and the legacy branch historically has NO term store/increment (research §4 finding 2). This is a deliberate deviation from design §283's loose wording ("route through ha_promote_to_primary / ha_register_persist_hooks"); the bare-helper route preserves term semantics and is the correct reading. - **(3.1) Legacy path routes through BARE helpers, not `ha_promote_to_primary`**: `ha_elect_evaluate_and_signal`'s PRIMARY branch calls `ha_register_persist_hooks` + `ha_start_primary_heartbeat` + `ha_arm_own_term_gate` directly (matching the raft path), NOT `ha_promote_to_primary` — because the latter calls `ha_do_promote` (term increment) and the legacy branch historically has NO term store/increment (research §4 finding 2). This is a deliberate deviation from design §283's loose wording ("route through ha_promote_to_primary / ha_register_persist_hooks"); the bare-helper route preserves term semantics and is the correct reading.
- **(4.1) No shims — DELETE, don't stub** (design §Transitional-State Deletions / §Design Synthesis: "rewritten or DELETED, not stubbed"). Machinery-subject test cases (convergence checker, committing watchdog, ack-driven promotion, ack counting) were DELETED, not shimmed: test_ha_role.c (5 majority_convergence + 2 committing_timeout_promotes_degraded), test_ha_rdhup_integration.c (rdhup_committing_to_primary_on_ack, rdhup_3node_one_ack_promotes), test_ha_connection_reuse.c (dispatcher_committing_handles_repl_ack, promoted_ep_repl_ack_promotes_to_primary). Kept state-entry cases had their deleted-fn CALLS excised (not shimmed). `test_ha_connection_reuse.c` was overlooked by the spec's §3 census disposition for machinery cases — confirmed in census (line 97) but its ack cases needed DELETE, not UPDATE. - **(4.1) No shims — DELETE, don't stub** (design §Transitional-State Deletions / §Design Synthesis: "rewritten or DELETED, not stubbed"). Machinery-subject test cases (convergence checker, committing watchdog, ack-driven promotion, ack counting) were DELETED, not shimmed: test_ha_role.c (5 majority_convergence + 2 committing_timeout_promotes_degraded), test_ha_rdhup_integration.c (rdhup_committing_to_primary_on_ack, rdhup_3node_one_ack_promotes), test_ha_connection_reuse.c (dispatcher_committing_handles_repl_ack, promoted_ep_repl_ack_promotes_to_primary). Kept state-entry cases had their deleted-fn CALLS excised (not shimmed). `test_ha_connection_reuse.c` was overlooked by the spec's §3 census disposition for machinery cases — confirmed in census (line 97) but its ack cases needed DELETE, not UPDATE.
- **(4.2) Residual stale COMMITTING comments in ha.c** (~lines 443, 826, 1240): documentary prose on the legacy raft_cb_become_leader/convergence path — NOT live branches. Scrub in 5.2/6.x (they fail the task-6.2 repo-wide grep). After 4.2, ZERO live code branches on `CTRL_PRIMARY_COMMITTING` in ha.c; the enum value persists in ha.h:64 until 5.2. - **(4.2) Residual stale COMMITTING comments in ha.c** (~lines 443, 826, 1240): documentary prose on the legacy raft_cb_become_leader/convergence path — NOT live branches. Scrub in 5.2/6.x (they fail the task-6.2 repo-wide grep). After 4.2, ZERO live code branches on `CTRL_PRIMARY_COMMITTING` in ha.c; the enum value persists in ha.h:64 until 5.2.
- **(5.2) Code is COMMITTING-free; only DOCS remain for task 6.1**: after 5.2 the grep `CTRL_PRIMARY_COMMITTING|HA_DEFAULT_COMMITTING_TIMEOUT_MS|committing_timeout_ms|repl_ack_count|committing_timeout_ctx` is ZERO across `src/controller/` + `src/lib/`. Remaining tree references (task 6.1 docs-sync targets, then 6.2 verifies): `src/docs/cluster-config-split-analysis.md:34` (committing_timeout_ms) AND `src/controller/CLAUDE.md` (TLA+/role-mapping table still names PRIMARY_COMMITTING — 6.1 must scrub ALL its COMMITTING refs, not just the :40 line) plus the steering/skill docs already listed in 6.1. ctrl_role_t now has 4 values: CTRL_PRIMARY=0, CTRL_STANDBY=1, CTRL_INIT=2, CTRL_ELECTING=3.
- **(5.1) Meson 300s timeout is now routinely hit** — barrier tests' commit-deadline waits push controller-suite wall-time over the 300s meson cap under load (`Fail: 0, Timeout: 1`). The AUTHORITATIVE green check is the DIRECT binary run (`src/xce.build/controller/controller_unit_test`, EXIT=0, 167 Tests/0 Failures aggregated). For task 6.2's "suite + ASAN green" gate: run the direct binary OR re-run meson until under-cap; a `Timeout: 1` is a wrapper artifact, NOT a test failure. `test_committing_timeout_default_is_60s` was DELETED in 5.1 (not 5.2) because its body used the uppercase production macro `HA_DEFAULT_COMMITTING_TIMEOUT_MS` (caught by the grep gate); the knob it tested dies in 5.2 regardless. - **(5.1) Meson 300s timeout is now routinely hit** — barrier tests' commit-deadline waits push controller-suite wall-time over the 300s meson cap under load (`Fail: 0, Timeout: 1`). The AUTHORITATIVE green check is the DIRECT binary run (`src/xce.build/controller/controller_unit_test`, EXIT=0, 167 Tests/0 Failures aggregated). For task 6.2's "suite + ASAN green" gate: run the direct binary OR re-run meson until under-cap; a `Timeout: 1` is a wrapper artifact, NOT a test failure. `test_committing_timeout_default_is_60s` was DELETED in 5.1 (not 5.2) because its body used the uppercase production macro `HA_DEFAULT_COMMITTING_TIMEOUT_MS` (caught by the grep gate); the knob it tested dies in 5.2 regardless.
- **(4.2) Test-leak / suite-timeout facts**: the controller test target sets `ASAN_OPTIONS=detect_leaks=0` (controller/meson.build:163), so a pre-existing suite-wide LSan leak (~153KB, present at baseline 678b7f8, this spec leaks slightly LESS) is suppressed under the meson gate — LSan exit=1 only on a RAW direct-binary run, not a regression. The controller suite runs ~268s against a 300s meson timeout — borderline; the final task-6.2 full regression may TIMEOUT under machine load — re-run if so (it is not a failure). - **(4.2) Test-leak / suite-timeout facts**: the controller test target sets `ASAN_OPTIONS=detect_leaks=0` (controller/meson.build:163), so a pre-existing suite-wide LSan leak (~153KB, present at baseline 678b7f8, this spec leaks slightly LESS) is suppressed under the meson gate — LSan exit=1 only on a RAW direct-binary run, not a regression. The controller suite runs ~268s against a 300s meson timeout — borderline; the final task-6.2 full regression may TIMEOUT under machine load — re-run if so (it is not a failure).
- **(4.1) Residual for task 6.2 final grep**: 3 DOCUMENTARY PROSE comments in `test_ha_role_lifecycle.c` (~lines 760/815/825) still mention `ha_repl_ack_received`/`committing_timeout_fn` by name (no live code). The 4.1 gate (`src/controller/*.c|*.h`) excludes testsuite so this passed; the task-6.2 repo-wide grep WILL catch them — scrub these comments in 5.x/6.x. Also a non-tracked editor backup `src/controller/.ha.c.~undo-tree~` holds stale refs (irrelevant; not a build input — do not commit). - **(4.1) Residual for task 6.2 final grep**: 3 DOCUMENTARY PROSE comments in `test_ha_role_lifecycle.c` (~lines 760/815/825) still mention `ha_repl_ack_received`/`committing_timeout_fn` by name (no live code). The 4.1 gate (`src/controller/*.c|*.h`) excludes testsuite so this passed; the task-6.2 repo-wide grep WILL catch them — scrub these comments in 5.x/6.x. Also a non-tracked editor backup `src/controller/.ha.c.~undo-tree~` holds stale refs (irrelevant; not a build input — do not commit).

View file

@ -372,7 +372,7 @@ static int myreq_handler(vtask_t* task)
* raft_on_vote_request handles: * raft_on_vote_request handles:
* - stale-term deny (term < current_term) * - stale-term deny (term < current_term)
* - term upgrade (term > current_term → clear voted_for) * - term upgrade (term > current_term → clear voted_for)
* - role gate: RAFT_LEADER denies; COMMITTING gate in on_vote_eligible * - role gate: RAFT_LEADER denies (via on_vote_eligible)
* - one-vote-per-term invariant (voted_for != -1 && voted_for != from_idx) * - one-vote-per-term invariant (voted_for != -1 && voted_for != from_idx)
* - on_persist before granting (durability guarantee) * - on_persist before granting (durability guarantee)
* *
@ -925,7 +925,6 @@ int cluster_init_phase1(cluster_t* c)
.max_promotion_retries = HA_DEFAULT_MAX_PROMOTION_RETRIES, .max_promotion_retries = HA_DEFAULT_MAX_PROMOTION_RETRIES,
.election_jitter_max_ms = HA_DEFAULT_ELECTION_JITTER_MAX_MS, .election_jitter_max_ms = HA_DEFAULT_ELECTION_JITTER_MAX_MS,
.election_priority_step_ms = HA_DEFAULT_ELECTION_PRIORITY_STEP_MS, .election_priority_step_ms = HA_DEFAULT_ELECTION_PRIORITY_STEP_MS,
.committing_timeout_ms = HA_DEFAULT_COMMITTING_TIMEOUT_MS,
}; };
int my_priority = 0; int my_priority = 0;
@ -1464,8 +1463,8 @@ int cluster_run(cluster_t* c)
* This triggers handle_ctrlhello on the receiving standby, which runs the * This triggers handle_ctrlhello on the receiving standby, which runs the
* existing catch-up replication sequence (WAL streaming, config push). * existing catch-up replication sequence (WAL streaming, config push).
* *
* Must be called from the COMMITTING path in ha_elect_evaluate_and_signal * Must be called from the post-election path in ha_elect_evaluate_and_signal
* after ha->role is set to CTRL_PRIMARY_COMMITTING (Req 7.2). * after ha->role is set to CTRL_PRIMARY (Req 7.2).
* *
* Thread safety: acquires ha->lock around standby_endpoints iteration. * Thread safety: acquires ha->lock around standby_endpoints iteration.
* Best-effort: failures per-endpoint are logged but do not abort the loop. * Best-effort: failures per-endpoint are logged but do not abort the loop.

View file

@ -437,13 +437,12 @@ void ha_compute_safe_committed_lsn(ha_elect_ctx_t *ctx,
} }
/* /*
* raft_cb_become_leader: node won the election — enter COMMITTING state. * raft_cb_become_leader: node won the election — enter CTRL_PRIMARY directly.
* *
* Equivalent of ha_elect_evaluate_and_signal CTRL_PRIMARY path: * Equivalent of ha_elect_evaluate_and_signal CTRL_PRIMARY path:
* - enter CTRL_PRIMARY_COMMITTING * - enter CTRL_PRIMARY (no transitional state)
* - call cluster_init_phase2 on first boot * - call cluster_init_phase2 on first boot
* - reset repl_ack_count, push config, send ctrlhello * - push config, send ctrlhello
* - arm committing timeout watchdog
* *
* ha->raft_c must be non-NULL (production path only; test mode uses noop stubs). * ha->raft_c must be non-NULL (production path only; test mode uses noop stubs).
*/ */
@ -469,7 +468,7 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
uint64_t safe_lsn = 0; uint64_t safe_lsn = 0;
uint64_t safe_tag = 0; uint64_t safe_tag = 0;
ha_compute_safe_committed_lsn(ectx, &safe_lsn, &safe_tag); ha_compute_safe_committed_lsn(ectx, &safe_lsn, &safe_tag);
/* Store results for use in WAL truncation and COMMITTING phase. /* Store results for use in WAL truncation and post-election commit.
* safe_lsn is the highest LSN on a majority; entries above it * safe_lsn is the highest LSN on a majority; entries above it
* may not have been committed on all nodes and must be truncated. */ * may not have been committed on all nodes and must be truncated. */
if (c) { if (c) {
@ -486,7 +485,7 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
* Req 2.3 (ha-repl-hardening). */ * Req 2.3 (ha-repl-hardening). */
atomic_store_explicit(&ha->last_known_committed_lsn, safe_lsn, atomic_store_explicit(&ha->last_known_committed_lsn, safe_lsn,
memory_order_release); memory_order_release);
/* Update committed config tag baseline for the COMMITTING decision loop */ /* Update committed config tag baseline for the post-election decision loop */
atomic_store_explicit(&ha->committed_config_tag, safe_tag, atomic_store_explicit(&ha->committed_config_tag, safe_tag,
memory_order_release); memory_order_release);
} }
@ -520,7 +519,7 @@ static void raft_cb_become_leader(void *ctx, uint64_t term)
", first_boot=%d)", term, is_first_boot); ", first_boot=%d)", term, is_first_boot);
/* Req 2.1 (ha-repl-hardening): re-initialize per-standby convergence /* Req 2.1 (ha-repl-hardening): re-initialize per-standby convergence
* tracking for THIS committing round. Stale converged flags from a * tracking for THIS primary term. Stale converged flags from a
* previous term (demote → re-elect cycle) must not satisfy the * previous term (demote → re-elect cycle) must not satisfy the
* majority-convergence gate before standbys re-report their state. */ * majority-convergence gate before standbys re-report their state. */
memset(ha->standby_reported_tag, 0, sizeof(ha->standby_reported_tag)); memset(ha->standby_reported_tag, 0, sizeof(ha->standby_reported_tag));
@ -823,7 +822,7 @@ int ha_init(ha_state_t *ha, const ha_config_t *cfg, int my_priority)
/* ha-repl-hardening Task 1.4: per-standby convergence tracking arrays. /* ha-repl-hardening Task 1.4: per-standby convergence tracking arrays.
* memset(ha, 0, sizeof(*ha)) above already zeroes these arrays; the explicit * memset(ha, 0, sizeof(*ha)) above already zeroes these arrays; the explicit
* note documents the invariant: on entry to CTRL_PRIMARY_COMMITTING every * note documents the invariant: on promotion to CTRL_PRIMARY every
* standby starts as not-converged (tag=0, lsn=0, converged=false). */ * standby starts as not-converged (tag=0, lsn=0, converged=false). */
/* heartbeat_seen starts at 0: no heartbeat seen yet. /* heartbeat_seen starts at 0: no heartbeat seen yet.
@ -1237,8 +1236,8 @@ ha_repl_action_t ha_evaluate_repl_action(uint64_t standby_tag,
/* /*
* ha_process_standby_state — update per-standby convergence tracking. * ha_process_standby_state — update per-standby convergence tracking.
* *
* Called on each heartbeat reply from a standby (CTRL_PRIMARY_COMMITTING or * Called on each heartbeat reply from a standby (CTRL_PRIMARY).
* CTRL_PRIMARY). Stores (reported_tag, reported_lsn) into the per-standby * Stores (reported_tag, reported_lsn) into the per-standby
* arrays, evaluates convergence, and logs INFO on the first * arrays, evaluates convergence, and logs INFO on the first
* not-converged → converged transition (Req 7.2). Regression resets the flag * not-converged → converged transition (Req 7.2). Regression resets the flag
* (Req 2.5). * (Req 2.5).
@ -2071,14 +2070,14 @@ int (*ha_phase2_hook)(void *c) = NULL;
/* /*
* ha_send_ctrlhello_hook: test-only seam for ha_send_ctrlhello_to_all_standbys. * ha_send_ctrlhello_hook: test-only seam for ha_send_ctrlhello_to_all_standbys.
* When non-NULL, the COMMITTING path calls this INSTEAD OF the real function * When non-NULL, the post-election path calls this INSTEAD OF the real function
* (cluster.c is not linked in unit-test binary). Req 7.2. * (cluster.c is not linked in unit-test binary). Req 7.2.
*/ */
int (*ha_send_ctrlhello_hook)(void *c) = NULL; int (*ha_send_ctrlhello_hook)(void *c) = NULL;
/* /*
* ha_push_config_hook: test-only seam for push_config_transport_only. * ha_push_config_hook: test-only seam for push_config_transport_only.
* When non-NULL, the COMMITTING path calls this INSTEAD OF the real function * When non-NULL, the post-election path calls this INSTEAD OF the real function
* (config_repl.c's push_config_transport_only needs a real cluster). Req 7.1. * (config_repl.c's push_config_transport_only needs a real cluster). Req 7.1.
*/ */
int (*ha_push_config_hook)(void *c) = NULL; int (*ha_push_config_hook)(void *c) = NULL;
@ -2976,7 +2975,7 @@ int election_timeout_fn_for_test(ha_state_t *ha, ha_elect_ctx_t *ctx,
/* /*
* repl_catchup_done_pkt_for_test: build a minimal io_packet_t encoding * repl_catchup_done_pkt_for_test: build a minimal io_packet_t encoding
* Request{opcode=replCatchUpDone, body_which=nodata}. * Request{opcode=replCatchUpDone, body_which=nodata}.
* Used by peer_ctrl_msg_handler tests to verify the COMMITTING dispatch path. * Used by peer_ctrl_msg_handler tests to verify the repl-ack dispatch path.
*/ */
io_packet_t *repl_catchup_done_pkt_for_test(void) io_packet_t *repl_catchup_done_pkt_for_test(void)
{ {
@ -3273,7 +3272,7 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
INFO("ha_start_election_async: no local config (tag=0, n_peers=%d) — " INFO("ha_start_election_async: no local config (tag=0, n_peers=%d) — "
"participating as voter only, not starting own election", n_peers); "participating as voter only, not starting own election", n_peers);
/* Transition to CTRL_STANDBY so myapp_run schedules fetch_config_from_primary. /* Transition to CTRL_STANDBY so myapp_run schedules fetch_config_from_primary.
* The primary will push config via ctrlhello once it enters COMMITTING. */ * The primary will push config via ctrlhello once it becomes CTRL_PRIMARY. */
atomic_store_explicit(&ha->role, (int32_t)CTRL_STANDBY, memory_order_release); atomic_store_explicit(&ha->role, (int32_t)CTRL_STANDBY, memory_order_release);
/* Free ctx — no vote sends will happen from this node this round. */ /* Free ctx — no vote sends will happen from this node this round. */
atomic_store_explicit(&ha->elect_ctx, NULL, memory_order_release); atomic_store_explicit(&ha->elect_ctx, NULL, memory_order_release);
@ -3285,8 +3284,8 @@ void ha_start_election_async(ha_state_t *ha, cluster_t *c,
raft_start_election(&ha->raft); raft_start_election(&ha->raft);
/* For single-node, raft_start_election fires on_become_leader synchronously. /* For single-node, raft_start_election fires on_become_leader synchronously.
* on_become_leader (raft_cb_become_leader) handles role → COMMITTING, * on_become_leader (raft_cb_become_leader) handles role -> CTRL_PRIMARY and
* phase2, and committing timeout. Free the ctx here since no raft callbacks * phase2. Free the ctx here since no raft callbacks
* will reference it after raft_start_election returns for single-node. */ * will reference it after raft_start_election returns for single-node. */
if (n_peers == 1) { if (n_peers == 1) {
atomic_store_explicit(&ha->elect_ctx, NULL, memory_order_release); atomic_store_explicit(&ha->elect_ctx, NULL, memory_order_release);

View file

@ -60,8 +60,7 @@ typedef enum {
CTRL_PRIMARY = 0, CTRL_PRIMARY = 0,
CTRL_STANDBY = 1, CTRL_STANDBY = 1,
CTRL_INIT = 2, /* Startup: listener open, awaiting heartbeat or election timeout */ CTRL_INIT = 2, /* Startup: listener open, awaiting heartbeat or election timeout */
CTRL_ELECTING = 3, /* Running quorum election via OP_requestVote */ CTRL_ELECTING = 3 /* Running quorum election via OP_requestVote */
CTRL_PRIMARY_COMMITTING = 4 /* Primary waiting for quorum ACK on config commit */
} ctrl_role_t; } ctrl_role_t;
/* ── Election types ────────────────────────────────────────── */ /* ── Election types ────────────────────────────────────────── */
@ -105,8 +104,8 @@ typedef struct {
* watchdog. Set by ha_start_election_async after scheduling the timeout. */ * watchdog. Set by ha_start_election_async after scheduling the timeout. */
void *timeout_ctx; /* election_timeout_ctx_t*; NULL until watchdog scheduled */ void *timeout_ctx; /* election_timeout_ctx_t*; NULL until watchdog scheduled */
/* Task 4.1: cluster_t* so ha_elect_evaluate_and_signal can call /* Task 4.1: cluster_t* so ha_elect_evaluate_and_signal can call
* cluster_init_phase2(c) and arm the committing timeout watchdog. * cluster_init_phase2(c).
* NULL in unit tests (c=NULL bypasses phase2 and watchdog arming). */ * NULL in unit tests (c=NULL bypasses phase2). */
cluster_t *c; cluster_t *c;
/* Task 6.3: candidate's own LSN/tag, stored unconditionally so that /* Task 6.3: candidate's own LSN/tag, stored unconditionally so that
* ha_compute_safe_committed_lsn can use them even for single-node elections * ha_compute_safe_committed_lsn can use them even for single-node elections
@ -128,7 +127,6 @@ typedef struct {
#define HA_DEFAULT_MAX_PROMOTION_RETRIES 5 #define HA_DEFAULT_MAX_PROMOTION_RETRIES 5
#define HA_DEFAULT_ELECTION_JITTER_MAX_MS 200 #define HA_DEFAULT_ELECTION_JITTER_MAX_MS 200
#define HA_DEFAULT_ELECTION_PRIORITY_STEP_MS 500 #define HA_DEFAULT_ELECTION_PRIORITY_STEP_MS 500
#define HA_DEFAULT_COMMITTING_TIMEOUT_MS 60000
typedef struct { typedef struct {
uint32_t heartbeat_interval_ms; /* Primary heartbeat send interval (min 2000 ms) */ uint32_t heartbeat_interval_ms; /* Primary heartbeat send interval (min 2000 ms) */
@ -138,7 +136,6 @@ typedef struct {
uint32_t max_promotion_retries; /* Max consecutive failed promotion attempts */ uint32_t max_promotion_retries; /* Max consecutive failed promotion attempts */
uint32_t election_jitter_max_ms; /* random jitter added to election timeout (default 200) */ uint32_t election_jitter_max_ms; /* random jitter added to election timeout (default 200) */
uint32_t election_priority_step_ms; /* extra ms per priority level (default 500) */ uint32_t election_priority_step_ms; /* extra ms per priority level (default 500) */
uint32_t committing_timeout_ms; /* Quorum-ACK deadline for CTRL_PRIMARY_COMMITTING (default 60000) */
} ha_config_t; } ha_config_t;
/* probe_ctx_t deleted — only used by am_i_primary() (election-iomgr-refactor) */ /* probe_ctx_t deleted — only used by am_i_primary() (election-iomgr-refactor) */
@ -177,14 +174,12 @@ typedef struct {
/* ── New fields added by election-iomgr-refactor spec (Task 2.1) ── */ /* ── New fields added by election-iomgr-refactor spec (Task 2.1) ── */
void *client_ssl_ctx; /* SSL_CTX* (TLS_client_method); created in myapp_run */ void *client_ssl_ctx; /* SSL_CTX* (TLS_client_method); created in myapp_run */
int phase2_done; /* 1 after cluster_init_phase2 runs */ int phase2_done; /* 1 after cluster_init_phase2 runs */
_Atomic int repl_ack_count; /* count of standbys that ACKed in COMMITTING state */
int n_peers; /* total cluster size (including self), matches raft convention */ int n_peers; /* total cluster size (including self), matches raft convention */
/* ── New fields added by raft-library spec (Task 6.1) ──────── */ /* ── New fields added by raft-library spec (Task 6.1) ──────── */
raft_state_t raft; /* Raft consensus state (wired in task 6.2) */ raft_state_t raft; /* Raft consensus state (wired in task 6.2) */
/* ── New fields added by raft-library spec (Task 6.2) ──────── */ /* ── New fields added by raft-library spec (Task 6.2) ──────── */
cluster_t *raft_c; /* cluster_t* set by ha_start_election_async; used by raft callbacks */ cluster_t *raft_c; /* cluster_t* set by ha_start_election_async; used by raft callbacks */
void *raft_election_timeout_ctx; /* election_timeout_ctx_t* for cancel (task 6.2) */ void *raft_election_timeout_ctx; /* election_timeout_ctx_t* for cancel (task 6.2) */
void *committing_timeout_ctx; /* committing_timeout_ctx_t* for cancel on step-down */
/* ── New fields added by ha-heartbeat-liveness spec (Task 1.1) ── */ /* ── New fields added by ha-heartbeat-liveness spec (Task 1.1) ── */
time_t last_activity; /* CLOCK_MONOTONIC seconds of last primary message; 0 = none yet */ time_t last_activity; /* CLOCK_MONOTONIC seconds of last primary message; 0 = none yet */
/* ── New fields added by ha-repl-hardening spec (Task 1.4) ──────── */ /* ── New fields added by ha-repl-hardening spec (Task 1.4) ──────── */
@ -376,7 +371,7 @@ ha_repl_action_t ha_evaluate_repl_action(uint64_t standby_tag,
* ha_process_standby_state: update per-standby convergence tracking. * ha_process_standby_state: update per-standby convergence tracking.
* *
* Called on each heartbeat reply from a standby while the primary is in * Called on each heartbeat reply from a standby while the primary is in
* CTRL_PRIMARY_COMMITTING or CTRL_PRIMARY. Stores the standby's reported * CTRL_PRIMARY. Stores the standby's reported
* (config_tag, applied_lsn) and evaluates whether the standby has converged * (config_tag, applied_lsn) and evaluates whether the standby has converged
* with the primary's committed state. * with the primary's committed state.
* *
@ -650,7 +645,7 @@ int ha_dispatch_repl_action_for_standby(ha_state_t *ha,
* an OP_hello CtrlHelloRequest to each connected standby. * an OP_hello CtrlHelloRequest to each connected standby.
* This triggers the existing catch-up replication path in handle_ctrlhello * This triggers the existing catch-up replication path in handle_ctrlhello
* (Req 7.2). Defined in cluster.c; called from ha_elect_evaluate_and_signal * (Req 7.2). Defined in cluster.c; called from ha_elect_evaluate_and_signal
* in the COMMITTING path. * in the post-election path.
*/ */
void ha_send_ctrlhello_to_all_standbys(cluster_t *c); void ha_send_ctrlhello_to_all_standbys(cluster_t *c);
@ -668,7 +663,7 @@ extern int (*ha_phase2_hook)(void *c);
/* /*
* ha_send_ctrlhello_hook: test-only seam for ha_send_ctrlhello_to_all_standbys. * ha_send_ctrlhello_hook: test-only seam for ha_send_ctrlhello_to_all_standbys.
* When non-NULL, ha_elect_evaluate_and_signal() calls this in the COMMITTING * When non-NULL, ha_elect_evaluate_and_signal() calls this in the post-election
* path instead of the real function (cluster.c not linked in test binary). * path instead of the real function (cluster.c not linked in test binary).
* Requirements: 7.2. * Requirements: 7.2.
*/ */
@ -676,7 +671,7 @@ extern int (*ha_send_ctrlhello_hook)(void *c);
/* /*
* ha_push_config_hook: test-only seam for push_config_to_standbys. * ha_push_config_hook: test-only seam for push_config_to_standbys.
* When non-NULL, ha_elect_evaluate_and_signal() calls this in the COMMITTING * When non-NULL, ha_elect_evaluate_and_signal() calls this in the post-election
* path instead of the real function. Requirements: 7.1. * path instead of the real function. Requirements: 7.1.
*/ */
extern int (*ha_push_config_hook)(void *c); extern int (*ha_push_config_hook)(void *c);

View file

@ -820,7 +820,6 @@ static ha_config_t g_fg_cfg = {
.max_promotion_retries = 5, .max_promotion_retries = 5,
.election_jitter_max_ms = 0, .election_jitter_max_ms = 0,
.election_priority_step_ms = 500, .election_priority_step_ms = 500,
.committing_timeout_ms = 10000,
}; };
/* Arrange a CTRL_PRIMARY ha_state_t with n_endpoints connected standby /* Arrange a CTRL_PRIMARY ha_state_t with n_endpoints connected standby

View file

@ -840,7 +840,6 @@ static ha_config_t g_gate_cfg = {
.max_promotion_retries = 5, .max_promotion_retries = 5,
.election_jitter_max_ms = 0, .election_jitter_max_ms = 0,
.election_priority_step_ms = 500, .election_priority_step_ms = 500,
.committing_timeout_ms = 10000,
}; };
/* Arrange a CTRL_PRIMARY ha_state_t with n_endpoints connected standby /* Arrange a CTRL_PRIMARY ha_state_t with n_endpoints connected standby

View file

@ -175,11 +175,11 @@ static void test_dispatcher_electing_calls_vote_reply_hook(void)
printf("test_dispatcher_electing_calls_vote_reply_hook: PASS\n"); printf("test_dispatcher_electing_calls_vote_reply_hook: PASS\n");
} }
/* ── Test 2 (dispatcher transitional-leader → repl_ack count) DELETED ───────── /* ── Test 2 (dispatcher transitional-leader ack count) DELETED ─────────
* ha-single-leader-role task 4.1. Subject was the deleted ack-counting * ha-single-leader-role task 4.1. Subject was the deleted ack-counting
* promotion machinery (replCatchUpDone incrementing repl_ack_count to drive a * promotion machinery (replCatchUpDone incrementing the per-term ACK tally to
* transitional leader state to PRIMARY). That path is gone — nothing left to * drive a transitional leader state to PRIMARY). That path is gone — nothing
* verify. No shim (design §"Transitional-State Deletions"). * left to verify. No shim (design §"Transitional-State Deletions").
*/ */
/* ── Test 3: dispatcher drops packet for CTRL_STANDBY ───────────────────── */ /* ── Test 3: dispatcher drops packet for CTRL_STANDBY ───────────────────── */
@ -477,10 +477,10 @@ static void test_follower_passive_no_outbound_connect(void)
printf("test_follower_passive_no_outbound_connect: PASS\n"); printf("test_follower_passive_no_outbound_connect: PASS\n");
} }
/* ── Test 10 (Task 7.3: promoted ep repl_ack promotes to PRIMARY) DELETED ───── /* ── Test 10 (Task 7.3: promoted ep ack promotes to PRIMARY) DELETED ─────
* ha-single-leader-role task 4.1. Subject was the deleted ack-counting * ha-single-leader-role task 4.1. Subject was the deleted ack-counting
* promotion machinery (replCatchUpDone incrementing repl_ack_count until the * promotion machinery (replCatchUpDone incrementing the per-term ACK tally until
* majority threshold promoted a transitional leader state to CTRL_PRIMARY via * the majority threshold promoted a transitional leader state to CTRL_PRIMARY via
* ha_promote_to_primary). * ha_promote_to_primary).
* The mechanism is gone — promotion is now the heartbeat decision loop + * The mechanism is gone — promotion is now the heartbeat decision loop +
* per-entry majority ack. Nothing left to verify; no shim (design * per-entry majority ack. Nothing left to verify; no shim (design

View file

@ -150,7 +150,7 @@ static void test_demote_sets_standby_role(void)
/* /*
* Verify that ha_init zeroes the new fields added for election-iomgr-refactor: * Verify that ha_init zeroes the new fields added for election-iomgr-refactor:
* phase2_done, repl_ack_count, n_peers, client_ssl_ctx * phase2_done, n_peers, client_ssl_ctx
* and that the surviving leader role values are distinct in ctrl_role_t. * and that the surviving leader role values are distinct in ctrl_role_t.
* *
* Requirements: 5.2, 8.1 (election-iomgr-refactor spec); 1.2 (ha-single-leader-role: * Requirements: 5.2, 8.1 (election-iomgr-refactor spec); 1.2 (ha-single-leader-role:
@ -171,9 +171,6 @@ static void test_ha_init_zeroes_new_fields(void)
/* phase2_done must be explicitly zero after ha_init */ /* phase2_done must be explicitly zero after ha_init */
TEST_ASSERT_EQUAL_INT(0, ha.phase2_done); TEST_ASSERT_EQUAL_INT(0, ha.phase2_done);
/* repl_ack_count is _Atomic int; must be zero after ha_init */
TEST_ASSERT_EQUAL_INT(0, atomic_load_explicit(&ha.repl_ack_count, memory_order_relaxed));
/* n_peers must be zero after ha_init */ /* n_peers must be zero after ha_init */
TEST_ASSERT_EQUAL_INT(0, ha.n_peers); TEST_ASSERT_EQUAL_INT(0, ha.n_peers);

View file

@ -755,7 +755,7 @@ static void *lifecycle_wait_thread(void *p)
* *
* SHARED-SEQUENCE CONTRACT PIN (GREEN now, MUST STAY GREEN — not a behavioral * SHARED-SEQUENCE CONTRACT PIN (GREEN now, MUST STAY GREEN — not a behavioral
* RED carrier). After task 2.2 BOTH roads to PRIMARY (the raft become-leader * RED carrier). After task 2.2 BOTH roads to PRIMARY (the raft become-leader
* callback and ha_repl_ack_received's quorum path) route through this same * callback and the legacy quorum-ack path) route through this same
* ha_promote_to_primary sequence, which task 2.2 augments with the noop+gate * ha_promote_to_primary sequence, which task 2.2 augments with the noop+gate
* tail while KEEPING hook registration. This case is therefore a CONTRACT/ * tail while KEEPING hook registration. This case is therefore a CONTRACT/
* REGRESSION pin proving the shared promotion sequence registers the persist * REGRESSION pin proving the shared promotion sequence registers the persist
@ -809,8 +809,8 @@ static void test_promotion_registers_persist_hooks(void)
/* ── Case 4: NO WATCHDOG — promotion arms no delaymgr task (Req 1.5, 2.3). /* ── Case 4: NO WATCHDOG — promotion arms no delaymgr task (Req 1.5, 2.3).
* *
* The shared promotion sequence must never arm a wall-clock watchdog (the * The shared promotion sequence must never arm a wall-clock promotion watchdog
* deleted committing_timeout_fn). Asserted via the delaymgr task-count seam: * (deleted in task 4.x). Asserted via the delaymgr task-count seam:
* the count is unchanged across ha_promote_to_primary. * the count is unchanged across ha_promote_to_primary.
* *
* Status note (RED vs already-GREEN): the force-promotion watchdog is armed * Status note (RED vs already-GREEN): the force-promotion watchdog is armed
@ -820,8 +820,8 @@ static void test_promotion_registers_persist_hooks(void)
* exercises — never armed a watchdog, so this case is ALREADY GREEN today and * exercises — never armed a watchdog, so this case is ALREADY GREEN today and
* stands as a regression-pin guaranteeing task 2.2's shared sequence does not * stands as a regression-pin guaranteeing task 2.2's shared sequence does not
* introduce one. The watchdog REMOVAL from raft_cb_become_leader is covered * introduce one. The watchdog REMOVAL from raft_cb_become_leader is covered
* at integration scope (task 4.x deletes committing_timeout_fn and the * at integration scope (task 4.x deletes the promotion-watchdog arm and the
* test_ha_evaluate_committing committing-timeout cases). */ * transitional-state evaluation cases). */
static void test_promotion_arms_no_delaymgr_watchdog(void) static void test_promotion_arms_no_delaymgr_watchdog(void)
{ {
#ifdef XCE_TESTING #ifdef XCE_TESTING
@ -1140,11 +1140,11 @@ static void test_legacy_and_raft_promotion_are_symmetric(void)
"legacy road must arm the own-term gate at noop_lsn (Req 1.4, 2.1) — " "legacy road must arm the own-term gate at noop_lsn (Req 1.4, 2.1) — "
"identical to the raft road's shared tail"); "identical to the raft road's shared tail");
/* Symmetry axis 4: NO committing-timeout watchdog armed (Req 1.5). */ /* Symmetry axis 4: NO promotion watchdog armed (Req 1.5). */
int after = delaymgr_task_count(&g_lc.delaymgr); int after = delaymgr_task_count(&g_lc.delaymgr);
TEST_ASSERT_EQUAL_INT_MESSAGE(before, after, TEST_ASSERT_EQUAL_INT_MESSAGE(before, after,
"legacy road must arm NO delaymgr watchdog (Req 1.5) — symmetric with " "legacy road must arm NO delaymgr watchdog (Req 1.5) — symmetric with "
"the raft road; the committing-timeout arm is deleted from this path"); "the raft road; the promotion-watchdog arm is deleted from this path");
pthread_mutex_destroy(&ctx->lock); pthread_mutex_destroy(&ctx->lock);
sem_destroy(&ctx->done); sem_destroy(&ctx->done);

View file

@ -120,8 +120,8 @@ int raft_on_vote_request(raft_state_t *s, int from_idx, uint64_t term,
/* Step 2: term upgrade — update term, clear vote, step down to FOLLOWER. /* Step 2: term upgrade — update term, clear vote, step down to FOLLOWER.
* If we were LEADER, notify the HA FSM so it updates ha->role. * If we were LEADER, notify the HA FSM so it updates ha->role.
* Without this, the HA FSM stays in CTRL_PRIMARY_COMMITTING and * Without this, the HA FSM stays in CTRL_PRIMARY after losing the term,
* eventually promotes to PRIMARY via the committing timeout → split-brain. */ * never stepping down to STANDBY → split-brain. */
if (term > s->term) { if (term > s->term) {
int was_leader = (s->role == RAFT_LEADER); int was_leader = (s->role == RAFT_LEADER);
s->term = term; s->term = term;