Extract cluster_init_managers() which initializes configmgr, jobmgr,
resmgr, workermgr, qmgr, sharedinst, xstrings, endpoints, and job_obj.
Call it from cluster_init_phase1 (after init_controller_tunables) so
all managers exist before election.
This fixes the heap-buffer-overflow in handle_connection: incoming
OP_requestVote SSL connections arrive during the pre-election phase
and call zarray_store(&c->endpoints) — which requires endpoints to
be initialized first.
cluster_init_phase2 is simplified: it no longer re-initializes these
managers and only sets up iomgr/SSL, opens the listener, and loads
cluster-config-dependent data.
Add ctrl_tunables struct to cluster_t with per-table fields:
jobmgr_nhash/delta, workermgr_nhash/delta, qmgr_nhash/delta,
resmgr_nhash/delta/inst, xstrings_nhash
init_controller_tunables() sets defaults (jobs=512, others=128)
and is called from cluster_init_phase1 so values are available
before cluster_init_phase2. Phase2 reads from c->tunables instead
of cluster_config or local #defines.
Each table gets its own values since they hold different item counts:
jobs can scale to hundreds, workers/queues/resources are typically
single-digit to tens.
Replace cluster_config-dependent hash sizes with compile-time constants
(CTRL_PERF_NHASH=512, CTRL_PERF_DELTA=64, CTRL_PERF_INST=64) for:
jobmgr, workermgr, qmgr, resmgr, xstrings
These are controller performance tuning parameters (capnp fields @13–@17)
that have no config generation tool yet. Using hardcoded defaults unblocks
cluster startup without requiring these fields in the config blob.
TODO: wire back to cluster_config once config generation tool is ready.
1. iomgr_monitor double-register: cluster_start skips iomgr_monitor
if c->ep was already registered by cluster_register_listener
(ep->flags non-zero after successful registration). Previously
returned EEXIST → -1 → "Operation not permitted" crash.
2. ASAN stack-use-after-return: cluster_fini now calls iomgr_stop()
before iomgr_fini() to join the worker thread before the cluster_t
stack frame is reclaimed by the caller.
3. Election timing: add ha_wait_for_heartbeat_or_timeout() before
election in myapp_run. If an existing primary sends a heartbeat,
node becomes STANDBY immediately without competing in election.
If timeout expires (no primary running), proceed to election.
This prevents ctrl-a from immediately losing when ctrl-b/c are
not yet started — it now waits election_timeout_ms (2s) first.
vote_request_thread connects to peers via SSL. iomgr was started before
election but c->ep was only registered with epoll inside cluster_start(),
which runs after election. The TCP connect succeeded (kernel accepts it)
but SSL_accept was never called server-side, so SSL_connect failed
immediately — both vote threads went to deny in <1ms and ctrl-a could
never win a quorum.
Fix: add cluster_register_listener() that wires c->ep into iomgr's
epoll watch. Call it from myapp_run() right after iomgr_start/vtaskmgr_start,
before election. cluster_start() still calls iomgr_monitor again
(idempotent via EPOLL_CTL_MOD).
The vote_request_thread was sending raw capnp(RequestVoteRequest) bytes
but the server-side iomgr decodes every incoming packet as a Request
struct. This caused the term field (uint64) to be misread as the opcode
field (uint16), so vote RPCs were silently misrouted (term=1 → OPCode_reply,
term=2 → OPCode_hello) and no node ever got a vote from its peers.
Fix:
- controller.capnp: add requestvotemsg @27 to Request.body and
requestvotereplymsg @14 to RequestReply.data
- typemap.h: move request_vote_req_t/reply_t before xce_request_t;
add requestvotemsg/requestvotereplymsg to body/data unions
- ha.c: rewrite vote_request_thread send/recv to use xce_encode()
wrapper and decode outer Request→inner RequestReply on receive
- cluster.c: add case OPCode_requestVote handler calling
handle_request_vote() and replying via xce_reply()
All 188 unit tests pass.
- Probe all peers: any tag>0 -> defer_config_fetch=true; all-zero -> proceed;
none reachable -> exponential backoff 500ms->10s cap with WARN per attempt
- Wire into myapp.c: before ha_wait_for_heartbeat_or_timeout()
- Post-election: defer_config_fetch && CTRL_STANDBY -> fetch_config_from_primary()
- INFO log for all three bootstrap outcomes (Req 10.2)
- XCE_TESTING hook for probe mock; test_ha_bootstrap_support.c installs mock
- 9/9 bootstrap tests GREEN, 141/141 full suite GREEN
- sem_timedwait on heartbeat_seen semaphore with CLOCK_REALTIME deadline
- deadline = now + election_timeout_ms + xorshift64 jitter
- Returns CTRL_STANDBY on heartbeat arrival (calls ha_do_demote)
- Returns CTRL_ELECTING on timeout (caller drives election)
- Allows OPCode_heartbeat through dispatch gate in CTRL_INIT state
- 3 new unit tests: standby-on-heartbeat, electing-on-timeout, gate-allows-heartbeat
- 24 Tests 0 Failures in test_ha_startup suite
- errnox.h: add E_XCE_NOT_READY and E_XCE_STANDBY error codes with
extern int XCE_ERR_NOT_READY / XCE_ERR_STANDBY runtime variables
- ha.h: declare ha_dispatch_gate(ha_state_t *, int opcode)
- ha.c: define XCE_ERR_* variables; initialize heartbeat_seen semaphore
and primary_ctrl_fd = -1 in ha_init(); sem_destroy in ha_fini();
implement ha_dispatch_gate() with allowlist for CTRL_INIT/CTRL_ELECTING
(only OP_requestVote and OP_probeConfigTag pass) and CTRL_STANDBY
(replication, election, and housekeeping ops pass; primary-only ops
return -XCE_ERR_STANDBY)
- cluster.c: wire ha_dispatch_gate() in myreq_handler() before opcode
switch; gate failure sends error reply and returns early
All 393 unit tests pass. Satisfies requirements 1.3 and 1.4.
- Implement validate_peer_count() in startup_validation.c: -EINVAL for even N>1
- Implement validate_cert_files() with err_buf output: access() check, writes
missing path into err_buf, returns -ENOENT
- Wire validate_peer_count() in main.c after peer list parse; fatal exit on fail
- Wire validate_cert_files() in main.c before SSL init; fatal exit on fail
- Fix ASAN leak: SSL_CTX_free in ha_fini() with NULL guard
- Fix double-free: null c->ha.ssl_ctx in cluster_fini() before ha_fini(),
so iomgr_fini() retains sole SSL_CTX ownership (design per ha.h comment)
- All 96 tests pass; all 12 ha_startup tests GREEN
- Add --vcpus N argp option in main.c; store in myconf.vcpus
- Replace inline argp guard with validate_vcpus() call (returns -EINVAL for <=0)
- Add validate_startup_config() in startup_validation.c: returns 0; writes
deprecation warning into caller-supplied warn_buf when vcpus key present
- Wire validate_startup_config() into myapp_run() after conffile resolved;
emit WARN() if warn_buf non-empty (Req 4.4)
- Remove vcpus field from cluster_config_t (clusterconfig.h)
- Preserve capnp ordinal: vcpus @7 -> vcpusDeprecated @7: Void in controller.capnp
- vtaskmgr_init uses only myconf->vcpus with hardcoded default 8
- Add stub C types in typemap.h for capnp schema extensions from task 1.1
- 96 tests pass
RED-phase tests for 4 startup validation functions — all fail at link time
because the functions under test do not exist yet:
- validate_peer_count: even N>1 returns -EINVAL, odd N returns 0
- validate_cert_files: missing cert/key returns -ENOENT with full path
in err_buf output param (asserted via strstr)
- validate_vcpus: vcpus<=0 returns -EINVAL, positive returns 0
- validate_startup_config: vcpus key in config returns 0 (not fatal)
and writes deprecation text into warn_buf (asserted via strstr)
Pre-decode opcode peek in myreq_handler() captures raw packet bytes for
OP_upgradeConfig before xce_decode() frees the capnp segment. Passes
raw bytes to xce_upgrade_config_req_extract_raw() to extract sha256 and
config blob, then delegates to do_upgrade_config_with_bytes().
Removes Task 11 dependency. Phase 2 still gated by CONF_ENABLE_UPGRADE_CONFIG.
Replaces -ENOSYS stub with:
- connect_ssl_raw() helper (non-blocking connect + poll + SSL handshake,
checks ha_is_primary() between iterations for early promotion exit)
- xce_replconfig_req_ssl() to send OP_replConfig request
- xce_replconfig_reply_ssl_decode() to receive and decode reply
- SHA-256 integrity verify (retry on mismatch with -EBADMSG)
- conf_save_config() atomic disk write to workdir/name/conf/
- load_primary_config() to reload cluster_config from newly saved file
Standby can now fetch ClusterConfig from primary on startup.
xce_replconfig_reply_raw() - primary sends reply with real bytes via endpoint
xce_replconfig_req_ssl() - standby sends request over raw SSL socket
xce_replconfig_reply_ssl_decode() - standby decodes reply from raw SSL
xce_upgrade_config_req_extract() - documents architectural limitation; returns -ENOTSUP
xce_upgrade_config_req_extract_raw() - extract bytes from raw packet for OP_upgradeConfig
Same bypass pattern as existing xce_replconfigpush_req_raw(): build the
capnp message directly via capn_new_list8 + capn_setv8, sidestepping the
encode_Data_ptr path that routes raw bytes through xce_data_t (a
discriminated cluster-data union that cannot carry raw byte arrays).
Unblocks handle_config_request() and do_one_config_fetch() wiring.
Task A research finding: xce_upgrade_config_req_extract(xce_request_t*)
is unimplementable because xce_decode() calls capn_free() before any
handler runs, and decode_Data_ptr converts the wire-format List(UInt8)
into a zero-filled xce_data_t. Task D must capture the raw io_packet_t
bytes before xce_decode tears them down, then call the _raw variant.
Replaces blocking pthread_join() per thread with a CLOCK_MONOTONIC
deadline + 5ms poll loop. CONF_PUSH_TIMEOUT_MS (500ms) now enforced.
Matches design §6.3 aggregate timeout guarantee.
- Add done field to push_ctx_t; push_thread sets it on exit
- Add static wait_all_with_timeout() before push_config_to_standbys()
- Wire push_config_to_standbys() to use wait_all_with_timeout()
- Add #include <stdatomic.h> to config_repl.c
- Guard push_ctx_t local typedef with #ifndef XCE_TESTING
- Expose push_ctx_t and wait_all_with_timeout_for_test() in header
under XCE_TESTING for unit test access
- Add test_wait_all_with_timeout_slow_threads (timeout fires, acked=0)
- Add test_wait_all_with_timeout_fast_threads (all complete, acked=3)
- Update design doc §6.3 with actual implementation signature and
5ms poll interval note
load_primary_config() reads from workdir/name/conf — conf_save_config()
was writing to workdir/conf. Latent bug: would have corrupted upgrade
restart cycle once Task 11 wired the upgrade path.
Update all conf_read_current_filename, conf_get_saved_tag, and
conf_check_stale calls in the test suite to use the corrected
effective_dir (workdir/name) path structure. Add new test
test_conf_save_config_path_consistency that explicitly verifies the
written path matches what load_primary_config() reads.
Crash: encode_NameValue_list called with count=21, s=NULL on worker
rejoin, causing SIGSEGV in persist_worker_hook_fun.
Root cause: free_name_val_list(xce_name_val_t***, int) accepted count by
value. It zeroed *vals (resvals_to_sched = NULL) but could not zero
nresvals_to_sched. Any caller that freed the scheduler resource snapshot
(opget.c workerlist, reply.c clusterconf, dataex.c, sched.c dispatch)
left nresvals_to_sched stale. When the worker later rejoined and the
WAL HOOK fired, the WAL encoder read count=21 with a NULL pointer.
Fix: change signature to free_name_val_list(xce_name_val_t***, int*)
so both fields are zeroed atomically. Update all 7 call sites.
Root cause: test_heartbeat_stale_no_crash_without_primary had
heartbeat_timeout_ms=100 < heartbeat_interval_ms=3000, causing ha_init
to return -EINVAL before memset. The test ignored the return value and
called ha_fini on garbage stack memory. The watchdog_thread field
happened to hold the value 3 (from a prior stack frame), so
ha_stop_watchdog called pthread_join(3, NULL) — crash at 0x2d3
(__pthread_clockjoin_ex accessing thread-struct field at offset 0x2d0).
Fix 1 (test): valid config (interval=2000, timeout=3000). Staleness is
injected via last_heartbeat -= 10s, so timeout value was irrelevant.
Added TEST_ASSERT_EQUAL_INT check on ha_init return.
Fix 2 (ha_init): move memset before validation so ha_fini is safe to
call even after a failed init — prevents the same bug class in callers
that check ha_init's return late.
Verified: test_ha_role 11/11 PASS in both non-ASAN and ASAN builds.
Replace monolithic add_worker with three focused functions:
- apply_join_state (static under production, exported under XCE_TESTING):
shared status/resource update called by both create and rejoin paths;
clears XCE_WORKER_UNAVAIL, applies hconfig overrides, updates resources
- init_worker: first-time join path via makeWorker + apply_join_state
- rejoin_worker: reconnect path with explicit admin-bit preservation
(XCE_WORKER_ADMIN_MASK = LOCKEDU|CLOSED|HIDDEN|CLOSED_BY_ADMIN)
Also makes find_matched_hostconfig non-static so join.c can call it
via the extern declaration already in worker.h.
Removes add_worker (no longer declared in worker.h, no callers).
All 7 tests in test_worker_join pass:
apply_join_state (clears UNAVAIL, hosttype override, resource values),
init_worker (creates entry, returns correct idx),
rejoin_worker (updates attrs/ep, preserves admin bits).