- Rename replConfigRequestMsg/replConfigPushRequestMsg/upgradeConfigRequestMsg
to replconfigrequestmsg/replconfigpushrequestmsg/upgradeconfigrequestmsg in
Request.body union to match existing lowercase convention
- Rename replConfigReplyMsg/replConfigPushReplyMsg/upgradeConfigReplyMsg
to replconfigreplymsg/replconfigpushreplymsg/upgradeconfigreplymsg in
RequestReply.data union
- Swap UpgradeConfigRequest fields so sha256@0/bytes@1 matches sibling
ReplConfigPushRequest pattern
- Move HeartbeatMsg out of the config replication section into its own
'# ── HA heartbeat ──' section above the replication structs
When a worker re-joins and already exists in workermgr, makeWorker_()
now overrides the existing slot instead of returning EEXIST. Admin-set
flags (LOCKEDU, CLOSED, HIDDEN, CLOSED_BY_ADMIN) survive the override.
Add XCE_WORKER_CLOSED_BY_ADMIN flag to distinguish admin-closed hosts
from workload-closed ones. worker_ctrl sets/clears it alongside CLOSED.
Refactor worker_init/worker_destroy into copy_worker/kill_worker helpers
shared by both fresh creation and override paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sel_key_cmp_fn compared keys with memcmp on the whole sel_node_key_t
struct, including the 4-byte padding between type and op1. The stack-
allocated search key in gc_sweep_select_nodes can have different padding
bytes than the heap-allocated copy stored by key_add, causing key_del
to miss the entry and leave a dangling pointer in the CSE intern table.
Replace memcmp with field-by-field comparison on type, op1, op2.
Also fix GC sweep tests that asserted pointer inequality after re-intern:
calloc may lawfully reuse the freed address, so pointer inequality does
not reliably indicate a successful sweep. Track the swept node's
node_id before freeing and assert the re-interned node gets a fresh id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire up 6 previously-stubbed message handlers in the scheduler:
resource dataset, resval dataset, job update notify, queue update
notify, resval update notify, and job rusage notify. Remove the
unreachable pushmsg branch (vtask_entry rewrites all pushmsg to
datamsg before the handler fires).
Changes by component:
- helper.h/helper.c: add xce_notify_event_name() for symbolic log names
- scheduler.h: add 5 new xce_sched_op_t values and matching union fields
- plugin.h/plugin.c: add plugin_job_rusage_hook_t and plugin_job_rusage_update()
- schedobj.c: add sched_update_existing_queue() with NULL/-ENOENT guards
- scheduler.c: wire 5 new dequeue cases; add sched_update_existing_job()
and static sched_op_name(); emit WARN log on handler failure
- handler.c: implement sched_handle_resources/resvals (dataset) and
sched_handle_sched_update_{job,queue,resvals}/job_rusage (notify);
remove dead sched_handle_jobpush/workerpush and pushmsg branch;
update TRACE logs to use symbolic name helpers
- test.c: 5 new unit tests covering notify event names, enum values,
and queue update NULL guard (15/15 pass)
Three fixes:
1. ExecHosts.UnmarshalJSON: JSON null (sent when no allocation exists)
was treated as a non-"-" string because json.Unmarshal(null, &str)
returns nil with str="". This created a phantom HostSlot{NumSlots:1,
HostName:""} producing "<1*>" for every PEND job. Now null and ""
are both treated as empty hosts.
2. page_print_jobs.go: added !startTime.IsZero() guard so a zero
timestamp ("0001-01-01T00:00:00Z") never produces a start line
even if the status check is bypassed.
3. Added PENDING_REASON field to JobFieldsMap so it is requested from
the API when -l is passed, and rendered in printJobsVerboseOutput
for PEND/PSUSP jobs before the start-time section.
Four bugs fixed:
1. persist.c: array job WAL loop aborted after index [0] when replication
returned -ENODEV (no standbys). Loop guard now uses the return value of
persist_wal_sync_append() which is 0 on local-disk success regardless of
replication status.
2. join.c: WAL-recovered worker slot (status=UNAVAIL) caused add_worker() to
return -EEXIST on reconnect. New EEXIST path in handle_worker_join() looks
up the existing slot by name, updates its endpoint, and clears UNAVAIL so
the normal hook/reply/on_worker_join flow proceeds.
3. xcectx.c: cbuf_get() in xce_hello() blocked indefinitely when the TCP
connection dropped because no error handler was registered. Added
handle_disconnect() which sets ctx->state=XCE_CTX_ERR and calls
cbuf_tryput(-ECONNRESET) to unblock the waiter, allowing
checking_connection() to detect the failure and rejoin.
4. cluster.c: persist_recover() was called before cluster_build_resources()
populated resmgr, causing restore_worker() → add_hostattrs_resvals() to
fail with EEXIST noise. Moved persist_recover() to after HOOK_AFTER_EVENT
so resmgr is fully populated before WAL replay.
All three persist hook functions (job/worker/queue) were ignoring the
situation argument, so they fired on both BEFORE and AFTER events.
BEFORE_EVENT hooks pass different va_list layouts than AFTER_EVENT:
- WORKER_JOIN BEFORE: (cluster_t*, char* name, int 0) vs AFTER: (cluster_t*, worker_t*)
- JOB_NEW BEFORE: (cluster_t*, xce_job_submit_t*) vs AFTER: (cluster_t*, xlist_t*)
Reading the wrong type from va_list caused a dereference crash.
Fix: return 0 immediately on HOOK_BEFORE_EVENT in all three hooks.
WAL payloads:
- Job state transitions (START/FINISH/FAIL/STOP/RESUME/UPDATE etc):
wal_jobstate_t {uint64_t jobid, uint32_t state} — restores exact state flags
- Queue state (QUEUE_CTRL): wal_queuehdr_t {uint32_t status} + name string —
re-applies admin open/close/active/inactive after config queues are built
- Worker CTRL: existing [uint32_t detail][capnp xce_worker_t] unchanged
New recovery functions:
- restore_job_state(c, jobid, state): looks up job, overwrites state flags
- restore_queue_state(c, name, status): looks up queue, overwrites status
apply_wal_entry: dispatches all WAL types including job state and queue
test_repl_apply.c: weak stubs for restore_job_state/restore_queue_state
so unit test binary links without job.c/queue.c
WAL hooks now write real Cap'n Proto payloads instead of stub uint32_t:
- WAL_JOB_SUBMIT: full xce_job_t serialized via encode_Job/write_Job
- WAL_WORKER_CTRL: [uint32_t detail][xce_worker_t capnp] via encode_Worker
- Other job events (START/FINISH/FAIL/STOP etc): uint64_t jobid only
restore_job(): deserializes Cap'n Proto xce_job_t, guards against duplicate
replay with key_find check, then calls create_job()
worker_from_capnp(): skips 4-byte detail prefix, deserializes xce_worker_t,
guards against duplicate replay, then calls restore_worker()
join.c: pass worker_t* (looked up by idx via vget) to WORKER_JOIN hook
so persist hook can serialize worker data without touching workermgr RTTI
Replace all fprintf(stderr,...) calls with INFO/WARN/ERROR/TRACE so
persist.c output appears in the controller's structured log rather than
mixing plain text on stderr with tlog-formatted output.
Three identical static copies (controller, worker, scheduler) replaced
with a single xce_parse_peers() in src/lib/cluster.c.
The libxce version is the canonical implementation: stores the logical
name from 'name=host:port' syntax so daemons on the same dev host can
identify themselves by name rather than IP address.
- xce_candidate_t gains name[] field for logical peer identity
- parse_peers stores name prefix before '=' (empty when omitted)
- cluster_init matches own identity by name first, hostname as fallback
enabling multiple controllers on the same dev host via
--peers ctrl-1=10.0.1.1:7000,ctrl-2=10.0.1.2:7001
- myconf_t gains peers_cfg (xce_cluster_config_t) populated from --peers
- controller/main.c: parse --peers host:port,... via argp; ca_cert_path
auto-derived from workdir/certs/xce.crt (no --ca-cert flag needed)
- cluster_init: derive my_priority from position in peers list (index 0 =
primary); fall back to 0 when --peers not given (single-node mode)
- Add HA_DEFAULT_* constants to ha.h; use them in cluster_init()
- Strip stale task-label comments (P1-3a, P1-5, P1-6, P3-6 refs)
- Remove self-documenting 'Step N:' annotations from ha_do_promote/demote
- Rewrite stale forward-reference notes as current WHY comments
- Remove resolved TODO P0-8b from cluster.c
- parsePeer: use net.SplitHostPort (handles IPv6, stdlib convention)
- tryConnect: take Go strings, own CGO allocation internally
- RegisterToBackend: remove unreachable 'no HA candidates' error path
- Sync Peers field comment between utils and backend config structs
Worker (xcecd):
- Add xce_cluster_config_t cluster_cfg field to controller_connector_ctx_t
(controller_connector.h includes xce/cluster.h)
- Populate cc_ctx->cluster_cfg from parsed cluster_cfg after xcecd_start()
sets cc_ctx (main.c, removes the P3 placeholder)
- Replace xcecd_join_cluster() single-shot connect with a candidate-iterating
loop: tries each candidate, calls xcecli_stop() before the next attempt,
falls back to the original single-host path when candidates_count == 0
Scheduler (xce_scheduler):
- Add xce_cluster_config_t cluster_cfg field to xce_scheduler_t
(scheduler.h includes xce/cluster.h)
- Add sched_set_cluster_cfg() setter
- Replace the hardcoded xcecli_start/xcecli_join pair in sched_init() with
the same candidate-iterating pattern; original single-host path preserved
- main.c calls sched_set_cluster_cfg() before sched_init() when --peers given
(removes the P3 placeholder comment)
checking_connection() already calls xcecd_join_cluster() on retry, giving
the worker free reconnect-after-failover behaviour for no additional code.
Worker (P2.5-5):
- Added parse_peers() static helper to src/worker/src/main.c
- Switched getopt to getopt_long to support --peers and --ca-cert long options
- When --peers is given: parses into xce_cluster_config_t, calls
xce_cluster_config_validate(), logs INFO with candidate count/identity/ca_cert
- If validation fails: prints clear error and exits(1)
- WARN logged when candidates_count > 3
- Backward compatible: no --peers falls back to existing single-controller path
Scheduler (P2.5-6):
- Added parse_peers() static helper to src/scheduler/src/main.c
- Extended argp struct with --peers (-P) and --ca-cert (-C) options
- Same validation/logging pattern as worker
- Backward compatible: no --peers falls back to existing single-controller path
Tests (new):
- src/lib/testsuite/test_cluster_parse.c: 8 unit tests for parse_peers logic
(single entry, multiple entries, no-name format, empty/null/missing-port errors,
name-stripped correctly, MAX_HA_CANDIDATES cap respected)
- Registered in test.c and meson.build
- All 8 tests GREEN; 3 pre-existing failures in unrelated GC/CSE tests unchanged
Add test-only function ha_trigger_manual_failover_for_test() to exercise manual
failover state machine (demote → promote cycle) without requiring live cluster_t.
Test coverage:
- test_manual_failover_transitions_role_and_increments_term: Validates role
transitions (PRIMARY → STANDBY → PRIMARY) and term monotonicity
- test_manual_failover_resets_hooks_registered: Confirms demotion resets the
hooks_registered idempotency guard
- test_manual_failover_monotonic_term_across_multiple_cycles: Validates term
remains monotonically increasing across 5 consecutive failover cycles
Implementation:
- Exposed only under #ifdef XCE_TESTING in ha.h
- Function body (6 lines): calls ha_do_demote() then ha_do_promote(); returns
the new term value via atomic_load_explicit()
- Enables design validation (P3-6) without test-cluster infrastructure
All tests pass. Build clean.
Add test_p2_integration.c with 7 integration tests covering the assembled P2
replication system: WAL_CHECKPOINT assert invariant, trim_lsn with active standby,
checkpoint must not update last_replicated_crc, sync degraded mode, full catch-up
lifecycle, needs_snapshot boundary conditions, and API completeness gate.
All 32 controller unit tests pass (0 failures).