Commit graph

548 commits

Author SHA1 Message Date
481e197928 feat(xce-test-e2e-primitives): session verdict + exit code + abort/continue + comment handling (task 1.1) 2026-06-19 15:32:11 +00:00
64ab210cbf fix(ha-wal-replication): apply_wal_entry must not own the WAL CRC chain (bug-778)
On the standby replicated-apply path, ha_apply_single_entry wrote the entry via
persist_wal_write_replicated (which correctly set the self-computed CRC chain
link p->last_crc), then called apply_wal_entry with a SYNTHETIC header whose
crc=0. apply_wal_entry's `p->last_crc = hdr->crc` clobbered the chain link with
0, so every subsequent replicated write stamped prev_crc=0 on disk. The on-disk
CRC chain broke at the second replicated entry — invisible while live (standbys
serve in-memory state) but fatal on COLD RESTART: replay hit the break, returned
RECOVER_TRUNCATED, recovered only the config prefix, and lost all committed jobs
(joblist=0). The sticky truncated verdict then drove a promotion-fence
availability wedge (bug-777) when every survivor was truncated.

Fix — single ownership of the WAL header + CRC chain:
- apply_wal_entry is now LOGICAL entity-restore only: signature
  (p, type, lsn, payload, payload_len); no header. It advances the current_lsn
  high-water (monotonic) but NEVER touches p->last_crc.
- The physical layer is the sole owner of the CRC chain: persist_wal_write_-
  replicated on the live path, replay_wal/persist_recover on recovery.
- ha_apply_single_entry drops the fabricated header and passes entry fields
  directly. The persist_recover replay caller is updated to the new signature.

Verified: new RED->GREEN test test_replicated_apply_chain_survives_cold_restart
(drives the real ha_apply_wal_catchup_entry path, cold-replays -> RECOVER_CLEAN,
all N recovered); controller suite 0 failures; 3-node rolling-SIGKILL soak on a
real ext2/3 fs now passes 5/5 (was wedge+loss at round 3); full cold restart
recovers all 6 committed jobs (was 0) with CLEAN verdicts and a normal election.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:12:53 +00:00
276e26c37b test(scheduler-controller-msg-gap): real handler/applier unit tests (bug-765 G-6)
The Task 9 tests were marked [x] but were stub/absent. Convert the
correctness-critical ones to real white-box tests, registered as meson tests
under --suite scheduler (leaks-off per convention):

- test_handlers.c (new) — #include ../src/handler.c; covers 9.2/9.3: each
  notify/dataset handler enqueues the correct op / skips NULL-empty / -EINVAL.
  Asserts the 5.6 fix (update_job -> UPDATE_JOB_FIELDS, not bulk UPDATE_JOB).
- test_job_update.c — add 9.5 (real sched_update_existing_queue, replacing the
  former local stub); already covers 9.4 (job) and 9.7 (rusage).
- meson.build — build + register all three scheduler test executables.

Suite GREEN: sched_test 15/15, sched_job_update_test 10/10,
sched_handlers_test 6/6; ASAN-clean.

Honest remaining: 9.6 dequeue worker-resvals APPLY is PARTIAL (enqueue tested;
apply is a dormant path, R-3); 9.8 is a documented compile-time absence check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 04:14:20 +00:00
080ff78a80 docs(scheduler-controller-msg-gap): reconcile sched_op_name layering (bug-765 G-7)
The spec/design placed the scheduler-op name lookup as xce_sched_op_name in
lib/helper, but xce_sched_op_t is a scheduler-only type and src/lib must not
depend on scheduler headers — so it is correctly a static sched_op_name() in
scheduler.c. Code kept as-is (correct); docs amended to match:
- Task 1 intro + 1.1/1.3 + 5.7 + 9.1: scheduler-layer placement + layering note.
- design.md Component 8/9, file table, risks: same reconciliation.
Only xce_notify_event_name (int-typed, lib-level) lives in lib/helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 03:51:01 +00:00
93124c5601 feat(scheduler-controller-msg-gap): store job rusage on the job + free leaked op payloads (tasks 5.5, 5.8; bug-765 G-5/G-3)
5.5 (Req 6 / G-5): the JOB_RUSAGE notify path called the plugin but never
stored the usage data — xce_sched_job_t had no rusage field (UB-6.1 unmet).
- Add xce_job_rusages_t* rusages to xce_sched_job_t.
- New sched_apply_job_rusage() stores the report ON the job (ownership
  transferred), replacing any prior via free_JobRusages_ptr; -ENOENT unknown,
  -EINVAL NULL. Dequeue NULLs the request pointer on success; kill_sched_job
  releases job->rusages.

5.8 (G-3): sched_pend_update_killer left five new-op payloads on empty
break cases — a per-message leak since ownership is transferred on enqueue.
- Free each with the exact generated deep-freer: free_QueueConf_ptr,
  free_Resource_ptr[], free_ResourceVal_ptr[], free_ResValList_ptr,
  free_JobRusages_ptr. Safe: handlers nullify on enqueue, makeResource copies
  via makeInstance, dequeue copies values (no aliasing).

Tests: sched_job_update_test 7/7 (3 update + 4 rusage), regression
sched_test 15/15, ASAN clean (no bad-free/double-free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 03:33:28 +00:00
5c1f715ccf feat(scheduler-controller-msg-gap): route notify job-update to in-place sched_update_existing_job (task 5.6, bug-765 G-8+G-4)
The notify schedUpdateJob path shared XCE_SCHED_OP_UPDATE_JOB with the
dataset bulk-load, so the dequeue routed both to sched_update_jobs()
(create/replace) — a notify update to an existing job corrupted/duplicated
live job state and never returned -ENOENT (Req 3 UB-3.1/UB-3.2 unmet;
sched_update_existing_job was dead code copying only state).

- Add distinct op XCE_SCHED_OP_UPDATE_JOB_FIELDS (+ jobfieldsupdate union),
  enqueued only by the notify handler, dequeued to sched_update_existing_job.
- Move sched_update_existing_job to schedobj.c; update all mutable fields
  (state, priority, queue, resreq) in place; -ENOENT unknown, -EINVAL NULL.
- Dataset bulk-create path unchanged. Killer frees the new payload.
- New white-box test sched_job_update_test (3/3); regression sched_test 15/15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 03:17:40 +00:00
d9b43c2d2f chore(ha-commit-durability): mark 5.1 + all phase groups complete (all 18 sub-tasks done) 2026-06-18 11:21:10 +00:00
f139e1c08e docs(ha-commit-durability): flip bug-661 KNOWN GAP to GUARANTEED + re-verify protocol checklist (task 5.3) 2026-06-18 11:15:13 +00:00
d128cc96ef test(ha-commit-durability): verify zero acked-job loss + cluster availability across failover on no-self-exit build (task 5.2) 2026-06-18 11:12:15 +00:00
59d9df246c fix(ha-commit-durability)!: controller never self-terminates — loud non-promotable fence replaces recovery FATAL/abort
Per design invariant (a controller exits only on crash or admin kill), replace the
self-abort recovery terminal (tasks 3.3/4.3/4.4) with a loud, permanent, non-promotable
fence that stays alive and keeps retrying catch-up (self-heals on peer return; no manual
admin recovery). Records the never-self-terminate invariant as project-wide guidance in
src/controller/CLAUDE.md + .kiro/steering/ha-async-io.md.
2026-06-18 11:10:19 +00:00
addf53e3b4 feat(ha-commit-durability): bounded-window catch-up before FATAL — fence no longer FATALs synchronously (task 4.4) 2026-06-18 10:35:13 +00:00
e14bd62115 test(ha-commit-durability): end-to-end durable-majority commit + liveness integration tests (task 5.1) 2026-06-18 09:56:26 +00:00
e9e2a6d703 feat(ha-commit-durability): pre-promotion eligibility gate + catch-up fence/release (task 4.3) 2026-06-18 09:40:32 +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
4b7457144c docs(ha-commit-durability): pin durable-backed freshness-floor formula + election safety/liveness verification (task 4.1) 2026-06-18 08:44:42 +00:00
c1c35ab1b3 feat(ha-commit-durability): publish durable floor from recovered WAL tip (task 3.4) 2026-06-18 08:24:46 +00:00
e3fc42c11e feat(ha-commit-durability): loud bounded FATAL on unrecoverable recovery (task 3.3) 2026-06-18 08:15:22 +00:00
6483ee8afd docs(ha-commit-durability): pin boot-policy decision table + pending skeleton tests (task 3.2) 2026-06-18 08:00:52 +00:00
dd4d70863b feat(ha-commit-durability): WAL recovery integrity verdict (CLEAN/TRUNCATED) + recovery_truncated flag (task 3.1) 2026-06-18 07:43:01 +00:00
526bca9679 feat(ha-commit-durability): gate legacy sync-append commit store on durable_lsn + commit<=durable invariant (task 2.4) 2026-06-18 07:09:47 +00:00
e4c4968331 feat(ha-commit-durability): gate standby ACK on fdatasync success + persist-before-ACK regression test (task 2.3) 2026-06-18 06:45:21 +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
cc83caf5a0 feat(ha-commit-durability): demand-driven group-commit flusher + durable_lsn watermark (task 2.1) 2026-06-18 06:06:55 +00:00
7e6f09f804 test(ha-commit-durability): deterministic strace-based ack-before-durable gap repro (task 1.3) 2026-06-18 05:39:06 +00:00
f90395282c feat(ha-commit-durability): add XCE_TESTING durable-LSN-on-disk verification seam (task 1.2) 2026-06-18 04:56:50 +00:00
1ca2d640cb feat(ha-commit-durability): add --wal-sync-ms flush-cadence override (task 1.1) 2026-06-18 04:46:01 +00:00
80bb1d766e fix(libxce-failover): classify and propagate netio I/O errors on failover
Add io_classify_io_error and propagate endpoint/epoll errors through
endpoint_update_epoll and cbuf_get instead of swallowing them, so client
failover surfaces the real error rather than hanging. Adds netio arm-failure,
close-propagation, and request send-propagation tests.

Pre-existing uncommitted work, committed to establish a clean baseline before
ha-commit-durability implementation.
2026-06-18 04:17:07 +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
223e1b8578 docs(libxce-failover): record live-E2E round-2 fixes (bug-609 EINVAL, bug-610 reconnect SEGV) 2026-06-17 06:54:28 +00:00
ffc2c590f9 fix(libxce-failover): correct endpoint reconnect on rotate — re-bind iomgr + re-arm handlers (fixes endpoint_connect SEGV during live failover)
xce_failover_rotate reconnected with endpoint_fini+endpoint_init+endpoint_connect
but never re-bound ep->iomgr (reset by endpoint_init) nor re-armed the owner/
msg/err handlers + iomgr_monitor. endpoint_connect then dereferenced a NULL
iomgr (->ssl_ctx) → SEGV at netio.c:672 during live master failover. Added
xce_ctx_reconnect() in xcectx.c that mirrors xcecli_start's establishment
(without restarting iomgr/vtaskmgr or touching ctx->state); rotate delegates to
it. Found via live 3-node E2E. bug-610.
2026-06-17 06:53:46 +00:00
312bd6be34 chore(wolf): record libxce EINVAL-guard gotcha + sandbox live-cluster limits 2026-06-17 04:48:01 +00:00
16f4ba67c7 docs(libxce-failover): log bug-609 (known-dead EINVAL) + record 6.2 live-E2E partial status 2026-06-17 04:46:45 +00:00
fa65e794ec fix(libxce-failover): known-dead ctx (XCE_CTX_ERR) triggers reconnect instead of surfacing -EINVAL
Live 3-node E2E revealed the reported 'Invalid argument' bug was still
reachable: after the primary dies, handle_disconnect leaves ctx.state=
XCE_CTX_ERR, so a FRESH xce_* call returns -EINVAL from the xceapi guard
before reaching a primary. The classifier mapped -EINVAL to TERMINAL, so the
loop bailed without rotating/recovering. decide() now detects the known-dead
ctx and treats it as a prompt reconnect cause (RETRY_NOW), surfacing the
distinct cluster-not-ready code on budget exhaustion, never -EINVAL.
Regression test: test_decide_known_dead_ctx_retries_not_terminal.
2026-06-17 04:46:01 +00:00
c616f41364 test(libxce-failover): consolidate unit suite green + jitter staggering coverage (task 6.1) 2026-06-17 04:26:00 +00:00
5d1d1e3f12 feat(libxce-failover): controller populates primary host/port on E_XCE_REDIRECT replies (task 5.3) 2026-06-17 04:16:26 +00:00
fdb8938059 feat(libxce-failover): best-effort single-shot reconnect for fire-and-forget ops (notify/heartbeat/nop) (task 5.2) 2026-06-17 04:08:58 +00:00
4345974b54 feat(libxce-failover): wrap 18 reply-bearing xcecli_* ops in failover loop under per-ctx lock; correct nop OpCategory (task 5.1) 2026-06-17 03:57:51 +00:00
8783ce5a9d feat(libxce-failover): reconnect/rejoin hook — public setter + post-reconnect invocation before resume (task 4.1) 2026-06-17 03:46:05 +00:00
12284bc3f5 feat(libxce-failover): reconnect recovery — ctx ERR->RUN reset + stale wbuf drain (fixes post-failover EINVAL) (task 3.2) 2026-06-17 03:36:24 +00:00
84c5e32c2c feat(libxce-failover): retry decision loop — pure decide (budget/backoff/jitter, D6 exhaustion) + thin next wrapper (task 3.1) 2026-06-17 03:27:38 +00:00
cec2e647dc feat(libxce-failover): capture redirect hint in handle_reply (before free, before status return); verify endpoint-error wake (task 2.3) 2026-06-17 03:11:18 +00:00
c7e11a4e9e feat(libxce-failover): endpoint rotator — hint-first target selection + master rotation; keep api_ctx_t decls private (task 2.2) 2026-06-17 02:56:48 +00:00
fa2ac05036 feat(libxce-failover): 4-action status classifier with attempt-aware DUP-replay rule (task 2.1) 2026-06-17 02:42:54 +00:00
d11c6e1d89 feat(libxce-failover): reusable failover test harness — fake masters, status taxonomy, concurrency runner (task 1.2) 2026-06-17 02:32:19 +00:00
387ccfa8dc feat(libxce-failover): foundation — per-ctx failover state, redirect-hint fields, remove orphaned retry primitive (task 1.1) 2026-06-17 00:05:41 +00:00
8c11655e53 fix(persist): interruptible fsync_worker sleep so persist_stop joins promptly
fsync_worker slept the full wal_sync_ms in one uninterruptible nanosleep()
before re-checking fsync_stop. persist_stop sets the flag and immediately
pthread_join()s, so the join blocked until the in-flight sleep returned —
up to a full wal_sync_ms per teardown. Tests set wal_sync_ms=5000 ("slow
fsync — no races"), so each persist_stop cost ~5s; across the ~7 suites
that use it the cumulative teardown time blew past the 300s controller-test
meson cap, killing the suite with SIGTERM.

Replace the nanosleep with pthread_cond_timedwait on a new fsync_mutex/
fsync_cond (added to persist_t, init in persist_init, destroyed in
persist_fini). persist_stop now sets fsync_stop and signals fsync_cond
under fsync_mutex before joining, so the worker wakes immediately. fdatasync
still runs under p->lock, released before re-acquiring fsync_mutex — no lock
nesting. The "commit: WAIT deadline expired" log lines were unrelated
intentional negative tests (50-300ms), not the cause.

Full controller suite now completes in ~10s (was >300s TIMEOUT); 0 failures.

Logged as bug-604.
2026-06-16 08:24:28 +00:00
5503c7a176 chore(ha-prevote): resolve log-reason nit; track elect_ctx leak as follow-up spec
- Mark the PreVote denial log-reason nit RESOLVED in ha-prevote tasks notes (47f7338).
- File ha-elect-ctx-lifecycle spec scaffold (spec.json + brief.md) capturing the
  pre-existing, bounded, UAF-sensitive election-carrier leak and the refcount/
  teardown-ordered-free approach across both election and PreVote paths. Deferred
  by decision — not folded into ha-prevote to avoid a risky lifetime refactor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:56:07 +00:00
47f733891b fix(ha-prevote): make PreVote denial log-reason match the eligibility gate order exactly
The cluster.c denial-reason elimination ordered catch_up before fresh_heartbeat,
disagreeing with raft_cb_vote_eligible (which checks fresh-heartbeat-on-STANDBY
before catch-up). In the rare STANDBY catch-up + fresh-heartbeat overlap the log
could report catch_up while the gate denied on fresh_heartbeat. Expose the pure
read predicate ha_heartbeat_is_fresh and mirror the gate's exact order so the
logged reason can no longer disagree with the actual denial. Decision unchanged;
log-string fidelity only. prevote/vote-gate/failover-prevote suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 07:48:50 +00:00
c45f80ab2b commit missed 2026-06-16 15:42:43 +08:00
a8840dbba5 chore(ha-prevote): record validation GO + commit-barrier-timeout learning 2026-06-16 07:37:50 +00:00