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 the67f3f6adscratch-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:
commit
0c8607345e
663 changed files with 139848 additions and 13614 deletions
|
|
@ -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*);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
* ====================================================== */
|
||||
|
|
|
|||
|
|
@ -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*);
|
||||
|
||||
|
|
|
|||
|
|
@ -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*);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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*);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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*);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
61
src/include/xce/ctrlstatus_fmt.h
Normal file
61
src/include/xce/ctrlstatus_fmt.h
Normal 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_ */
|
||||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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*);
|
||||
|
|
|
|||
|
|
@ -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
44
src/include/xcelist.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue