Commit graph

12 commits

Author SHA1 Message Date
a81f2a249c fix(controller-tests): make Unity test-file attribution and counters consistent suite-wide
Only 21 of 86 test_*.c group functions called their own nested
UNITY_BEGIN()/UNITY_END(). Each nested call reset Unity's global
NumberOfTests/TestFailures counters, so controller_test.c's final
`return UNITY_END();` -- the value meson test actually gates on --
only ever reflected whichever small window of tests ran since the
LAST nested reset, silently discarding pass/fail evidence for
everything before it (measured: 16 of 972 total tests in one run).
The same nested-BEGIN calls also caused PASS/FAIL banners to show
the wrong source file for tests in files that don't reset it
themselves (Unity.TestFile just carries over from whichever group
last called UNITY_BEGIN()).

Fix: replace every nested UNITY_BEGIN() with UnitySetTestFile(__FILE__)
(sets file attribution only, no counter reset) and drop the matching
UNITY_END(); add the same UnitySetTestFile(__FILE__) as the first
statement to the other group functions that had neither. The 2
genuinely standalone binaries (test_job_array_sched_push.c,
test_job_array_apiserver_push.c -- each has its own int main(), not
registered via controller_test.c's TEST() macro) are untouched.

Verified: single consolidated summary now reads
"972 Tests 0 Failures 0 Ignored OK", exactly matching the
ground-truth RUN_TEST call-site count across all 83 registered
group files (976 total minus 4 belonging to the 2 standalone
binaries). Every PASS/FAIL banner now names its own true source
file. Full xce-build.sh all regression pass clean (Ok: 9, Fail: 0).

No production code touched -- test-harness plumbing only.
2026-07-17 14:49:12 +00:00
7eefe769a3 test(raft-ha-core-safety): fix 2 stale tests surfaced by first full-suite ASAN sweep (task 7.1)
A full 69-group sweep of controller_unit_test (this spec's harness had
never been run end-to-end before) found 2 deterministic failures,
both test-only staleness against already-shipped, already-approved
production behavior from earlier in this same spec:

- test_ha_vote_gate: test_pre_vote_catch_up_denies asserted the
  pre-task-5.3 unconditional in_catch_up PreVote deny. Task 5.3
  (bf88681) deliberately gated that deny on primary_ctrl_fd>=0 &&
  ha_heartbeat_is_fresh to fix a real round-4 election-stall — this
  test's exact scenario (stale heartbeat, primary_ctrl_fd=-1) is the
  case 5.3 intentionally flipped to grant. Split into
  test_pre_vote_catch_up_grants_when_primary_unreachable (matches the
  current contract) and test_pre_vote_catch_up_denies_while_primary_live
  (preserves the original protective intent, correctly scoped).

- test_egress_filter: test_live_submit_tags_submit_lsn called the
  JOB_NEW persist_job_hook_fun AFTER_EVENT hook directly, but task 5.9
  (15be5d9, bug-1306 fix) made that hook's WAL_MODE_WAIT branch
  notify-only, moving the real WAL append + commit barrier into
  persist_submit_wal_and_commit(). 5.9 relocated 4 equivalent tests in
  test_persist_hooks.c but missed this one. Now drives
  persist_submit_wal_and_commit() directly, mirroring that pattern.

No production code changed. Full 69-group suite: 0 Failures, 0 ASAN
reports (independently reproduced 3x: initial sweep, implementer,
reviewer).
2026-07-01 14:31:26 +00:00
91b9a445f0 fix(ha-term-safety): bug-828 partial — revive dead safe-commit-from-votes + advertise real durable tip
Clean failover was truncating committed jobs because the post-election
safe-commit-from-votes computation was dead: vote_reply_dispatch decoded each
granting voter's advertised peerLastLogLsn but discarded it, so ha_compute_safe
_committed_lsn saw only the self-vote (the 'from 1 granted voters' fingerprint)
and truncated to self_lsn.

Part 1: vote_reply_dispatch now records the granting voter's (lsn,tag,grant) into
ctx->votes[] via a record-only ha_record_vote_reply seam (no double-promotion;
raft_on_vote_reply stays the sole production promotion driver) — so safe-commit
reflects the true voted majority.
Part 2: the candidate/voter advertise the real DURABLE (fsync'd) tip; persist_wal
_write_replicated advances durable_lsn after its fail-loud inline fdatasync.

Reviewer-verified SAFE (advertises only durable positions; safe-commit bounded by
the durable majority, preserving more committed entries, never marking a
non-majority-durable LSN safe). 177 unit tests; new test_ha_vote_gate + election
tests green.

PARTIAL: does NOT yet make the live clean-failover pass. A deeper slice of
bug-828/795 remains — a catch-up'd standby still under-advertises its durable
position in the live run (the documented architectural catch-up/durability gap),
so a job-less node can still win and truncate. Tracked for a dedicated effort.
2026-06-23 03:13:02 +00:00
004a40bb3b feat(ha-term-safety): restore controller build + wire vote/AE term wire fields (tasks 1.1, 1.2)
1.1: WalCatchupEntry gains term@4/prevLogIndex@5/prevLogTerm@6 (append-only).
1.2: RequestVote/PreVote gain lastLogTerm@4/lastLogIndex@5; cluster.c vote callers
fixed to the extended library arity (RESTORES xce_controller + controller_unit_test
compile+link, broken by the raft-commit-safety signature extension); ha.c emits the
candidate's last-log tuple on send.

5 minimal stub log/commit callbacks wired into s_real_cbs so raft_init's new
non-NULL requirement is satisfied — tasks 2.x replace them with real bodies; the
logOk freshness gate is inert until the term seam (1.3) + on_get_entry_term (2.2).

Pre-existing test_repl_unit.c wal_catchup -EAGAIN failures + ASAN leaks are an
overlayfs /tmp fsync env artifact (bug-850), not from this task (untouched paths;
append-only schema cannot affect the @3 entries-list decode). Build/baseline
isolation is blocked by a c-capnproto flake-hash drift — see Implementation Notes.
2026-06-22 03:41:49 +00:00
3049b620f3 feat(ha-commit-durability): durable-backed vote/PreVote freshness floor + bare-durable candidate advertise (task 4.2) 2026-06-18 09:02:11 +00:00
749e194207 feat(ha-election): per-priority failover election stagger + prevote dispatch-gate
Desync FAILOVER re-elections by node priority (ha_election_stagger_ms /
ha_failover_stagger_fn) mirroring the startup stagger, with seed jitter and
async prevote start; allow prevote in voter roles through the dispatch gate.
Adds HA role and vote-gate tests (stagger desync by priority, prevote on dead
primary, stickiness-lapse grants).

Pre-existing uncommitted work, committed to establish a clean baseline before
ha-commit-durability implementation.
2026-06-18 04:16:54 +00:00
6f95e4cdfc feat(ha-prevote): controller voter-side PreVote dispatch (task 3.1)
Add OPCode_preVoteRequest case in cluster.c, mirroring the real-vote case:
freshness floor + reused eligibility/stickiness gate (raft_on_pre_vote_request),
reply over preVoteReply with the voter's UNCHANGED term (no adoption), denial
logging with candidate id + reason. Pure insertion; real-vote case untouched.
6 new vote-gate tests assert deny-while-leader-alive, grant-when-lapsed, and the
no-mutation/term-unchanged contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 05:21:12 +00:00
c97c7632e9 fix(ha-config-bootstrap,ha-repl-hardening): config-less node must not win election
Root cause (found via ha-convergence-recovery task 8 live 3-node E2E): a
controller with no local config (config_version_tag==0) could become PRIMARY
and push an empty config, breaking replication.

Two upstream election-safety gaps, both outside ha-convergence-recovery:

1. RDHUP re-election bypassed the no-config 'voter only' guard. The startup
   path (ha_start_election_async) refuses to start an election when
   my_config_tag==0 && n_peers>1, but the RDHUP failover path
   (ha_on_primary_ctrl_fd_rdhup) called raft_on_leader_lost unconditionally.
   Fix: mirror the guard before elect_ctx alloc / hook / raft_on_leader_lost;
   a config-less node stays CTRL_STANDBY (voter only) and grants the
   configured peer instead of starting (and winning) its own election.

2. Voter freshness gate compared the candidate only against the voter's LOCAL
   config, ignoring the cluster's known committed state. Fix: ha_vote_self_freshness
   floors the comparison at max(local, learned-committed config_tag / committed_lsn),
   so a voter denies any candidate whose config/cluster state is outdated vs
   what the cluster has committed. Strictly additive (never denies the freshest
   node — Leader Completeness preserves liveness).

TDD: test_rdhup_noconfig_node_does_not_elect + configured-node contrast;
test_ha_vote_gate learned-floor deny + liveness + backward-compat. Full
controller suite green under ASAN. commit.c and ha-convergence-recovery
files untouched.
2026-06-14 08:58:08 +00:00
7f974e766b test(ha-single-leader-role): sweep all COMMITTING fixtures to direct-PRIMARY contract (task 5.1) 2026-06-13 13:59:42 +00:00
71ce41f486 feat(ha-single-leader-role): delete dispatch-gate COMMITTING branch + collapse role predicates to PRIMARY (task 4.2) 2026-06-13 12:59:58 +00:00
9821ec4f3c fix(ha): election-safety hardening — vote gates, restored-vote term, freshness source
Election protocol fixes validated against raft.tla invariants:

- ha_init_raft(): one-time raft state init at startup (re-init would lose
  durable election progress); n_peers = full cluster size including self
- handle_request_vote(): higher-term step-down moved BEFORE the role gate
  (split-brain fix); standby vote-deny now requires a live primary control
  channel (primary_ctrl_fd >= 0) AND a fresh heartbeat
- ha_heartbeat_is_fresh(): derive freshness from ha->last_activity (the
  field production actually writes); the orphaned last_heartbeat timespec
  had no production writer, leaving the vote-deny gate inert (bug-337);
  field removed from ha_state_t
- handle_request_vote(): restore same-candidate idempotent re-grant —
  deny only a DIFFERENT candidate in the same term, matching raft.c and
  Raft §5.2 votedFor ∈ {Nil, candidateId} (bug-338)
- raft_init(): restore voted_for_term alongside a persisted vote; leaving
  it 0 allowed a double vote in the same term across a restart (bug-339)
- myapp_run(): leader_known check treats a vote recorded under this node's
  own name as a self-vote — production persists the real candidate name,
  the "self" placeholder only appears in test mode
- raft_on_vote_request(): leader steps down on higher-term vote request
  instead of denying it

Tests: test_proto_H6_restored_vote_blocks_second_vote added; H1 restore
assertion strengthened; test_ha_vote_gate.c driven through last_activity +
primary_ctrl_fd (the real production fields). Full /xce-build all green
(lib + controller + scheduler + Go suites, 0 failures, ASAN clean).

deploy-ha.sh: start.sh now forwards extra args to the controller binary.
2026-06-10 01:25:39 +00:00
6de4b3ff0e feat(election-iomgr-refactor): extend handle_request_vote with role-based deny gate — task 3.2 2026-05-30 11:03:35 +00:00