Commit graph

22 commits

Author SHA1 Message Date
06e99b9081
fix(controller): size the config-push barrier for loaded hosts and admit its wait (bug-1914)
The configAck byte-delivery barrier waited 500 ms, below scheduling
delay on an oversubscribed host, so a push to a healthy majority was
rejected with -110 while its already-appended marker still committed.
CONF_PUSH_TIMEOUT_MS is now 2000 ms, the wait holds a commit-wait
admission slot (new commit_blocking_admit/release; a full budget fails
fast with not-ready), and the timeout reply no longer claims the change
was not committed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 23:59:29 +00:00
e6c484a0e7
feat(job-array-one-based-index): classify every job submission first (task 2.1)
handle_jobsubmit classifies (first_jobid, array_size) before the field
checks, the before-event hook and any registry access: a tagged array
starting at index 0 is answered with the new E_XCE_ARRAY_START0 and an
ERROR log asking for the client to be upgraded (7.1, wins per 7.9);
size 0, tags 2/3, untagged multi-element submissions and arrays whose
last element exceeds 32767 (checked without addition) get -EINVAL.
Nothing is stored for a rejected submission. This also closes a crash:
a size-0 submission used to reach payload building and overflow calloc.
Existing array tests move to tagged block|1 first IDs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 09:44:12 +00:00
b5d59253e5
fix(controller): stop a failed test from hanging the unit suite (bug-1429); mkstemp for upgrade temp file (bug-1861)
A failing TEST_ASSERT longjmps out and skips the fixture teardown, leaving
the commit engine's sweep thread and persist's fsync_worker asleep on a
static struct. The next test re-inits it underneath them, corrupting the
condvar, and a later teardown blocks in pthread_cond_destroy/join until
the 300s meson timeout. The failure line itself was lost because stdout
was fully buffered.

- controller_test: line-buffer stdout; tearDown() reaps what a failed test
  leaked via new XCE_TESTING registries (persist_test_reap_unstopped, then
  commit_test_reap_unfinalized_engines -- persist_stop before engine fini).
  Clean run reaps 0; an injected failure now fails in 2s instead of hanging.
- persist: flusher_flush_once reports only its own durable advance, so
  fsync_worker no longer re-fires the durable callback for an advance a
  concurrent persist_flush made (flaky "must not re-fire" test). Regression
  test test_flusher_pass_does_not_claim_concurrent_flush_advance.
- tests: jap observer is atomic and waits (bounded) for a continuation that
  may resolve on the flusher thread; pump catch-up tests wait for trailing
  replies instead of a single scan.
- config_repl: upgrade decode temp file via mkstemp instead of a
  pthread_self()-derived /tmp name (same value across processes, opened
  without O_EXCL -- collisions and a symlink target).

Verified: controller_unit_test 1222/1222; 24 parallel runs (6x4), no
hangs or ASAN errors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:15:03 +00:00
9f655c61c7
feat(worker-rejoin-resync): task 6.2 - route job-state report to reconciliation
Add OPCode_jobStateReport dispatch in myreq_handler, reassembling a
report across multiple incoming pages before handing it to task 3.2's
resync_apply_report() exactly once - proven via a test-only call
counter, not just end-state equivalence (a double-apply could pass an
end-state check if idempotent).

Standby-redirect investigation (Req 8.1/8.3): confirmed
ha_dispatch_gate already redirects every rejected opcode uniformly, so
no opcode-specific code was needed for that. OPCode_jobStateQuery has
no controller-side receive path at all (only sent, never received)
so needed no gate change either.

That same investigation found a different, more consequential gap:
fixes bug-1753, ha_dispatch_gate's quorum-loss fact-admission allowlist
(OPCode_notify/update/setpreason/clrpreason) was missing
OPCode_jobStateReport, so a report arriving during a transient
quorum-loss window - this feature's own core failover scenario - would
be rejected outright with no retry (worker sends fire-and-forget).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
2026-09-01 21:06:42 +00:00
0e96a69f7d
fix(bug-1429): persist_stop() before commit_engine_fini() in 21 fixtures
The oldest unresolved bug in this project's tracker (first sighted
2026-06-20): controller_unit_test intermittently hung for the full
300s meson timeout with no crash/ASAN report, never previously caught
live under gdb.

Live-reproduced this session (first attempt): thread 1 stuck forever
in commit_engine_fini() -> pthread_cond_destroy() (test_commit_barrier_
rollout.c's jap_teardown), while two fsync_worker threads sat parked
in pthread_cond_timedwait on the same persist_t. Root cause: several
test teardown functions called commit_engine_fini() BEFORE
persist_stop() -- the exact opposite of commit_engine_fini()'s own
documented contract. This let the live background fsync_worker thread
call back into commit_on_durable_advance() while commit_engine_fini()
was mid-destroy of the engine's lock/condvar -- a genuine TOCTOU race
with no synchronization between the two threads.

This is the same race already root-caused once for a SEGV symptom in
bug-1452 (2026-07-19, test_persist_truncate.c) and partially re-fixed
in bug-1687 (2026-08-25, cbrp_teardown) -- but never swept across all
affected files. Bug-1452's own remediation note explicitly flagged
test_demotion_convergence.c as a known, unfixed instance; that flag
went unactioned until now.

Fix: insert persist_stop() immediately before commit_engine_fini() at
21 confirmed-vulnerable sites across 12 files (test_apply_pump.c,
test_apply_pump_integration.c, test_commit_barrier_rollout.c,
test_commit_durability.c, test_commit_engine.c,
test_convergence_verification.c, test_demotion_convergence.c,
test_egress_filter.c, test_ha_role_lifecycle.c, test_job_array_fence.c,
test_job_array_replay_fence.c, test_p2_integration.c) -- the same
pattern bug-1452/1687 already established. persist_stop() is
idempotent (guarded by an atomic "already running" check), so each
fixture's own pre-existing persist_stop() call becomes a safe no-op.
No production code changed.

Known remaining exposure, documented but not fixed this session:
test_config_repl.c (~23 sites) and test_term_safety_adapter.c
(~5 sites) have the same ordering bug in their own teardown helpers,
flagged as follow-up.

Verified: full controller suite run 4 times total (once as part of
the fix, three further independent sequential runs) -- Ok:6 Fail:0
every time, 38-42s wall clock each run, no hang, nowhere near the
300s timeout. 1220/1220 individual controller_unit_test cases pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QxaSzHRB7pmYFp4cVnwkA
2026-08-30 16:04:43 +00:00
41af05402d feat(config-upgrade-snapshot): remove restart/replay fallback, marker-trigger materializes alone
do_upgrade_config_with_bytes drops step 9 (wait-for-checkpoint,
restart_requested=1, cbuf_put stop signal) -- materialization now
happens entirely via task 5's apply_committed_entry WAL_CONFIG_MARKER
case on every member's own apply, no restart required. Confirmed
do_upgrade_config_with_bytes was the sole production setter of
restart_requested; cluster.c's read of it is now permanently
false-for-this-trigger, which is the intended behavior.

Adds xce_upgrade_marker_committed_hook (#ifdef XCE_TESTING, mirrors
xce_snapshot_chunk_hook's nullable-function-pointer shape), firing
right after config_commit_change succeeds -- a deterministic seam for
later leadership-change fault injection (task 8.7) at exactly the
marker-commit-to-every-member-installed window. NULL by default, true
no-op in production.

Task 7 of 19.

Requirements: 1.2, 1.3, 1.4, 2.2, 5.4, 6.3
2026-07-25 05:37:27 +00:00
ba6fdc575e feat(controller): clusterconfig-schema-split task 3 — fix test fixtures for shrunk cluster_config_t
Drops the two cfg.port = ...; struct-literal assignments in
test_commit_barrier_rollout.c and test_config_repl.c (removed field, task
1.2). Repo-wide sweep found no other genuine cluster_config_t literal
referencing a removed field.

Full clean build now succeeds (153/153 targets) and controller_unit_test
passes 976/976 with zero new failures.
2026-07-21 04:52:25 +00:00
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
78eb983a74 test(job-array-single-wal-record): close remaining array append/commit gaps
Adds 3 targeted tests after confirming tasks 2.1/2.2 already cover
most of this task's observable criteria:
- one-entry/one-continuation swept across sizes {2,3,100,1000}
  (existing coverage only tested size 5)
- payload-size boundedness at the real idgen()-enforced ceiling
  (MAX_ARRAY_IDX+1 = 32768), plus an absolute byte-cap assertion
  (existing coverage only compared 2 vs 10000)
- array_size==1 baseline: explicit WAL-byte readback proving the
  entry type is WAL_JOB_SUBMIT (not WAL_JOB_ARRAY_SUBMIT) and the
  payload decodes via struct Job, not struct JobSubmit (existing
  coverage only proved "old tests still pass" implicitly)

No production code changed -- test-only.

Task 2.3 of job-array-single-wal-record.
2026-07-14 10:53:20 +00:00
7c4afbbb83 feat(job-array-single-wal-record): implement single-record array append/commit
Adds persist_submit_job_array_async(): encodes one JobSubmit-shaped
payload (base jobid, array size, one shared spec), appends exactly
one WAL_JOB_ARRAY_SUBMIT entry, and registers exactly one commit
continuation regardless of array size -- reuses the existing
job_commit_adapter_ctx_t/job_commit_resolve_adapter machinery, no
per-element loop. Not yet wired into handle_jobsubmit (task 2.2).

Also fixes a test-helper bug (WAL offset back-computed from the
post-append cursor, only valid for zero-payload entries) that
corrupted state across tests when exercising a nonzero-payload
array-submit entry.

Task 2.1 of job-array-single-wal-record.
2026-07-14 09:50:00 +00:00
2e62e73d84 feat(job-array-single-wal-record): add WAL_JOB_ARRAY_SUBMIT record type
Adds the array-shaped WAL entry type (distinct from WAL_JOB_SUBMIT,
value 11 in the unused WAL_JOB_* gap) and classifies it WAIT-mode via
wal_classify, matching the single-job type's commit-barrier semantics.
No dispatch/apply wiring yet -- foundation for task 2.x/3.x.

Task 1.1 of job-array-single-wal-record.
2026-07-14 06:47:14 +00:00
c3201a2b1e feat(ha-term-safety): append config markers via unified log + derive committed config (task 4.2)
Wires do_upgrade_config_with_bytes to append WAL_CONFIG_MARKER through
raft_log_append (live term-stamped consensus record), demotes committed_config_tag
to a non-authoritative cache, and derives the committed config from the latest
committed marker. Preserves the derive across checkpoint compaction via an optional,
backward-compatible config-ref tail in the snapshot sentinel.
2026-06-22 10:34:10 +00:00
20e59d0b42 checkpoint: bug-851 root cause + raft TLA+ conformance spec family
Investigation & fixes (failover committed-job loss):
- root-cause bug-851 on an owned 3-node cluster: chaotic failover causes
  silent committed-prefix divergence (WAL hdr.term always 0; two leaders
  per term; no (term,lsn) reconcile). Evidence in repro-851/ + INVESTIGATION.
- live-validated fixes 848/849/850 (recovery-on-promotion, advertise real
  applied_lsn, persist-layer contiguity guard); add waldump WAL inspector.

Specs (strict raft.tla conformance — library owns the log):
- TLA-gap-analysis.md: impl vs raft/raft.tla, gaps G1-G6.
- raft-log-replication, raft-commit-safety, raft-sim-conformance: extend
  src/lib/raft to own log/commit/verification via callbacks (specs generated,
  cross-spec review passed).
- ha-term-safety: controller-side adapter + config marker (R1-R7); design done.
- config-upgrade-snapshot: R8 descoped here (deps entity-bearing-snapshots, bug-857).
- roadmap + buglog (851/854/857) + .wolf knowledge updated.
2026-06-21 14:02:15 +00:00
c41edc515e feat(ha-commit-durability): cap commit self-contribution at durable_lsn + durable-advance commit wake (task 2.2) 2026-06-18 06:29:47 +00:00
7519fa96dd fix(ha-commit-engine,persist): reserve WAL lsn 0 — first entry starts at lsn 1
Root cause (found via live 3-node E2E + a verification test): lsn was 0-based
(hdr.lsn = current_lsn++ from current_lsn=0), and 0 was overloaded as a
sentinel, causing three cold-start defects on a freshly-promoted leader:
  - the first WAL entry (own-term promotion NOOP at lsn 0) was structurally
    uncommittable: commit_advance_locked only stores when cand>cur and
    committed_lsn starts at 0, so lsn 0 can never advance it;
  - commit_set_writable_lsn(noop_lsn=0) set writable_lsn=0, which wait_done
    reads as 'gate off' — the own-term safety gate was a no-op on the first leader;
  - bug-543 (WAL hole): ha_stream_wal_to_standby filters hdr.lsn>standby_lsn;
    a fresh empty standby reports standby_lsn=0 (ambiguous 'empty' vs 'has lsn 0'),
    so lsn 0 failed 0>0 and was dropped -> non-contiguous standby WAL.

Fix: a fresh persist store initializes current_lsn=1, reserving lsn 0 as the
none/empty sentinel. The first real entry is lsn 1 -> committable (0->1),
writable_lsn=1 -> gate effective, and standby_lsn=0 unambiguously means 'empty'
so catch-up streams from the first entry with no hole. persist_recover is
data-driven (current_lsn=max(hdr.lsn+1)), so existing on-disk WALs and non-fresh
recovery are unaffected; the change only sets the base for brand-new stores.
No wire/capnp change.

Verification: test_coldstart_lsn0 drives the real commit engine + real streamer
and proves H1 (first entry commits 0->1), H3 (gate effective + blocks when
committed<writable), H4 (empty-standby catch-up is contiguous [1,2], control
standby_lsn=1 gets only [2]). Full controller suite green under ASAN. Tests that
relied on the lsn-0 base were shifted (several strengthened); commit.c /
config_repl.c / ha.c / wire format untouched.

Owning specs: ha-commit-engine / ha-wal-replication / persist. Follow-up:
sweep stale lsn-0 comments (repl.c:429-436, persist.c:1055-1075/1240-1243/1296).
2026-06-14 10:46:16 +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
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
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
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