merge: sync Rongsong's scheduler-framework (c5b172c7..3442a57d, 781 commits)

Merge origin/scheduler-framework into merge-sf-20260903 (97 commits on
ours: bug-1490/1494/1498/1509/1513/1521/1522 fixes, PEND-reason P1,
scheduler-agent Phase 2, bug-924 owner-pid shim).

Bookkeeping excluded per policy: .kiro/, .wolf/, CHANGES.md, reports/
(92 files dropped); .claude/ + CLAUDE.md keep ours.

Key theirs-side changes absorbed:
- bug-1938/1960: QUEUE_STAT_CLOSED no longer gates dispatch (closed
  queue refuses new submissions only; pending jobs still run) --
  supersedes our QUEUE_CLOSED preason on the dispatch path;
  sched_queue_status_reason drops that branch
- bug-1846: config push now stashes and applies between scheduling
  rounds (sched_stash_config/sched_apply_pending_config), applied at
  the same main-thread round boundary that preserves bug-1513
  serialization
- bug-1760: sched_check_pending_rejoin exposed +
  xcecli_is_connection_broken public predicate
- bug-1936: execuser/exechome/execcwd/exdata carried on submission
  (superset of our bug-1500 execuser fallback; our job-typed
  resolveExecUser removed as dead code)
- bug-1945~1962: apiserver auth hardening (HS256 pinning, default-
  secret rejection, logout revocation, bcrypt, RBAC) -- our API-key
  branch folded into controllers.ParseTokenClaims so both coexist
- bug-1736: rawRunUsage job run-usage fields in the Go model
- xce_test relocated src/controller/testsuite/test.c ->
  src/harness/test.c; controller suite split into
  controller_test.c + fixtures; their clitest fixtures spawn xce_test
  mid-suite under `meson test -j`

Ours preserved across the merge:
- PEND-reason P1 (codes 1-9, preason.h, job_preason machinery) --
  gate re-ported into their relocated allocator (sched_election.c)
  after plugin_job_election, with attach on the allocator thread
  only (bug-1521 race discipline)
- PEND-reason P0 main_reason semantics RECONCILED to theirs: the
  merge initially re-carried our first-detail-add-promotes-main_reason
  fallback in add_job_preason (4e2ac86), but their whole trace/verify
  stack treats "detail-only => main_reason 0" as a contract --
  sched_trace_capture counts main_reason as its own occurrence
  ("Expected 2 Was 3" in test_trace_capture), the outcome renderer
  prints "main reason: none" for detail-only PENDs (test_repl_trace_e2e),
  and the committed .replay/.sim_verify baselines encode main_reason=0
  (test_repl_trace_baseline byte compare, test_trace_equivalence
  off-vs-traced). The fallback is dropped from the shared lib;
  the user-visible "overall reason" survives as an API-layer view:
  rawJobToModel (convert_pure.go) promotes the first detail row to
  main_reason_name when main_reason is 0 (main_reason VALUE marshals
  unchanged). add_job_preason keeps only the NULL-reasons guard
- bug-1498: sched_job_is_ready rejects non-PEND/suspended jobs
- bug-1490/1781: xce_jobctrl_t.action as capnp union discriminant
  (their jobCtrlUnionDiscriminant, same semantics)
- xcecli typed PendingReason field (53c00834), bug-1522 reconnect
  resend, xcecli_force_reconnect + their is_connection_broken both
  exported in client.h/libclient
- t1/t2/t3 test-node hostconfig in src/configs/hosts.yaml (bug-1494),
  /xce/v1/events + /xce/v1/apikeys whitelist routes (agent Phase 2)
- lib ASAN_OPTIONS=detect_leaks=0 test env + their oom-test block
- drop-audit: our bug-1504 fix covered by their bug-1680 (no action)

bug-924 follow-up (replaces the 67f3f6ad scratch-dir wipe): the
take-theirs audit's one "drop" was re-evaluated and REVERTED instead of
re-applied -- a global `find /tmp -name 'xce_*' -exec rm -rf` at suite
start/exit races the LIVE /tmp/xce_* fixtures of sibling suites under
`meson test -j` (harness_unit_test's clitest fixtures spawn xce_test as
a child mid-suite; the wipe destroyed the parent's fixtures ->
test_test_cli failures). Coverage instead:
- mkdtemp_clean shim gains mkdtemp_clean_mark/sweep_since
  (testsuite_support/mkdtemp_clean.[ch])
- new case_scratch_sweep.[ch]: aggregate runners (controller_test.c,
  harness_test.c -- hundreds of cases per process) call
  case_scratch_sweep_begin/_end_ok from setUp/tearDown; a PASSED case's
  dirs are swept immediately in tearDown (after engine reaping), FAILED
  cases keep their dirs for post-mortem; XCE_MKDTEMP_KEEP_ALL=1
  disables. One full controller_unit_test run was observed to leak 12G+
  to ENOSPC without this, cascading into unrelated "Expected Non-NULL"
  failures
- harness_unit_test_deps links the shim library as well

Test reconciliations on adopt:
- core-ready-gates: sched_queue_is_ready now gates only
  INACT_ADMIN|INACT_WINDOW (bug-1938/1960); CLOSED queues elect --
  covered end-to-end by test_queue_admin_control.c
- test_worker_resync: the 4 restart-replay RUN assertions predate the
  bug-1509 B3 marking (8858afed) landing on the same lineage; replayed
  RUN jobs now assert RUN|UNKWN|TRANSIENT (TWR_ASSERT_REPLAYED_RUN);
  terminal DONE/EXIT assertions unchanged
- convert_pure.go: our job-typed resolveExecUser deleted (superseded
  by their bug-1780/1936 two-arg version)
- test_preason: the 3 P0-promotion assertions reverted to the
  detail-keeps-main-0 contract; new tests cover limit-code detail
  rows and explicit-main-wins-over-later-details; apiserver
  convert_test.go gains the detail-only name-promotion cases

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-10-14 05:17:03 +00:00
commit 0c8607345e
663 changed files with 139848 additions and 13614 deletions

View file

@ -52,6 +52,20 @@ extern int xcecli_hello(xcecli_ctx_t, char*);
extern int xcecli_nop(xcecli_ctx_t);
extern int xcecli_heartbeat(xcecli_ctx_t, int);
extern int xcecli_force_reconnect(xcecli_ctx_t);
/*
* xcecli_is_connection_broken (bug-1760) - PURE, socket-free, side-effect-free
* query: has the transport been detected dead since the last successful
* (re)connect? Mirrors the private xcecli_ff_needs_reconnect() predicate
* (client_internal.h) exactly, exposed publicly so a consumer's own idle
* loop can passively check for a dead connection -- with ZERO network I/O
* and zero blocking risk -- instead of periodically probing a possibly-
* healthy one. The underlying signal (ctx->state == XCE_CTX_ERR) is set
* asynchronously by the transport's own RDHUP/EPOLLERR handler
* (handle_disconnect(), xcectx.c) the instant the connection drops, with no
* application call involved -- this function only ever reads that
* already-computed flag. Returns false for a NULL ctx.
*/
extern bool xcecli_is_connection_broken(xcecli_ctx_t);
extern int xcecli_shutdown(xcecli_ctx_t);
extern int xcecli_reslist(xcecli_ctx_t, xce_resource_t***);
extern int xcecli_join(xcecli_ctx_t, int, hostattrs_t*);
@ -95,6 +109,10 @@ extern int xcecli_resval(xcecli_ctx_t, char*, char*, xce_resval_t***);
extern int xcecli_update_jusage(xcecli_ctx_t, uint64_t, int, int*, int, int*,
int, xce_rusage_t**);
extern int xcecli_notify(xcecli_ctx_t, xce_notify_data_t*);
/* bug-1741: acknowledged notify -- retried (failover) until the controller
* confirms the fact committed; falls back to fire-and-forget against a
* controller that predates OPCode_notifyAcked. */
extern int xcecli_notify_acked(xcecli_ctx_t, xce_notify_data_t*);
extern int xcecli_jobctrl(xcecli_ctx_t, xce_jobctrl_t*);
extern int xcecli_notify_jobchkpt(xcecli_ctx_t, uint64_t, char*);
extern int xcecli_queuectrl(xcecli_ctx_t, xce_queuectrl_t*);

View file

@ -59,18 +59,12 @@ typedef uint64_t jobid_t;
#define SEQUENCE_HIGH_SHIFT (N_ARRAY_IDX_BITS)
#define N_MAX_FIELDS_JOBID (5)
#define UMAX_BITS(w) ((((uint64_t)0x01) << ((int)w)) - 1)
#define MAX_CLUSTER_ID (UMAX_BITS(N_CLUSTER_ID_BITS))
#define MAX_APISERVER_ID (UMAX_BITS(N_APISERVER_ID_BITS))
#define MAX_ARRAY_IDX (UMAX_BITS(N_ARRAY_IDX_BITS))
#define REGEX_JOBID "[ \t]*([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)[ \t]*"
#define REGEX_JOBARRAY_ID \
"[ \t]*([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)\\[([0-9]+)\\][ \t]*"
/* ======================================================
* Persistence & HA system constants (Part 2.6, Appendix A.10)
* ====================================================== */

View file

@ -9,6 +9,8 @@ typedef struct _data_item {
} data_item_t;
typedef int (*data_item_match_fn_t)(data_item_t*);
/* like data_item_match_fn_t, plus a caller-owned per-scan context */
typedef int (*data_item_match_ctx_fn_t)(data_item_t*, void*);
typedef void (*data_item_killer_t)(data_item_t*);
typedef struct {
@ -22,6 +24,10 @@ typedef struct {
extern int dataq_init(dataq_t*, data_item_killer_t);
extern data_item_t* dataq_deq(dataq_t*, data_item_match_fn_t);
/* bug-1864: first item matching f, with ctx passed to every f call of this
* one scan (under the queue lock) -- lets a matcher remember what it saw
* earlier in the same scan */
extern data_item_t* dataq_deq_ctx(dataq_t*, data_item_match_ctx_fn_t, void*);
extern int dataq_enq(dataq_t*, data_item_t*);
extern void dataq_fini(dataq_t*);

View file

@ -13,6 +13,7 @@ extern xce_group_t* makeGroup(var, xce_group_config_t*, char*);
extern int groupmgr_init(var*, int, int);
extern void groupmgr_fini(var);
extern int groupmgr_add(var, xce_group_t*);
extern int groupmgr_load_configs(var, int, xce_group_config_t**);
extern xce_group_t* groupmgr_lock(var, char*, int);
extern void groupmgr_del(var, xce_group_t*);
extern int groupmgr_iter(var, xlist_t*, void*);

View file

@ -17,7 +17,22 @@ typedef struct {
} idgen_ctx_t;
extern int idgen_ctx_init(idgen_ctx_t*);
extern int idgen(idgen_ctx_t*, size_t, size_t, size_t, uint64_t*);
extern void idgen_ctx_fini(idgen_ctx_t*);
/* Ordinary (non-array) job ID. arraySize must be 1; array jobs are
* allocated with idgen_array(). On success *pidx is an untagged ID and 1 is
* returned. -EINVAL for bad args or arraySize != 1; -EOVERFLOW if the
* sequence field is exhausted for the current second (the context is left
* unchanged). */
extern int idgen(idgen_ctx_t* ctx, size_t clusterId, size_t apiserverId,
size_t arraySize, uint64_t* pidx);
/* First element ID of a new n-element array, numbered from 1.
* 1 <= n <= MAX_ARRAY_IDX (32767). On success *first = MAKE_JOBID(c, a, ts,
* TAG_JOB_ARRAY, (block << 15) | 1), the context is advanced past the whole
* block and n is returned. -EINVAL for bad args or n; -EOVERFLOW if the next
* block would leave the sequence field (the context is left unchanged). */
extern int idgen_array(idgen_ctx_t* ctx, size_t clusterId, size_t apiserverId,
size_t n, uint64_t* first);
#endif

View file

@ -9,6 +9,7 @@
extern job_submit_data_t* makeJobsub(job_submit_data_t*);
extern job_startinfo_t* makeJobstartinfo(job_startinfo_t*);
extern job_t* makeJob(var, xce_job_t*);
extern unsigned long job_get_refcount(job_t*);
extern int job_obj_init(int, int);
extern void job_obj_fini();

View file

@ -141,8 +141,8 @@ typedef struct endpoint_t {
* via the vtask dispatcher, so the vtask_enq/-1 ref pair does not cover this
* call). A handler whose own logic drops ep's last reference (e.g. a stale
* vote reply driving raft_cb_become_follower -> endpoint_unref on the very ep
* currently being read) frees ep out from under the trailing pkt=NULL write
* below, a heap-use-after-free. Hold a ref for the duration of the call, same
* currently being read) frees ep out from under the macro's own trailing use
* of it, a heap-use-after-free. Hold a ref for the duration of the call, same
* pattern as the vtask dispatch path, so the handler's own unref cannot drop
* the count to zero until this macro is done touching ep. */
#define INVOKE_MSG_HANDLER(ep) \
@ -150,9 +150,12 @@ typedef struct endpoint_t {
TRACE("%s msg handler %p", __func__, (ep)->msg_handler); \
if ((ep)->msg_handler != NULL) { \
endpoint_ref(ep); \
/* A handler that takes the packet clears ep->fsm_in.pkt itself \
* (vtask_handle_msg, peer_ctrl_msg_handler). One that refuses it \
* (early -EINVAL/-ENOMEM) leaves it endpoint-owned: the next \
* message reuses it and endpoint_fini frees it. Overwriting the \
* slot here leaked every refused packet (bug-1918). */ \
(ep)->msg_handler(ep); \
/* passing ownership of packet to upper layer */ \
(ep)->fsm_in.pkt = NULL; \
endpoint_unref(ep); \
} \
} while (0)
@ -167,6 +170,10 @@ typedef struct endpoint_t {
typedef struct {
endpoint_t* ep;
io_packet_t* pkt;
/* ep->reconnect_gen when the message was received (bug-1917): a task of
* a connection that has since been re-established is stale and is
* dropped instead of being delivered to the new session. */
uint64_t gen;
} io_task_data_t;
extern int alloc_packet(io_packet_t*);

View file

@ -340,6 +340,15 @@ struct persist_s {
* before the node participates; read by the promotion gate thereafter. */
recover_verdict_t recovery_verdict;
/* Set once a RECOVER_TRUNCATED member has caught up from the live primary
* past the cluster HWM its promotion fence was armed with (bug-1919):
* entries replicated on top of the truncated log prove the lost tail was
* refilled. recovery_verdict keeps the raw boot verdict for diagnostics;
* persist_recovery_truncated() reports false once this is set. Reset by
* persist_recover. Set by the fence re-check timer, read by the
* promotion gate. */
_Atomic bool recovery_truncation_healed;
/* Sticky "unrecoverable, loudly fenced" flag (ha-commit-durability Req 2.4,
* NEVER-SELF-EXIT rework 2026-06-18). Set by
* persist_recover_loud_fence_if_unrecoverable when a member is
@ -789,11 +798,14 @@ uint64_t persist_durable_floor(persist_t* p);
* persist_recovery_verdict returns the RECOVER_CLEAN / RECOVER_TRUNCATED
* verdict computed by the last persist_recover (RECOVER_CLEAN on a fresh
* store before recovery runs). persist_recovery_truncated is the convenience
* flag form (true iff the verdict is RECOVER_TRUNCATED) the promotion gate
* (task 4.3) consumes to mark the member non-promotable. Both are pure reads
* of recovery state established single-threaded at startup. */
* flag form (true iff the verdict is RECOVER_TRUNCATED and the truncation has
* not since been healed by catch-up) the promotion gate (task 4.3) consumes
* to mark the member non-promotable. persist_recovery_verdict is a pure read
* of the boot-time verdict. persist_mark_recovery_truncation_healed records
* that catch-up from the primary refilled the truncated tail (bug-1919). */
recover_verdict_t persist_recovery_verdict(persist_t* p);
bool persist_recovery_truncated(persist_t* p);
void persist_mark_recovery_truncation_healed(persist_t* p);
/* Bounded catch-up RE-ALERT window (ha-commit-durability task 3.3, Req 2.4;
* never-self-exit rework 2026-06-18).
@ -1022,6 +1034,21 @@ uint64_t persist_test_durable_lsn_on_disk(persist_t* p);
* Compiled out of production (xce_controller links without it). */
void persist_test_set_force_fsync_fail(persist_t* p, int fail);
/* bug-1429: run one flusher pass (the fsync_worker's flush step) and report
* whether THAT pass advanced durable_lsn -- the worker fires the
* durable-advance callback only on its own advance, never on one a
* concurrent persist_flush() made. Returns the durable watermark. */
uint64_t persist_test_flusher_flush_once(persist_t* p, bool* advanced);
/* bug-1429: persist_stop() every instance persist_start()ed but never
* stopped -- a failed TEST_ASSERT skips the fixture teardown, and the next
* test re-inits the same static persist under its live fsync_worker
* (corrupting fsync_cond; persist_fini then hangs). Called from the test
* runner's tearDown() before commit_test_reap_unfinalized_engines(), the
* documented persist_stop-before-commit_engine_fini order. Stack-local
* instances are skipped. Returns the number stopped. */
int persist_test_reap_unstopped(void);
/* Read the standby-path fdatasync counter (task 2.3, persist-before-ACK
* regression). Counts the inline fdatasync calls made by
* persist_wal_write_replicated. A test snapshots this before the call and
@ -1077,6 +1104,12 @@ extern void (*persist_checkpoint_test_hook)(persist_t*);
/* Checkpoint */
int persist_checkpoint(persist_t* p);
#ifdef XCE_TESTING
/* bug-1870: test-only -- write a pre-fix WAL_CHECKPOINT marker (consumes an
* LSN) to reproduce a WAL written by an older binary. */
int persist_test_write_legacy_checkpoint_marker(persist_t* p);
#endif
/*
* persist_set_checkpoint_offload_for_test / persist_checkpoint_offload_enabled:
* Feature-flag accessors (xce-audit-invariants controller pass, dimension 1
@ -1508,6 +1541,19 @@ int persist_submit_job_start_fact(persist_t* p, uint64_t jobid, int state,
int persist_submit_job_finish_fact(persist_t* p, uint64_t jobid, int state,
uint64_t finish_time, int exit_code);
/* bug-1754/bug-1747: WAL_JOB_FAIL's deferred-apply sibling of
* persist_submit_job_fact_ex() -- header {jobid, 0, WAL_JOB_FAIL_STAGE_FLAG
* | stage} followed by a fixed int32_t code (job_failinfo_t.code) and a
* fixed uint64_t finish_time (seconds). A worker reports every nonzero job
* exit as a FAIL, so for a MAIN-stage failure `code` is the job's real exit
* code; apply_job_fail_exit_code() (job.c) adopts it into job.exit_info and
* both apply seams adopt finish_time instead of apply-time time(NULL).
* Older payloads decode by length: exactly sizeof(wal_jobstate_t) (before
* bug-1754) carries neither; header + code (bug-1754) carries no time.
* Returns the local-append rc, same contract as persist_submit_job_fact(). */
int persist_submit_job_fail_fact(persist_t* p, uint64_t jobid, uint32_t stage,
int code, uint64_t finish_time);
/* bug-1679: fire XCE_CONTROLLER_HOOK_JOB's HOOK_DETAIL_JOB_UPDATE AFTER_EVENT
* for a job whose durable WAL fact the caller has ALREADY appended itself
* (e.g. dispatch_a_job_()'s own persist_submit_job_dispatch_fact() call,
@ -1527,6 +1573,30 @@ int persist_submit_job_finish_fact(persist_t* p, uint64_t jobid, int state,
* function is job.c's only sanctioned way to reach it. */
void persist_notify_job_updated_already_persisted(cluster_t* c, job_t* j);
/* bug-1841 (worker-side sibling of bug-1679/persist_notify_job_updated_
* already_persisted above): fires XCE_CONTROLLER_HOOK_WORKER's
* HOOK_DETAIL_WORKER_UPDATE AFTER_EVENT for `w`, guarded by the SAME
* t_apply_seam_notifying flag so persist_worker_hook_fun (also subscribed
* to this hook) does NOT append a fresh WAL_WORKER_CTRL fact for this fire.
* Unlike the job version, nothing here was "already persisted" -- this is
* for a worker mutation that is intentionally never durable at all (e.g.
* resync.c's XCE_WORKER_RECONCILING coordination bit, which is armed and
* cleared per rejoin and re-derived fresh every time, never replayed from
* WAL) but still needs HOOK_DETAIL_WORKER_UPDATE's live subscribers
* (sched_worker_change/apiserver_worker_change) to see `w`'s current
* status. Without this, a subscriber's only view of `w->host.status` is
* whatever the last WORKERS_PUSH-triggering hook fire carried -- resync.c's
* own resync_resolve_round() clearing the bit in memory is invisible to
* them until some unrelated worker event happens to re-fire the hook. */
void persist_notify_worker_status_updated(cluster_t* c, worker_t* w);
/* bug-1469: fire HOOK_DETAIL_QUEUE_UPDATE AFTER_EVENT for `q` under the same
* t_apply_seam_notifying guard, so the scheduler/apiserver learn about a
* queue created by config activation while persist_queue_hook_fun appends
* no WAL_QUEUE_CTRL fact -- config queues are config-owned and built on
* every member from the committed config, never replayed from WAL. */
void persist_notify_queue_updated_no_wal(cluster_t* c, queue_t* q);
int persist_worker_hook_fun(int type, int situation, int detail,
void* cls, va_list ap);
@ -1637,3 +1707,11 @@ int persist_load_snapshot(persist_t* p, const char* path, uint64_t lsn,
bool job_is_terminal(const job_t* j);
bool job_should_retain(const job_t* j, const persist_config_t* cfg);
bool worker_should_retain(const xce_worker_t* w, const persist_config_t* cfg);
/* bug-1877: is the WAL's storage encrypted? persist_dev_is_dmcrypt() walks
* <sysfs>/dev/block/<maj>:<min> (and its `slaves`, for LVM on LUKS) for a
* dm-crypt layer (dm/uuid "CRYPT-..."); persist_path_encrypted() also
* accepts an fscrypt-encrypted directory (statx STATX_ATTR_ENCRYPTED).
* Both return 1 when encrypted, 0 when not shown to be. */
int persist_dev_is_dmcrypt(const char* sysfs, unsigned maj, unsigned min);
int persist_path_encrypted(const char* path);

View file

@ -21,4 +21,84 @@ extern void erase_job_preason(xce_job_preason_t*);
extern int add_job_preason(xce_job_preason_t*, int, char*, char*);
extern xce_pendreason_t* make_pendreason(uint64_t);
/*
* De-duplicating pend-reason builder (bug-1927).
*
* add_job_preason() always appends, which is right for callers that build a
* reason list entry by entry (tests, the harness setpend command). A caller
* that re-evaluates the same hosts many times while building one job's
* reasons -- the scheduler's allocator, once per placement plan -- uses
* add_job_preason_once() instead, so each distinct (host, code) host reason
* and each distinct (host, resource, code) resource reason appears once.
* Entries keep the order of their first add. A NULL resource-reason host is
* stored as "" (as add_job_preason() does) and is the same key as "".
*
* xce_job_preason_t is the wire-mapped record (controller.capnp
* JobPendReason), so the lookup index lives beside it, owned by the
* builder: zero-initialize it, pass the same index with the same reasons
* for every add, and release it with erase_preason_index(). The index only
* points at list positions and checks the entry there on every hit, so an
* erase of the lists (a main reason, erase_job_preason()) or plain
* add_job_preason() calls in between leave it correct, never stale: an
* erased entry is simply added again. Expected O(1) per add.
*
* host == NULL && res == NULL sets the main reason exactly like
* add_job_preason() (erasing both lists) and returns its result.
*
* Returns the list's new entry count (> 0) when an entry was appended, 0
* when an identical entry is already present (or a main reason was set),
* -EINVAL for a NULL reasons or index, or -ENOMEM (nothing appended).
*/
typedef struct {
int nslots; /* power of two; 0 until the first add */
int nused;
uint32_t* hashes;
int* refs; /* 0 empty, else 1 + (list position << 1 | is_res) */
} xce_preason_index_t;
extern int add_job_preason_once(xce_job_preason_t*, xce_preason_index_t*, int,
char*, char*);
extern void erase_preason_index(xce_preason_index_t*);
/*
* Pending-reason equality: "are these two pending-reason records identical".
*
* job_preason_eq() is a FULL structural match. It compares, in this order:
* main_reason; nhost_reasons, then host_based_reasons[i] in order, each by
* host then reason; nres_reasons, then res_based_reasons[i] in order, each
* by resource, host, then reason. Nothing is excluded. Lists are
* order-sensitive: the same entries in a different order are not equal.
*
* The scheduler simulator (--sim-verify, sim_verify.c) deliberately does NOT
* use this function for its PEND outcome check: it compares main_reason only
* (bug-1845), because a recorded PEND and its replay can legitimately carry
* different detail (an alloc-cache hit recorded before bug-1932 carries only
* the main reason). That loosening is a replay property, not object
* identity, so it stays simulator-local.
*
* NULL handling: identical pointers (including two NULLs) are equal; exactly
* one NULL argument, list entry or string field is not equal. Precondition:
* a non-zero count with a NULL entry array is a malformed record and is not
* handled. Neither argument is modified.
*
* `why` is optional. When NULL, nothing is allocated. Otherwise *why is set
* to NULL when the arguments are equal, or to a newly allocated,
* NUL-terminated description of the first difference found, in comparison
* order, e.g.
* main_reason: 5 != 6
* nhost_reasons: 2 != 1
* host_based_reasons[1](host=h2).host: "h2" != "h9"
* res_based_reasons[0](resource=mem).reason: 3 != 8
* res_based_reasons[0]: non-NULL != NULL
* job_preason: NULL != non-NULL (a NULL top-level argument)
* Path labels come from `a`, from `b` when a's string is NULL, or are `?`.
* The caller owns the description and releases it with free(). The value
* *why holds on entry is never read or freed. If allocating the description
* fails, *why is NULL and the result is unchanged. Nothing is logged.
*
* Returns non-zero when equal, 0 when not.
*/
extern int job_preason_eq(const xce_job_preason_t* a,
const xce_job_preason_t* b, char** why);
#endif

View file

@ -27,12 +27,15 @@ extern int xce_reply(endpoint_t*, xce_peerinfo_t*, xce_reply_t*);
extern int xce_hello_req(endpoint_t*, char*);
extern int xce_nop_req(endpoint_t*);
/* HA heartbeat: an OP_nop request carrying a HeartbeatMsg body with the
* primary's committed position. Non-blocking, no reply awaited (the standby's
* StateReport rides the normal nop reply). senderPriority is unused (0).
* Generic xce_nop_req is left unchanged (body-less). */
* primary's committed position and its own election time (Req 12.2 — echoed
* to standbys so they can learn the currently-known primary's election
* time). Non-blocking, no reply awaited (the standby's StateReport rides the
* normal nop reply). senderPriority is unused (0). Generic xce_nop_req is
* left unchanged (body-less). */
extern int xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term,
uint64_t committed_lsn,
uint64_t committed_config_tag);
uint64_t committed_config_tag,
uint64_t elected_at_ms);
extern int xce_shutdown_req(endpoint_t*);
extern void init_reply_by_req(xce_reply_t*, xce_request_t*);
extern int xce_join_req(endpoint_t*, int, hostattrs_t*);
@ -86,6 +89,8 @@ extern int xce_job_state_report_req(endpoint_t*, xce_peerinfo_t*,
xce_check_fn_t, void*, int);
extern int xce_notify_req(endpoint_t*, xce_notify_data_t*);
/* bug-1741: OPCode_notifyAcked -- answered once the fact commits. */
extern int xce_notify_acked_req(endpoint_t*, xce_notify_data_t*);
extern int extract_data(dataset_t*, xce_data_t*);
extern int xce_update_req(endpoint_t*, xce_update_data_t*);
extern int xce_jobctrl_req(endpoint_t*, xce_jobctrl_t*);

View file

@ -8,4 +8,14 @@
extern xce_resource_t* makeResource(var, xce_resource_t*);
/* bug-1854: register a pushed resource definition, tolerating re-pushes.
* Returns RES_APPLY_ADDED (newly registered), RES_APPLY_SAME (already
* registered with the same scheduling-relevant definition: valtype,
* ordtype, flags, interval), RES_APPLY_CHANGED (registered with a different
* definition -- NOT applied; the registered one is kept), or -errno. */
#define RES_APPLY_ADDED 1
#define RES_APPLY_SAME 0
#define RES_APPLY_CHANGED 2
extern int resourceApplyDef(var, xce_resource_t*);
#endif

View file

@ -191,6 +191,7 @@ struct select_context_t_ {
int err_pos;
int has_error;
char errmsg[256]; /* fixed-size — no alloc/free in grammar actions */
int unknown_resource; /* bug-1887: the error was an unknown resource name */
int depth; /* current nesting depth — guard vs SELECT_MAX_DEPTH */
xce_select_node_t* root; /* result root set by grammar top-level action */
};
@ -240,6 +241,12 @@ typedef struct {
int maxTasks;
xce_select_t* select;
xlist_t rusages;
/* Req 7.1 (scheduler-allocation spec): stable resource-usage
* fingerprint over `rusages`, set once by parse_resreq_primary() and
* stable for the life of this resreq -- every later cache lookup for
* the job this resreq belongs to reuses this value instead of
* recomputing it. See resreq.c's hash_rusages(). */
uint64_t rusage_hash;
xce_span_t* span;
xce_same_t* same;
xce_affinity_t* affinity;
@ -299,6 +306,12 @@ extern void kill_section_ctx(section_context_t*);
extern void kill_resreq_data(xce_resreq_data_t*);
extern int parse_section(xce_resreq_data_t*, char*, char**);
extern int parse_rusage(xlist_t*, char*, char**);
/* bug-1744: syntax-check every rusage[...] clause of a resource-request
* string with the real rusage grammar -- no resource manager needed, for
* front ends validating a submission before accepting it. Returns 1 if all
* clauses parse (or there are none), else -EINVAL with *errmsg set (caller
* frees). */
extern int xce_check_rusage_syntax(const char*, char**);
extern void kill_rusage_value(rusage_value_t*);
extern int parse_resreq_primary(xce_primary_resreq_t*, char*, var, char**);
extern void kill_primary_resreq(xce_primary_resreq_t*);
@ -309,5 +322,19 @@ extern int calc_wanted_global_resvals(xvec_idval_t*, var, xlist_t*,
rusage_per_type_t, int, int);
extern int calc_wanted_host_resvals(xvec_idval_t*, var, xlist_t*,
rusage_per_type_t, int, int);
extern int calc_wanted_job_total_resval(xlist_t*, rusage_per_type_t, int,
int, const char*, float*);
/*
* Overwrite minTasks/maxTasks on resreq's own top-level primary with the
* job's real requested task count. No-op if resreq is NULL or if its
* top-level tag is not XCE_RESREQ_PRIMARY -- a COMPOUND or ALTER resreq's
* nested primaries are left exactly as parse_resreq_primary() set them.
* If mintasks > maxtasks, maxtasks is defensively raised to mintasks
* before assignment (mirrors the apiserver's own convert.go clamp
* direction). Idempotent; never allocates memory.
*/
extern void apply_job_task_count(xce_resreq_t* resreq, uint32_t mintasks,
uint32_t maxtasks);
#endif

View file

@ -264,6 +264,7 @@ typedef struct {
char* name;
char* host;
uint32_t port;
uint8_t status; /* @3: 0=unknown 1=ok 2=catching-up 3=dead (Req 13.1) */
} xce_ctrlpeer_t;
typedef struct {
@ -284,6 +285,8 @@ typedef struct {
} peers;
uint32_t peerCount;
uint64_t responder_time_ms; /* @13: responder wall-clock at sample time (epoch ms) */
uint64_t elected_at_ms; /* @14: current primary's election time (epoch ms),
* learned off-primary via heartbeat; 0 = unknown */
} xce_ctrlstatus_t;
/* ── Worker rejoin/resync job-state reconciliation (worker-rejoin-resync
@ -613,14 +616,19 @@ struct heartbeat_msg_s {
uint64_t term; /* sender's current term */
uint64_t committed_lsn; /* Req 5: primary's committed WAL LSN */
uint64_t committed_config_tag; /* Req 8: primary's committed config tag */
uint64_t elected_at_ms; /* Req 12.2: primary's own last_known_elected_at_ms,
* echoed on every heartbeat */
};
/* CtrlHello state sync (Req 6.5, 6.6, 6.7) */
struct ctrl_hello_req_s {
char *name; /* standby's logical name */
xce_data_t *key; /* SHA-256(cluster_name + shared_secret) auth key */
uint8_t *key; /* SHA-256(cluster_name + shared_secret) auth key,
* malloc'd, or NULL; wire type is List(UInt8),
* not Data (see controller.capnp's own comment) */
uint64_t applied_lsn; /* standby's last applied WAL LSN */
uint64_t config_tag; /* standby's current config tag */
uint32_t key_len; /* length of key */
};
struct ctrl_hello_reply_s {

View file

@ -6,6 +6,7 @@
#include <capnp_c.h>
#include <math.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <tlog.h>
@ -56,6 +57,11 @@
#define float_equal(a, b) (fabs((a) - (b)) < float_near_zero)
/* NULL-safe string equality: non-zero iff a and b are both NULL, or both
* non-NULL and strcmp(a, b) == 0; zero otherwise (exactly one NULL, or
* different contents). Reads only; never modifies either argument. */
extern int str_eq(const char* a, const char* b);
#define STR(x) _STR(x)
#define _STR(x) #x
@ -141,6 +147,12 @@
#define TRACE(...) tlog_debug3(__VA_ARGS__)
/* Named DBG, not DEBUG -- DEBUG is a bare command-line macro (-DDEBUG,
* src/meson.build) defined for both the debug and asan buildtypes, so
* `#define DEBUG(...)` here would trigger a "DEBUG redefined" warning in
* every translation unit that includes this header under either profile. */
#define DBG(...) tlog_debug(__VA_ARGS__)
#define INFO(...) tlog_info(__VA_ARGS__)
#define ERROR(...) tlog_error(__VA_ARGS__)
@ -206,6 +218,110 @@ extern int fmt_jobid(char*, size_t, uint64_t);
extern int parse_jobid(uint64_t*, char*);
extern uint64_t array_jobid(uint64_t);
extern uint64_t array_idx(uint64_t);
/*
* Bounded job-identity / element-selection parser (job-array-one-based-index,
* Req 1.5, 5.7, 5.8, 5.12, 5.14, 5.15, 8.1).
*
* Grammar: optional leading/trailing spaces or tabs, then
* `num-num-num-num` (cluster-apiserver-timestamp-sequence), optionally
* followed by `[num]` or `[num-num]`. `num` is one or more decimal digits
* (leading zeros allowed, no sign, no inner whitespace).
*
* Checks, in this order; only the first failure is reported:
* JOBSEL_ESYNTAX grammar (5.15)
* JOBSEL_EWIDTH a field wider than its bit width (5.14)
* JOBSEL_EALIGN element part present but sequence % 32768 != 0 (5.8)
* JOBSEL_EINDEX an element number above JOBID_FIELD_MAX_INDEX (5.7)
* JOBSEL_EREVERSED range whose last number is below its first (5.5)
* Digits are accumulated with an overflow guard, so oversized numbers fail
* the width or index check instead of wrapping.
*
* On JOBSEL_OK: BARE -> `key` is the exact untagged ID, lo == hi == 0;
* SINGLE -> `key` is the array ID (tag 0, low 15 bits clear), lo == hi == k;
* RANGE -> `key` is the array ID, [lo, hi]. A NULL `text` is
* JOBSEL_ESYNTAX; `out` must not be NULL.
*/
#define JOBID_FIELD_MAX_CLUSTER MAX_CLUSTER_ID
#define JOBID_FIELD_MAX_APISERVER MAX_APISERVER_ID
#define JOBID_FIELD_MAX_TIMESTAMP (UMAX_BITS(N_TIMESTAMP_BITS))
#define JOBID_FIELD_MAX_SEQUENCE (UMAX_BITS(N_SEQUENCE_BITS))
#define JOBID_FIELD_MAX_INDEX MAX_ARRAY_IDX
typedef enum {
JOBSEL_OK = 0,
JOBSEL_ESYNTAX, /* 5.15 */
JOBSEL_EWIDTH, /* 5.14 */
JOBSEL_EALIGN, /* 5.8 */
JOBSEL_EINDEX, /* 5.7 */
JOBSEL_EREVERSED /* 5.5 */
} jobsel_err_t;
typedef enum {
JOBSEL_BARE = 0,
JOBSEL_SINGLE,
JOBSEL_RANGE
} jobsel_form_t;
typedef struct {
uint64_t key; /* BARE: the exact (untagged) ID; else the array ID */
jobsel_form_t form;
uint32_t lo; /* SINGLE: lo == hi == k */
uint32_t hi;
} jobsel_t;
extern jobsel_err_t parse_jobsel(const char* text, jobsel_t* out);
extern const char* jobsel_strerror(jobsel_err_t err);
/* Non-zero iff the ID's tag equals TAG_JOB_ARRAY (tags 2 and 3 are not). */
extern int is_array_tag(uint64_t jobid);
/* Element ID of number k (masked to 15 bits) in the array `s->key`; 0 if
* `s` is NULL. */
extern uint64_t jobsel_element_id(const jobsel_t* s, uint32_t k);
/*
* xce_units_mode_t / xce_format_bytes_human() / xce_is_bytes_valued_resource()
* / xce_format_units_value() -- scheduler-sim-diagnostics spec, task 7 (Req
* 6.1, 6.3, 6.5). Shared-library promotion of the scheduler REPL's
* byte-unit scaling algorithm (formerly repl_fmt.c's
* repl_format_bytes_human()), its byte-valued-resource classification
* (formerly repl_fmt.c's repl_is_bytes_valued_resource()), and a
* display-mode-aware value composer (formerly repl_info.c's
* repl_format_numeric_resource_value()'s byte-valued branch), so any
* `libxce` consumer -- not only the scheduler's REPL -- can reach this
* display-only formatting. `fmt_jobid()`/`fmt_jobid_ex()` immediately above
* are the precedent this follows: a small, generic, cross-component
* display-formatting helper with no domain coupling.
*
* All three functions are pure and display-only -- none of them ever
* mutate a caller-supplied value or feed back into a stored/wire value.
* `xce_is_bytes_valued_resource()` is a fixed 5-name list (`mem`, `tmp`,
* `maxmem`, `maxtmp`, `maxswap`); a `NULL` name returns `false`.
* `xce_format_bytes_human()` adaptively scales `bytes` to the smallest
* B/KB/MB/GB/TB/PB unit it fits, writing e.g. "20.56 GB" into `buf`.
* `xce_format_units_value()` assumes the caller has already decided
* `bytes_value` is byte-valued (via `xce_is_bytes_valued_resource()` or the
* caller's own equivalent check) -- it does not re-derive that decision:
* RAW renders the bare "%f" of `bytes_value` alone, HUMAN renders
* `xce_format_bytes_human()`'s scaled form alone, and BOTH (the default for
* any other mode value) renders "<raw> (<human>)".
*
* `xce_repl_units_mode_t` (scheduler-private, `repl_internal.h`) is a
* deliberately separate, independently-defined enum with the same three
* semantic values -- a `src/lib` type must not be defined in terms of a
* scheduler-internal concept. The scheduler bridges the two at its one call
* site (`repl_info.c`'s `translate_units_mode()`).
*/
typedef enum {
XCE_UNITS_RAW = 1,
XCE_UNITS_HUMAN,
XCE_UNITS_BOTH
} xce_units_mode_t;
extern void xce_format_bytes_human(double bytes, char* buf, size_t bufsz);
extern bool xce_is_bytes_valued_resource(const char* name);
extern void xce_format_units_value(xce_units_mode_t mode, double bytes_value,
char* buf, size_t bufsz);
extern void unblock_sig(int);
extern void block_sig(int);

View file

@ -51,12 +51,14 @@ typedef struct {
/* raft-drive-async task 3.1: when non-zero AND data->vtype == VTASK_IO, the
* carried io_packet_t (data->u.iodata.pkt) is OWNED by the task and must be
* freed by taskq_item_killer if the task is destroyed while still queued
* (never executed) at vtaskmgr_fini. Default 0 preserves the legacy
* endpoint-owned-packet model used by the libxce client path (xcectx.c's
* vtask_handle_msg does NOT clear ep->fsm_in.pkt, so the packet lifetime is
* tied to the endpoint, not the task — freeing it here would double-free).
* The controller's peer-control dispatch sets this to 1 because it clears
* ep->fsm_in.pkt and the task becomes the sole owner. */
* (never executed) at vtaskmgr_fini. Both IO handoffs set it to 1 because
* both clear ep->fsm_in.pkt and the task becomes the sole owner: the
* controller's peer-control dispatch (ha.c) and, since bug-1916,
* vtask_handle_msg (libxce client xcectx.c + controller cluster.c), which
* used to leave ep->fsm_in.pkt naming a packet the task's xce_decode freed
* until INVOKE_MSG_HANDLER cleared it after the handler returned — a window
* in which endpoint_fini double-freed it. 0 only for tasks that do not
* own a packet. */
int owns_io_pkt;
} vtask_t;
@ -99,7 +101,17 @@ extern int vtask_init(vtask_t*, vtask_func_t, void*);
extern void vtask_fini(vtask_t*);
extern int vtask_enq(vtaskmgr_t*, vtask_t*);
/* bug-1864: dequeue the first dispatchable task from a vtaskmgr task queue.
* is_blocked reports whether an endpoint already has a task running
* (production: endpoint_is_task_blocked); it is a parameter so a test can
* make an endpoint unblock part-way through one scan. */
extern vtask_t* vtask_deq_eligible(dataq_t* taskq,
int (*is_blocked)(endpoint_t*));
extern int vtask_handle_msg(vtaskmgr_t*, vtask_func_t, void* arg);
/* bug-1917: the endpoint whose VTASK_IO task the calling thread is running,
* or NULL when it is not running one. */
extern endpoint_t* vtask_current_io_ep(void);
/* LivenessAdmission (raft-drive-async task 1.6) — pool-level reservation.
*

View file

@ -44,9 +44,12 @@ typedef struct {
* as-is". A nonzero _pad's exact interpretation is per-type — see the two
* markers below. */
#define WAL_JOB_FAIL_STAGE_FLAG 0x80000000u
/* WAL_JOB_FAIL: _pad = WAL_JOB_FAIL_STAGE_FLAG | job_failinfo_t.stage.
* Apply time reconstructs a minimal job_failinfo_t from {jobid, stage}
* and calls the real apply_job_fail_transition() against current state. */
/* WAL_JOB_FAIL: _pad = WAL_JOB_FAIL_STAGE_FLAG | job_failinfo_t.stage,
* optionally followed by an int32_t job_failinfo_t.code (bug-1754) and a
* uint64_t finish_time in seconds (bug-1747). Apply time reconstructs a
* minimal job_failinfo_t from {jobid, stage, code} and calls the real
* apply_job_fail_transition() against current state, then
* apply_job_fail_exit_code() and adopts finish_time when present. */
#define WAL_JOB_UPDATE_POSTDONE_MARK 1u
/* WAL_JOB_UPDATE: _pad = WAL_JOB_UPDATE_POSTDONE_MARK marks a postdone
* merge-fact (`state` is unused/0) — apply time calls

View file

@ -18,10 +18,13 @@ typedef enum {
(DUP on a re-sent write) */
XCE_FO_RETRY_NOW, /* E_XCE_REDIRECT / E_XCE_STANDBY / transport error
— rotate, no backoff */
XCE_FO_RETRY_BACKOFF, /* E_XCE_NOMASTER / E_XCE_NOT_READY — rotate after
jittered backoff */
XCE_FO_TERMINAL /* AUTH/PERM/INVALID/NOENT and other non-retriable
XCE_FO_RETRY_BACKOFF, /* E_XCE_NOMASTER — rotate after jittered backoff */
XCE_FO_TERMINAL, /* AUTH/PERM/INVALID/NOENT and other non-retriable
— surface to caller */
XCE_FO_RETRY_SAME /* E_XCE_NOT_READY (bug-1850) — the controller
answered but is briefly not ready: jittered
backoff, then retry the SAME controller (no
rotate/reconnect/rejoin) */
} xce_fo_action_t;
/*

View file

@ -129,7 +129,10 @@ void xce_reconnect_fini(xce_reconnect_ctx_t* rctx);
/**
* xce_parse_peers() — parse "--peers name=host:port,..." into cfg->candidates[].
*
* Each token may be "name=host:port" or bare "host:port".
* Each token may be "name=host:port" or bare "host:port". Whitespace around
* each comma-separated token is ignored and tokens that are blank after
* trimming are skipped (they do not use a MAX_HA_CANDIDATES slot), matching
* xceadmin's resolve_controllers() (bug-1912).
* The name field is stored when present; it allows multiple daemons on the
* same host to identify themselves by logical name rather than IP address.
*
@ -137,6 +140,32 @@ void xce_reconnect_fini(xce_reconnect_ctx_t* rctx);
*/
int xce_parse_peers(const char* peers_str, xce_cluster_config_t* cfg);
/**
* xce_controller_peers_env() — default controller candidate list from the
* XCE_CONTROLLER_PEERS environment variable.
*
* Returns the variable's value unchanged (same format as --peers, not parsed
* here) when it is set and non-empty; NULL when it is unset or empty, so an
* empty value behaves exactly like an unset one. The returned pointer is
* owned by the process environment; do not free or modify it.
*/
const char* xce_controller_peers_env(void);
/**
* xce_peers_find_identity() — the one "which candidate is me" rule.
*
* A candidate's key is its name when non-empty, otherwise its hostname
* (a named candidate is never matched by its hostname). The first
* candidate whose key matches name (compared over XCE_MAXNAME_LEN - 1
* characters) wins. An empty name is compared like any other, so it
* matches an unnamed candidate with an empty hostname.
*
* Returns the matching candidate's index, or -1 when cfg or name is NULL
* or nothing matches. No default is applied; each caller keeps its own
* no-match handling.
*/
int xce_peers_find_identity(const xce_cluster_config_t* cfg, const char* name);
/* Test seam: set to non-NULL to override the real connect (unit tests only) */
typedef void* (*xce_connect_fn_t)(const char* host, int port);
void xce_cluster_set_connect_fn(xce_connect_fn_t fn);

View file

@ -0,0 +1,61 @@
#ifndef _CTRLSTATUS_FMT_H_
#define _CTRLSTATUS_FMT_H_
/* ctrlstatus_fmt.h — render a responder snapshot timestamp (epoch-millis) in
* the EXACT wall-clock string the controller logger emits, so the test client's
* `ctrlstatus` output is byte-comparable against ctrl-N.log (controller-status
* task 6.3, Req 11.2/11.3).
*
* The tlog prefix (vendored tinylog, _tlog_root_default_format) is built inline
* as:
*
* snprintf(... "[%.4d-%.2d-%.2d %.2d:%.2d:%.2d,%.3d]...",
* tm->year, tm->mon, tm->mday, tm->hour, tm->min, tm->sec,
* tm->usec / 1000, ...);
*
* where the broken-down time comes from gettimeofday() + localtime_r() — i.e.
* LOCAL time, with sub-second precision rendered as zero-padded MILLISECONDS
* (usec / 1000). tlog exposes no reusable formatter for callers, so this helper
* REPLICATES that construction: localtime_r + strftime("%Y-%m-%d %H:%M:%S")
* (byte-identical to "%.4d-%.2d-%.2d %.2d:%.2d:%.2d") then ",%03u" for the
* millisecond fraction. Both the logger and this helper use local time, so the
* rendered value matches the log line under the same TZ. */
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
/* Render `ms` (epoch milliseconds, e.g. xce_ctrlstatus_t.responder_time_ms)
* into `buf` as local-time "YYYY-MM-DD HH:MM:SS,mmm". Needs at least 24 bytes.
* Returns `buf` on success, NULL if buf is NULL / too small / time conversion
* fails. */
static inline char* ctrlstatus_format_time_ms(uint64_t ms, char* buf, size_t n)
{
/* "YYYY-MM-DD HH:MM:SS,mmm" + NUL == 24 bytes. */
if (buf == NULL || n < 24) {
return NULL;
}
time_t secs = (time_t)(ms / 1000u);
unsigned msec = (unsigned)(ms % 1000u);
struct tm tm;
if (localtime_r(&secs, &tm) == NULL) {
return NULL;
}
char datetime[20]; /* "YYYY-MM-DD HH:MM:SS" + NUL */
if (strftime(datetime, sizeof(datetime), "%Y-%m-%d %H:%M:%S", &tm) == 0) {
return NULL;
}
/* Match tlog's ",%.3d" millisecond fraction (zero-padded to 3 digits). */
if (snprintf(buf, n, "%s,%03u", datetime, msec) < 0) {
return NULL;
}
return buf;
}
#endif /* _CTRLSTATUS_FMT_H_ */

View file

@ -23,6 +23,9 @@ enum {
E_XCE_REDIRECT, /* Standby is redirecting client to primary */
E_XCE_NOT_READY, /* Controller in CTRL_INIT or CTRL_ELECTING state */
E_XCE_STANDBY, /* Controller in CTRL_STANDBY; op is primary-only */
E_XCE_ARRAY_START0, /* Array submission numbered from 0; numbering starts
* at 1, the submitting client must be upgraded */
E_XCE_QUEUE_CLOSED, /* Submission to a closed queue (bug-1960) */
E_XCE_MAX
};

View file

@ -451,6 +451,10 @@ typedef struct {
double mem_usage;
double mem_swap;
int tasks_current;
/* bug-1736: this host's last report (epoch seconds) and its CPU rate
* over the interval that report closed (cores busy) */
int64_t sample_time;
float cpu_rate;
} xce_host_runusage_t;
typedef struct {
@ -465,6 +469,7 @@ typedef struct {
float peak_cpu_efficiency;
int num_hosts;
xce_host_runusage_t** host_runusage;
float peak_cpu; /* bug-1736: peak cores, unrounded */
} xce_job_runusage_t;
typedef struct {
@ -667,6 +672,7 @@ typedef struct {
typedef struct {
int signo;
int force;
int grace; /* bug-1957: > 0: graceful kill/stop, seconds between signals */
} xce_signal_t;
typedef struct {
@ -826,6 +832,15 @@ typedef struct {
typedef struct {
xce_resource_t* vres;
xce_resource_t* res;
/* bug-1834: resolved capacity-source resource for a percent-based
* limit on a dynamic/consumable resource (e.g. "mem" -> "maxmem",
* "swap" -> "maxswap") -- NULL when `res` itself already IS the right
* source (percent==0, or no static counterpart is registered/known).
* update_limit_res_value() (plugins/limit.c) reads this in preference
* to `res` so a percent limit's capacity is pinned to the resource's
* static total instead of drifting with its live, fluctuating value
* (see .kiro/steering/resource-accounting.md's "mem is not maxmem"). */
xce_resource_t* percent_res;
xce_limit_t* limit;
} xce_vres_t;

View file

@ -14,5 +14,57 @@ extern xce_alloc_t** make_allocs(int, ...);
extern xce_host_alloc_t** make_host_allocs(int, ...);
extern xce_job_account_t* make_job_account(xce_job_alloc_t*);
extern xce_job_alloc_t* dup_job_alloc(const xce_job_alloc_t*);
extern int merge_job_alloc(xce_job_alloc_t*, const xce_job_alloc_t*);
/*
* Allocation equality: "do these two records describe the same allocation
* decision".
*
* alloc_eq() compares, in this order: resname, reserve, value, duration,
* from. `value` is compared exactly (`==`, no floating-point tolerance): both
* sides are exact copies of the same double, so any difference is real.
* `from` (the reservation start time) is compared: the scheduler stamps it
* with the deciding session's start, which a replay reproduces. `instid` is
* NOT compared: it comes from a per-process counter (sched_get_id()) whose
* value depends on how many allocations that process built before, so a
* recording and its replay give the same allocation different instids
* (bug-1844).
*
* This is not merge_job_alloc()'s slot matching. merge_job_alloc() asks
* "are these the same reservation slot to add into" (resname, instid,
* reserve; value ignored because merging adds values), where instid IS
* significant. Neither predicate can stand in for the other.
*
* host_alloc_eq() compares host, then nallocs, then allocs[i] in order.
* job_alloc_eq() compares jobid, nhosts, nres_shared, then allocs[i] in
* order, then allocs_shared[i] in order. Lists are order-sensitive: the same
* entries in a different order are not equal.
*
* NULL handling: identical pointers (including two NULLs) are equal; exactly
* one NULL argument, list entry or string field is not equal. Precondition:
* a non-zero count with a NULL entry array is a malformed record and is not
* handled. Neither argument is modified.
*
* `why` is optional. When NULL, nothing is allocated. Otherwise *why is set
* to NULL when the arguments are equal, or to a newly allocated,
* NUL-terminated description of the first difference found, in comparison
* order, e.g.
* allocs[1](host=h1).allocs[0](resname=mem).value: 2 != 3
* allocs[0](host=h0).host: "h0" != "h2"
* allocs_shared[0]: NULL != non-NULL
* nhosts: 1 != 2
* job_alloc: non-NULL != NULL (a NULL top-level argument)
* Path labels come from `a`, from `b` when a's entry/string is NULL, or are
* `?`. The caller owns the description and releases it with free(). The value
* *why holds on entry is never read or freed. If allocating the description
* fails, *why is NULL and the result is unchanged. Nothing is logged.
*
* Returns non-zero when equal, 0 when not.
*/
extern int alloc_eq(const xce_alloc_t* a, const xce_alloc_t* b, char** why);
extern int host_alloc_eq(const xce_host_alloc_t* a, const xce_host_alloc_t* b,
char** why);
extern int job_alloc_eq(const xce_job_alloc_t* a, const xce_job_alloc_t* b,
char** why);
#endif

View file

@ -24,6 +24,8 @@ extern int xce_update_rows(xce_ctx_t*, int, int, xlist_t*);
extern int xce_resval(xce_ctx_t*, char*, char*, xce_resval_t***);
extern int xce_notify(xce_ctx_t*, xce_notify_data_t*);
/* bug-1741: send a notify and wait for the controller's commit ACK. */
extern int xce_notify_acked(xce_ctx_t*, xce_notify_data_t*);
extern int xce_update_resvals(xce_ctx_t*, int, xce_resval_t**);
extern int xce_jobctrl(xce_ctx_t*, xce_jobctrl_t*);
extern int xce_queuectrl(xce_ctx_t*, xce_queuectrl_t*);

View file

@ -9,6 +9,7 @@
#include "group.h"
#include "kvmgr.h"
#include "utils.h"
#include "xce/types.h"
extern int build_consumer(xce_consumer_t**, var, xce_consumer_type_t, int,
@ -20,6 +21,14 @@ extern int build_vres_name(char**, xce_limit_t*, char*, char*, char*);
extern int is_host_based_limit(var, xce_limit_t*);
extern int fmt_consumer(char*, int, xce_consumer_t*);
extern int fmt_limit(char*, int, xce_limit_t*);
/* fmt_limit_units() -- scheduler-sim-diagnostics spec, Requirement 6, task
* 8.1. Sibling of fmt_limit() (the fmt_jobid()/fmt_jobid_ex() pattern):
* identical consumer-dump loop, but the final configured-value line renders
* a byte-valued, non-percentage resource's amount via xce_format_units_value()
* according to the supplied units_mode instead of always the bare "%f".
* fmt_limit() itself is now a one-line wrapper: fmt_limit_units(buf, len,
* limit, XCE_UNITS_RAW) -- byte-identical to its own pre-existing output. */
extern int fmt_limit_units(char*, int, xce_limit_t*, xce_units_mode_t);
extern void kill_limit(xce_limit_t*);
extern int instantiate_limit(xce_vres_t**, var, xce_limit_t*, char*, char*,
char*);

44
src/include/xcelist.h Normal file
View file

@ -0,0 +1,44 @@
#if !defined(__XCELIST_H__)
#define __XCELIST_H__ 1
/*
* Intrusive doubly-linked list.
*
* A list entry must have its `forw` and `back` pointers as its first two
* members so that a user-defined node (e.g. `struct { void* forw; void* back;
* ...payload... }`) can be safely cast to `xcelist_entry_t*`.
*
* The list head (`xcelist_t`) is itself layout-compatible with an entry: its
* `forw` field points to the first entry (or back to itself when empty) and
* its `back` field points to the last entry. `numEnts` tracks the entry count.
*
* This replaces the previously external `basiclib/list.h` (extralib) with the
* minimal subset of functionality actually used in-tree.
*/
typedef struct _xcelist_entry {
struct _xcelist_entry* forw;
struct _xcelist_entry* back;
} xcelist_entry_t;
typedef struct _xcelist {
/* head sentinel: must mirror xcelist_entry_t layout (forw/back first) */
xcelist_entry_t* forw;
xcelist_entry_t* back;
int numEnts;
char* name;
} xcelist_t;
/* Create an empty list. `name` is copied and may be NULL. Returns NULL on OOM. */
extern xcelist_t* list_create(const char* name);
/* Destroy a list. Does not free the entries; caller owns them. */
extern void list_destroy(xcelist_t* list);
/* Append `entry` to the back (tail) of `list`. */
extern void list_insert_entry_back(xcelist_t* list, xcelist_entry_t* entry);
/* Unlink `entry` from `list`. Does not free `entry`. */
extern void list_entry_remove(xcelist_t* list, xcelist_entry_t* entry);
#endif