Commit graph

299 commits

Author SHA1 Message Date
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
e399328339 test(ha-convergence-recovery): regression pins for fan-out, snapshot, degraded-gate, re-inclusion (task 6.1) 2026-06-14 06:56:57 +00:00
3218d9ffaf test(ha-convergence-recovery): RED quorum transition logging tests + seam (task 5.1) 2026-06-14 06:09:01 +00:00
c2221077ff test(ha-convergence-recovery): RED ACK-driven catch-up continuation tests + seam (task 4.1) 2026-06-14 05:16:54 +00:00
63c12461a5 feat(ha-convergence-recovery): clamped rejoin reconciliation + intake trigger (task 3.2) 2026-06-14 04:53:08 +00:00
5521720b0c test(ha-convergence-recovery): RED clamped rejoin reconciliation tests + intake extraction (task 3.1) 2026-06-14 04:18:44 +00:00
09103a3141 feat(ha-convergence-recovery): self-restart primitive + demotion convergence (task 2.2) 2026-06-14 03:47:12 +00:00
c8e55947c2 test(ha-convergence-recovery): RED demotion-convergence + self-restart tests + seams (task 2.1) 2026-06-14 03:12:25 +00:00
b9ddfa60cb feat(ha-convergence-recovery): commit-gated scheduler egress — submit_lsn tagging + watermark gate (task 1.2) 2026-06-14 02:33:36 +00:00
7ff81156c2 test(ha-convergence-recovery): RED egress-filter tests + submit_lsn field + dataex decls (task 1.1) 2026-06-14 01:40:22 +00:00
93dfc02717 feat(ha-single-leader-role): remove CTRL_PRIMARY_COMMITTING enum + remaining decls — compiler-enforced (task 5.2) 2026-06-13 14:41:42 +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
a5bf86112a feat(ha-single-leader-role): delete force-promotion watchdog + convergence/ack machinery (task 4.1) 2026-06-13 12:05:08 +00:00
b7b951b9e7 feat(ha-single-leader-role): legacy election path symmetry — direct CTRL_PRIMARY via shared sequence (task 3.1) 2026-06-13 10:16:14 +00:00
c089fb69f3 feat(ha-single-leader-role): direct CTRL_PRIMARY promotion + own-term gate via shared sequence (task 2.2) 2026-06-13 09:26:08 +00:00
b7a69afa0b test(ha-single-leader-role): RED lifecycle tests for direct promotion + own-term gate (task 2.1) 2026-06-13 08:31:42 +00:00
6df1283f1a test(ha-single-leader-role): RED tests for WAL_NOOP entry type (task 1.1) 2026-06-13 07:30:43 +00:00
2308389eb1 test(ha-commit-barrier-rollout): RED tests for mark_job_* guards (task 6.1)
3 RED carriers (duplicate FINISH on DONE appends a 2nd WAL entry +
overwrites state; START on terminal clobbers it; replay non-equivalent)
+ 2 preservation pins (forward postdone PDONE-merge, fail-after-finish
PERR-merge) that 6.2 must not break. Reviewer-confirmed: the
UPDATE-clears-terminal arm is production-unreachable (pinned, not
driven). 22 tests / 3 RED; census 508 distinct triples, only those 3
fail.
2026-06-13 04:20:05 +00:00
5a2fae1d37 feat(ha-commit-barrier-rollout): config barrier with production ACK ingestion (task 5.2 + r1)
configAck branch: per-push majority (acked_config_lsn >= push_tag,
1+count > n_peers/2 over r->ce->n_peers, n_peers<=1 immediate,
ce==NULL -EINVAL); legacy >0 counting and n_active==0 shortcut dead.
push_config_to_standbys returns the barrier result; upgrade failure ->
send_upgrade_error (rc forwarded), no success ACK, no restart,
committed_config_tag success-only.

Remediation r1 (two CRITICAL review findings):
- F1: wire production config-ACK ingestion — replconfigpushreplymsg
  decode branch in repl_ack_decode_and_dispatch -> repl_on_config_ack
  on the promoted-vote ep; + defensive cluster.c OPCode_reply case.
  Closes the unlanded half of ha-config-bootstrap 8.1; without this the
  barrier would brick every multi-member config upgrade. No schema change.
- F2: keep the barrier off the iomgr thread — export
  push_config_transport_only (non-blocking) for the 5 ha.c + join.c
  streaming sites; barrier path now vtask-only (do_upgrade_config).

Wire-path test drives real capnp ACK bytes through the production decode
branch. 30/30 rollout, 6/6 repl_majority, 46/46 config_repl; census 0
FAIL; pre-existing fixture leak only (less than baseline).
2026-06-13 03:46:29 +00:00
9cdeba5816 test(ha-commit-barrier-rollout): RED tests for config barrier (task 5.1)
6 RED carriers pinning the three configAck defects (previous-tag acks
count, n_active==0 success shortcut, no-self-count denominator) and the
push/upgrade failure path (logged-only result, unconditional success
ACK + restart). 2 sanctioned pins (single-member, success-unchanged).
Behavior-preserving signature change: push_config_to_standbys gains
acked_out (return still legacy push count; 7 call sites NULL'd,
body-diff verified). 29/6 in group; no collateral failures.
2026-06-12 16:31:42 +00:00
732e836868 test(ha-commit-barrier-rollout): fact-retention-across-quorum-loss proof (task 4.3)
Green integration proof: 4 facts driven during total quorum loss append
RETAIN with rc 0 in <1000ms (vs 5000ms timeout — wrongly-applied WAIT
tripwire), pending with retained-depth=4 logged, present exactly once
on disk (real WAL header scan); reconnect + one covering ACK commits
the suffix with current_lsn unchanged (no redelivery). Completes the
bug-396 fix evidence (umbrella Req 2.8). Census 495/495.
2026-06-12 15:29:47 +00:00
4fa38c6f88 test(ha-commit-barrier-rollout): RED tests for dispatch-gate fact allowlist (task 4.1)
4 RED carriers (notify/update/setpreason/clrpreason must be admitted
without quorum — currently NOT_READY, the bug-396 hole) + 2 pins (six
request opcodes stay rejected; quorum-present admits all ten). Census
494 = 488 + 6; exactly 4 FAILs.
2026-06-12 14:05:27 +00:00
7103fa643d test(ha-commit-barrier-rollout): receipt-ACK verdict pins (task 3.3)
4 green pins: handle_jobdispatch replies receipt-only with zero WAL
appends through a real 2-alloc dispatch cycle (notify outcomes intact,
reply-precedes-processing); set/clr_preason and do_update_op rusage
stay receipt-style with RETAIN-pending fact appends ACK-committed.
Umbrella Reply-Ordering Contract cited per pin. DISCOVERY bug-451
(reviewer-verified): RUSAGE hook va_list type confusion — 4-byte OOB
read + garbage state in WAL_JOB_RUSAGE payloads; fix deferred to 7.1.
Census 488; zero failures.
2026-06-12 11:16:51 +00:00
e1c3c173f4 test(ha-commit-barrier-rollout): RED commit-first-then-reply tests per opcode (task 3.1)
New test_commit_barrier_reply.c drives do_jobcontrol_op /
do_queuecontrol_op / do_hostcontrol_op / handle_worker_join directly
(pthread harness, ep->outq reply capture, wired engine n_peers=3).
12 cases: 8 RED (reply-before-commit + bug-447 pins), 4 validation
pins green. DISCOVERY bug-447 (reviewer-confirmed): key_find found-rc
leak means queue/host control never append WAL in production — 3.2's
rc normalization is the fix point. Census 484 = 472 + 12; zero
baseline regressions.
2026-06-12 10:00:38 +00:00
611fbc8d7b test(ha-commit-barrier-rollout): RED tests for persist_commit_barrier (task 2.1)
7 cases: two covered short-circuits, timeout with committed_lsn
unmoved, covering-ACK release, sync_timeout_ms fallback (bounded both
directions), n_peers=1 standalone, NOT_READY passthrough via cap
saturation. Watermark resolved to current_lsn - 1 (design's literal
phrasing unsatisfiable per persist.h:71 invariant) with off-by-one
killing power both ways. 15 tests / 7 RED; suite baseline zero.
2026-06-12 05:09:11 +00:00
fec9519960 test(ha-commit-barrier-rollout): RED tests for classification totality (task 1.1)
New test_commit_barrier_rollout.c: per-type rows matching the VERIFIED
design table (STOP/RESUME/UPDATE/FAIL RETAIN per supersession), 13-value
totality sweep, fork/SIGABRT unknown-type fail-fast, fact-hook retention
contract against real persist+engine. wal_commit_mode_t + wal_classify
declared in persist.h; inert sentinel stub. 8/8 RED; suite baseline
zero preserved. Finding: RETAIN is registration-free (commit.c:233-239)
— observable contract documented in Implementation Notes.
2026-06-12 04:13:22 +00:00
95a10cedf3 test(ha-commit-engine): full regression green + legacy-path audit (task 6.1)
Fixed the 3 bug-411 baseline failures honestly: stale timeout
expectation updated to the reject-on-timeout contract (engine-wired
fixture, killing power retained); two healthy-primary gate fixtures
populated with standby_endpoints (gate logic untouched). Census: 46
groups / 467 tests / 0 failures standalone under default ASAN.
Audit (reviewer-confirmed, recorded in tasks.md): no production walAck
path reaches the deprecated acked_lsn>0 counting or n_active==0
short-circuit except the documented LSN-0 first entry. Leak inventory
for 6 groups documented as follow-up.
2026-06-12 03:32:19 +00:00
f0d6074d4e feat(ha-commit-engine): engine instantiation + sync-append path switch (task 5.3)
cluster_t embeds commit_engine_t; init in cluster_init_phase2 after the
vtaskmgr guard (idempotence-guarded — phase2 can run twice), wiring
repl_set_commit_engine + ha.repl back-pointer before any standby can
register; fini after iomgr/vtask shutdown. persist_wal_sync_append
passes written_lsn; engine-wired path drops -ENODEV and the
committed_lsn store (engine owns advancement); LSN-0 first-entry
routes to legacy per the accepted interim. p2 -ENODEV pin updated to
the Req 6.3 contract. 34/34 + 6/6 green through honest delegation;
census exactly at the 3 validated baselines; production binary links.
2026-06-12 00:16:22 +00:00
d1353360cc test(ha-commit-engine): RED integration tests for ACK feed and sync-append (task 5.1)
Tests 30-34: wired repl_on_ack feeds engine, membership mutators
trigger re-evaluation, per-entry sync-append contract (stale-ACK
rejection, committed_lsn unmoved on timeout, n_peers=1 replaces
-ENODEV). test_repl_majority walAck cases rewired to per-LSN
semantics (configAck byte-identical). Sanctioned scaffolding:
entry_lsn param on repl_wait_majority_ack (legacy body byte-identical,
callers pass 0), inert repl_set_commit_engine stub. 9 RED failures;
baseline signatures unchanged. LSN-0 edge documented for 5.2/5.3.
2026-06-11 23:09:05 +00:00
c9154bd5cc feat(ha-commit-engine): honest write-admission quorum accounting (task 4.2)
repl_active_standby_count: registry truth (ep != NULL && !in_catch_up)
under r->lock. ha_state_t gains repl back-pointer (wired at init by
task 5.3); ha_has_write_quorum uses registry truth when wired, exact
legacy fallback when NULL; formula unchanged. Test 29 (wired gate,
catch-up-only standby → NOT_READY, admits after clear) captured RED
pre-fix. 29/29 group tests; suite at the 3-failure baseline.
2026-06-11 22:28:44 +00:00
b2598823c0 test(ha-commit-engine): RED tests for write-admission quorum counting (task 4.1)
Tests 21-28: repl_active_standby_count contract (5 RED vs inert -1
stub) + 3 gate-level pins (n_peers<=1 always-true, zero-connected
reject, stateless re-accept) that 4.2 must preserve. Declaration in
repl.h; ha.c/ha.h untouched (4.2 boundary). 28 tests / 5 RED; suite
at the 3-failure baseline with unchanged signatures.
2026-06-11 16:15:21 +00:00
631a08939b test(ha-commit-engine): membership re-evaluation regression tests (tasks 3.1+3.2 joint)
Tests 17-20: catch-up ACK exclusion vs pending waiter, membership-change
wake of covered waiter, removal-below-majority fails at own deadline
(no forced early-fail), non-blocking bound. Landed GREEN: task 1.2's
commit_on_membership_change already implements the full 3.2 contract,
so RED was unachievable; reviewer confirmed mutation-killing power and
zero production changes. 20/20 group tests; suite at baseline.
2026-06-11 15:48:42 +00:00
d91f446e9e test(ha-commit-engine): RED tests for WAIT mode, admission cap, own-term gate (task 2.1)
9 new cases (8-16): block-until-majority, deadline timeout, standalone
fast path, zero-ACK regression, n_vcpus-relative cap with floor, ACK
progress at saturation, gate as blocking predicate (NOT_READY is
admission-cap-only per corrected contract). Main-thread-only assertions
after joins; 16 tests / 9 RED failures; baseline unchanged.
2026-06-11 14:56:31 +00:00
d5a0f9c64d test(ha-commit-engine): RED tests for per-LSN majority evaluation (task 1.1)
commit.h umbrella-verbatim interface + inert stub commit.c (test target
only); 7 Unity tests covering per-LSN vs acked_lsn>0 counting, majority
median, monotonicity, append clamp, lagging standby, RETAIN immediacy.
All 7 fail RED as intended. Documented 3 pre-existing masked suite
failures (bug-411) and the per-group exit-code masking root cause.
2026-06-11 14:17:09 +00:00
06135954ff chore: baseline snapshot — prior-session work + ha-consistency-invariants spec family
Pre-existing uncommitted work: ha-repl-hardening payload data plane (7.x),
heartbeat liveness fix (Req 1.3), connection-reuse ASAN fixes, doc cleanup.
New: ha-consistency-invariants umbrella (requirements/design/research) and
its four implementation sub-specs (specs generated via batch, cross-spec
review applied), steering roadmap + product.md Raft+CP decision.
2026-06-11 13:21:50 +00:00
ab270fab99 test(ha-wal-replication): registration lifecycle, broadcast, ACK tests (tasks 5.1-5.3)
Add test_repl_registration_lifecycle covering the complete 3-step standby
lifecycle: register (nstandbys==1, ep pointer verified), duplicate
(idempotent, same slot), deregister (ep==NULL, nstandbys==0 — the
previously-untested slot-zeroing invariant). Tasks 5.1/5.2 were already
covered by test_broadcast_one_standby_sends_entry, test_wal_wire
round-trip, and test_wal_ack_reply_updates_acked_lsn_and_signals.
18 repl unit tests pass under ASAN.
2026-06-10 11:41:43 +00:00
e2fba08f5d test(ha-wal-replication): verify majority-ack advances committed_lsn (task 4.2)
Fix defective assertion in test_committed_lsn_advances_after_majority_ack
(before/after delta instead of >0 which fails for lsn=0 first write).
Add test_single_node_committed_lsn_unchanged documenting -ENODEV degraded
mode for no-standby path. Fix NACK test expected return from -ETIMEDOUT to
0: with 1 registered standby, quorum=1 is met by primary self-count alone.
308 tests pass, 0 fail under ASAN.
2026-06-10 11:18:25 +00:00
861b746c84 feat(ha-wal-replication): primary WalAckReply handler (task 4.1)
Handle incoming WalAckReply in repl_ack_decode_and_dispatch: extract
lsn+success, call repl_on_ack on success to update standby acked_lsn
and signal majority-ack waiter. NACK is WARN-logged with no state
change. Two unit tests verify positive path (acked_lsn updated, waiter
unblocked) and negative path (NACK leaves acked_lsn=0, wait times out).
2026-06-10 09:46:22 +00:00
566c91cd8b feat(ha-wal-replication): standby WAL ACK reply (tasks 3.1-3.3)
Standby sends WalAckReply via xce_reply after each successfully
applied WAL entry. ha_apply_wal_catchup_entry signature extended
with endpoint_t* and cluster_t* parameters. walackreplymsg union
arm added to RequestReply capnp schema and xce_reply_t. Error
entries skip ACK. Dispatch gate test confirms OPCode_walCatchupEntry
allowed in CTRL_STANDBY/ELECTING/INIT.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-10 09:14:25 +00:00
81dbc4d7b4 feat(ha-wal-replication): HA→replicator standby registration bridge (tasks 2.1-2.3)
Register standbys with replicator on connect (election promotion +
late-joiner), deregister on disconnect (vote err handler + standby err
handler). repl_add_standby now idempotent (duplicate ep returns
existing slot).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-10 08:35:02 +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
f236d320a2 refactor(ha-repl-hardening): standardize n_peers = total cluster size including self
- ha_state_t.n_peers now matches raft_config_t.n_peers convention:
  total cluster size (including self), consistently across both layers
- ha_start_election_async: ha->n_peers = n_peers (candidates_count)
- Convergence/counting loops: i < n_peers - 1 (skip self for standby arrays)
- Single-node: n_peers=1, threshold=0 (same behavior, clear semantics)
- ha_elect_ctx_t.n_peers unchanged (separate field for vote array sizing)
- Tests: 3-node n_peers=2->3, single-node 0->1, loop bounds updated

E2E test 6.7: 3-node cluster lifecycle verified
- Initial: ctrl-1 PRIMARY via majority convergence, no degraded mode
- Kill primary: RDHUP detected on both standbys, ctrl-2 elected at term=4
- No data loss: config tag preserved across failover
- Full ASAN build green
2026-06-05 09:57:23 +00:00
345d51dd61 feat(ha-repl-hardening): audit safe commit point and snapshot transfer tests (tasks 6.3, 6.4)
6.3: Fix single-node bug — ha_compute_safe_committed_lsn now returns
self_lsn instead of 0 for n_peers=0 (prevents WAL truncation on
single-node re-election). Add self_lsn/self_tag to ha_elect_ctx_t,
populated unconditionally in ha_start_election_async. All 3 test
scenarios pass under ASAN.

6.4: Add test_snapshot_recv_128kb_two_chunks (128KB snapshot in
2x64KB chunks, verifies size and applied_lsn); 4 other test cases
already existed from tasks 3.5c/3.5d. All ASAN-clean.
2026-06-05 06:21:00 +00:00
d41cfc5813 feat(ha-repl-hardening): audit unit tests for decision loop and WAL catch-up (tasks 6.1, 6.2)
6.1: Add missing test_evaluate_repl_action_stream_wal_fresh_system
(tag=T, lsn=0, oldest_wal_lsn=0 → STREAM_WAL in fresh system);
5 other required test cases already existed from tasks 3.1/3.2/5.2/2.3

6.2: Add test_wal_catchup_batch_256_entries (max batch size boundary)
and test_wal_catchup_batch_applied_lsn_only_after_full_batch (Req 5.5);
4 other required test cases already existed from tasks 2.1/2.2
2026-06-05 05:59:16 +00:00
0762577bf7 feat(ha-repl-hardening): committing timeout 60s degraded-mode safety net (task 5.3)
- Change HA_DEFAULT_COMMITTING_TIMEOUT_MS from 10000 to 60000
- committing_timeout_fn: count converged/pending standbys from
  standby_converged[], log WARN with counts, then promote (degraded mode)
- Replace hardcoded 10000 in test_ha_evaluate_committing.c with macro
- Three new tests: default=60000, degrade-promote with 1/2 and 0/2 converged
2026-06-05 05:03:40 +00:00
534bfd68fe feat(ha-repl-hardening): majority-convergence → CTRL_PRIMARY transition (task 5.2)
- Add ha_check_majority_convergence() in ha.c: counts standby_converged[],
  promotes CTRL_PRIMARY_COMMITTING → CTRL_PRIMARY when all standbys converge
- Threshold: n_peers (all standbys) per spec test cases; single-node satisfies immediately
- Called from repl_ack_decode_and_dispatch() before replication dispatch
- Logs INFO "majority converged (N/M standbys) → CTRL_PRIMARY" (Req 7.4)
- Three unit tests: 1/2 converged → stays COMMITTING; 2/2 → PRIMARY; single-node → immediate
- Fix ha.h comment: clarify double-promotion prevention via vtask context, not CAS
2026-06-05 04:42:53 +00:00
d13010e4e0 feat(ha-repl-hardening): reduce handle_ctrlhello to connection setup only (task 5.1)
- Strip handle_ctrlhello: remove repl_add_standby, WAL streaming,
  snapshot check, push_config_to_standbys, and CtrlHelloReply
- Keep only: set primary_ctrl_fd, call ha_on_heartbeat_received,
  arm ha_arm_heartbeat_timeout_vtask, log INFO
- All replication decisions now handled by heartbeat decision loop
- Test: call with NULL repl + config_tag mismatch → no crash, no repl
  messages enqueued, primary_ctrl_fd set correctly
2026-06-05 03:54:43 +00:00
5915150c3b feat(ha-repl-hardening): set committed_lsn from vote-derived safe point (task 4.2)
- In raft_cb_become_leader, after persist_truncate_wal_above(safe_lsn),
  store safe_lsn to ha->last_known_committed_lsn via atomic release
- Test: 3-node voters [100,150,200] → safe_lsn=150; asserts
  ha->last_known_committed_lsn == 150 after raft_cb_become_leader_for_test
2026-06-05 03:28:59 +00:00
e7919304c3 feat(ha-repl-hardening): safe_committed_lsn from vote replies (task 4.1)
- Add ha_compute_safe_committed_lsn() in ha.c: collects LSNs from
  granted voters (self + peers), sorts descending, picks index
  floor(n_granted/2) for majority-threshold value
- Wire into raft_cb_become_leader: compute safe_lsn/safe_tag from
  ha->elect_ctx after winning election, call persist_truncate_wal_above
- Four unit tests in test_ha_evaluate_committing.c:
  voters [99,100]+self=100 → 100; voters [50,51]+self=100 → 51;
  single-node → 0; denied voter excluded → self-only
2026-06-05 02:41:22 +00:00