merge: sync Rongsong's scheduler-framework into chatbot

Full merge of origin/scheduler-framework (scheduler-core restructure,
failover-adoption, WAL_JOB_DISPATCH, apiserver backend health/heartbeat,
build-time versioning, bug-1673/1676/1678/1679/1680).

Conflict resolution:
- scheduler: adopt Rongsong's scheduler-core architecture (driver loop,
  queue project/picker fields, resource bug-1569 fix)
- worker/controller_connector.c: keep Rongsong's reconnect/rejoin
  structure, PORT our bug-1509 resend_job_status onto the rejoin success
  path (his reconnect didn't re-report job state)
- apiserver backend: take Rongsong's health/heartbeat (supersedes our
  e505066 circuit breaker)
- group.c: keep our bug-1513 comment
- misc: take theirs for additive fixes (go_build versioning,
  test_group.c, persist.c RUSAGE_UPDATE, FilterRows bug-1680)

NOTE: our scheduler fixes (bug-1494 family dispatch-fail requeue,
PEND-reason classification) are NOT in Rongsong's branch — needs
verification/porting onto scheduler-core (separate follow-up).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-25 12:33:16 +00:00
commit 4e894d2184
395 changed files with 88517 additions and 5742 deletions

View file

@ -6,6 +6,7 @@
#include <stddef.h>
#include <stdint.h>
#include "config.h"
#include "netio.h"
#include "request.h"
#include "xce/types.h"
@ -25,7 +26,9 @@ typedef void* xcecli_ctx_t;
*/
typedef int (*xcecli_reconnect_cb)(xcecli_ctx_t ctx, void* arg);
#define N_MAX_MASTERS (3)
/* N_MAX_MASTERS derives from config.h's MAX_HA_CANDIDATES so the two limits
* can never silently diverge (Req 1 AC7, component-failover-adoption). */
#define N_MAX_MASTERS MAX_HA_CANDIDATES
typedef struct {
int nmasters;
@ -134,4 +137,24 @@ extern int xcecli_jobinfo(xcecli_ctx_t, uint64_t, xce_job_t**);
extern int xcecli_reply_fetchfile(xcecli_ctx_t, xce_request_t*, xce_fetchfile_reply_t*);
extern int xcecli_fetchfile(xcecli_ctx_t, xce_fetchfile_t*, xce_fetchfile_reply_t**);
/*
* xcecli_reply_schedadmin: send a SchedAdminReply back over `c`'s
* connection in response to a previously-received OPCode_schedAdmin
* request (scheduler-core spec, task 9.4, Req 5 AC 10). Mirrors
* xcecli_reply_fetchfile() above exactly, with one difference: it takes
* the original request's `seq` directly rather than an `xce_request_t*`,
* since `recordStart`/`recordStop` defer this reply to the driver thread
* (see driver.c's xce_sched_driver_translate_pending_events()), by which
* point the original xce_request_t has already been freed by the
* request-handling thread (xcectx.c's vtask_entry(), immediately after the
* registered handler returns) -- seq is the only field of it the reply
* needs (opcode is always OPCode_schedAdmin). `recordStatus` (answered
* synchronously from the handler thread, real request still in scope)
* uses this same function, simply passing `request->seq` directly.
* Returns xce_reply()'s own return value; -EINVAL if `c` or `areply` is
* NULL.
*/
extern int xcecli_reply_schedadmin(xcecli_ctx_t, uint64_t seq,
sched_admin_reply_t* areply);
#endif

View file

@ -120,6 +120,21 @@ typedef struct endpoint_t {
* their lifetime directly. */
_Atomic int refcnt;
int heap_alloc; /* 1 iff allocated by endpoint_get (unref may free) */
/* bug-1548: bumped once, unconditionally, by xce_ctx_reconnect() (xcectx.c)
* on every successful transport reconnect, regardless of trigger path
* (write-path XCECLI_FAILOVER_CALL retry or heartbeat XCECLI_FF_CALL
* best-effort self-heal). Neither endpoint_init() nor endpoint_fini()
* (netio.c) touch this field, so it survives reconnect cycles the same
* way refcnt/heap_alloc already do. Exists so a consumer (e.g. apiserver's
* heartbeat prober, client.h's xcecli_get_my_endpoint) can detect that a
* reconnect happened even when it completed silently inside a single
* fire-and-forget call and the call's own return code never surfaced a
* failure — XCECLI_FF_CALL can rotate+recover before evaluating its EXPR,
* so a failure->success transition on the caller's own return code is not
* a reliable signal (component-failover-adoption Phase 6). Does not
* change reconnect timing/ordering/semantics — a passive observation
* point at the existing success path, same shape as on_fini above. */
_Atomic uint64_t reconnect_gen;
} endpoint_t;
/* bug-1298 family: msg_handler runs SYNCHRONOUSLY on the io_worker thread (not

View file

@ -151,10 +151,18 @@ typedef enum {
* the loaded snapshot's checkpoint_lsn (entries at/below it are skipped
* before ever reaching dispatch — see replay_wal), so each one names a
* job that must now exist in the live job table that did NOT exist
* before this replay pass began. */
* before this replay pass began.
* expected_new_worker_count — bug-1427: the worker analog of
* expected_new_job_count. The number of WAL_WORKER_CTRL/
* HOOK_DETAIL_WORKER_JOIN entries replay dispatched in this pass that
* genuinely created a new worker (worker_from_capnp() returns 1 for
* that case, 0 for an UPDATE/DEAD applied to an already-existing
* worker or -EEXIST for a duplicate JOIN — only the 1 case is tallied,
* mirroring WAL_JOB_SUBMIT's apply_rc == 1 convention). */
typedef struct {
uint64_t expected_last_applied_lsn;
uint64_t expected_new_job_count;
uint64_t expected_new_worker_count;
} apply_expectation_t;
/* ============================================================
@ -528,6 +536,13 @@ struct persist_s {
* must NOT advance and the failure is logged loudly. Compiled out of
* production (xce_controller is built without -DXCE_TESTING). */
atomic_int test_force_fsync_fail;
/* Flusher attempt counter (bug-1563): incremented once per
* flusher_flush_once() invocation, regardless of outcome. A sustained
* fdatasync failure must not spin the async fsync_worker thread at full
* CPU with zero backoff between retries -- this proves attempt rate
* stays bounded to roughly the wal_sync_ms cadence even while durable_lsn
* is stuck. Compiled out of production. */
atomic_uint test_flusher_flush_attempt_count;
/* fdatasync observability counter (ha-commit-durability task 2.3):
* incremented at the standby-path inline sync in
* persist_wal_write_replicated, BEFORE that function returns (and thus
@ -915,24 +930,28 @@ bool persist_recovery_complete(persist_t* p);
void persist_mark_recovery_pending(persist_t* p);
/* persist_verify_apply_correctness (raft-apply-decoupling task 5.1, Req
* 8.1/8.2). Pure comparison — mirrors persist_recover_decision's separation
* of decision from action: no logging, no fence side effects, no mutation of
* `p` beyond the read of last_applied_lsn. Compares `exp` (derived by
* scanning the committed log during replay — see apply_expectation_t) against
* the actual materialized state: p's replay-derived apply cursor, and — when
* p->cluster is non-NULL — the live job table's growth since
* `jobmgr_count_before_replay` (the job count captured just before replay
* began). Returns APPLY_CLEAN on a match. Returns APPLY_DIVERGENT when either
* the apply cursor or the live job count disagrees with `exp`.
* 8.1/8.2; worker half added bug-1427). Pure comparison — mirrors
* persist_recover_decision's separation of decision from action: no
* logging, no fence side effects, no mutation of `p` beyond the read of
* last_applied_lsn. Compares `exp` (derived by scanning the committed log
* during replay — see apply_expectation_t) against the actual materialized
* state: p's replay-derived apply cursor, and — when p->cluster is
* non-NULL — the live job table's growth since `jobmgr_count_before_replay`
* and the live worker table's growth since `workermgr_count_before_replay`
* (both counts captured just before replay began). Returns APPLY_CLEAN on a
* match. Returns APPLY_DIVERGENT when the apply cursor, the live job count,
* or the live worker count disagrees with `exp`.
*
* Safe to call with p->cluster == NULL (a bare unit-test persist_t, or any
* caller with nothing to dispatch into): the job-count half of the check is
* skipped — only the position check applies — mirroring
* apply_committed_entry()'s own "nothing to dispatch into" convention. Safe
* on NULL p or NULL exp (returns APPLY_CLEAN — nothing to verify). */
* caller with nothing to dispatch into): the job-count and worker-count
* halves of the check are both skipped — only the position check applies —
* mirroring apply_committed_entry()'s own "nothing to dispatch into"
* convention. Safe on NULL p or NULL exp (returns APPLY_CLEAN — nothing to
* verify). */
apply_verdict_t persist_verify_apply_correctness(
persist_t* p, const apply_expectation_t* exp,
uint64_t jobmgr_count_before_replay);
uint64_t jobmgr_count_before_replay,
uint64_t workermgr_count_before_replay);
/* Accessor for the sticky verdict computed by the last persist_recover()
* (task 5.1). APPLY_CLEAN before recovery has run (fresh store default,
@ -1389,6 +1408,38 @@ int persist_submit_job_fact(persist_t* p, uint32_t wal_type, uint64_t jobid,
* case, but is still written for forward-compatibility/diagnostics). */
int persist_submit_job_fact_ex(persist_t* p, uint32_t wal_type,
uint64_t jobid, int state, uint32_t pad);
/* bug-1678: WAL_JOB_DISPATCH's own sibling of persist_submit_job_fact(),
* additionally encoding the scheduler's resource allocation into the fact's
* payload (header {jobid, state} + trailing Cap'n Proto JobAlloc bytes) so a
* restart/catch-up replay can reconstruct job.allocs instead of leaving it
* permanently NULL -- the WAL_JOB_DISPATCH-only reason persist_submit_job_fact
* itself was never sufficient for this one fact type. `alloc` must be
* non-NULL (the real call site, dispatch_a_job_(), always has one by
* construction). Returns the local-append rc, same contract as
* persist_submit_job_fact(). */
int persist_submit_job_dispatch_fact(persist_t* p, uint64_t jobid, int state,
const xce_job_alloc_t* alloc);
/* 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,
* made just before this). Without this call, dispatch_a_job_()'s live,
* synchronous state/allocs mutation is never notified: it fires no hooks at
* all, and apply_committed_entry()'s later apply of that same WAL_JOB_DISPATCH
* entry sees the state already matches (JOB_TRANSITION_NOOP_DUPLICATE) and
* skips its own would-be notify -- the job is stuck at PEND from the
* apiserver's perspective forever, restart or not.
*
* Internally guards with the SAME t_apply_seam_notifying flag
* apply_committed_entry()'s own shared WAL_JOB_START/.../DISPATCH case uses
* around its identical hook fire (mirrors that exact bracketing) -- without
* it, persist_job_hook_fun (also subscribed to this hook) would redundantly
* re-append this same transition as a second, duplicate WAL_JOB_UPDATE
* entry. The flag stays file-local (static) to persist.c on purpose; this
* function is job.c's only sanctioned way to reach it. */
void persist_notify_job_updated_already_persisted(cluster_t* c, job_t* j);
int persist_worker_hook_fun(int type, int situation, int detail,
void* cls, va_list ap);

View file

@ -71,6 +71,15 @@ extern int xce_set_pend_reason_req(endpoint_t*, xce_preason_set_t*);
extern int xce_clear_reason_req(endpoint_t*, xce_clear_reason_t*);
extern int xce_fetchfile_req(endpoint_t*, xce_fetchfile_t*);
/*
* xce_schedadmin_req: send OPCode_schedAdmin (SchedAdminRequest) to `ep` --
* the async send half of task 10.1's controller->scheduler forward. Mirrors
* xce_fetchfile_req above exactly. The caller (myreq_handler, cluster.c)
* blocks separately for the matching SchedAdminReply; this function only
* enqueues the request, it does not wait.
*/
extern int xce_schedadmin_req(endpoint_t*, sched_admin_req_t*);
/*
* xce_replconfigpush_req_raw: send OP_replConfigPush carrying raw byte arrays.
* Bypasses the encode_ReplConfigPushRequest_ptr path (which maps sha256/bytes

View file

@ -197,6 +197,14 @@ typedef struct {
uint64_t peer_last_log_lsn;
} request_vote_reply_t;
typedef struct {
char *name;
} probe_config_tag_req_t;
typedef struct {
uint64_t config_tag;
} probe_config_tag_reply_t;
/* Heartbeat reply state report — carries standby's applied_lsn and config_tag,
* plus its raft term so the primary can discover a higher term it lost track
* of (bug-1421 term-discovery: an AE reject response never reaches the raft
@ -216,6 +224,31 @@ typedef struct {
int success;
} wal_ack_reply_t;
/* Scheduler admin opcode request/reply (scheduler-core task 7.4, Req 5 AC 10).
* OPCode_schedAdmin: start/stop/query a scheduler-side `.replay` recording
* session. No field-level $C.mapname on either capnp struct (per the exact
* verbatim schema spec), so -- same convention as xce_ctrlstatus_t below --
* the generated accessors and this mapped struct's field names match the
* capnp schema's camelCase names exactly (no snake_case conversion). action
* mirrors the existing HostRole/JoinData.role convention: an enum-typed
* capnp field maps to a plain `int` here, not a separate named C enum type
* (the generated `enum SchedAdminAction` in controller.capnp.h already
* supplies the SchedAdminAction_recordStart/recordStop/recordStatus
* constants for callers that want them). */
typedef struct {
int action;
char *recordFile;
} sched_admin_req_t;
typedef struct {
int ok;
char *error;
int recording;
char *file;
uint64_t eventCount;
uint64_t startedAt;
} sched_admin_reply_t;
/* ctrlstatus reply payload (controller-status spec): one configured
* controller (CtrlPeer) and the lean identity/health snapshot (CtrlStatus).
* The peers list maps via $C.mapname("peers.list")/$C.maplistcount("peers.count")
@ -265,6 +298,8 @@ typedef struct {
wal_ack_reply_t* walackreplymsg;
xce_ctrlstatus_t* ctrlstatus;
xce_fetchfile_reply_t* fetchfilereply;
sched_admin_reply_t* schedadminreplymsg;
probe_config_tag_reply_t* probeconfigtagreplymsg;
} data;
/* Req 7: standby redirect fields — populated when status == XCE_ERR_STANDBY */
char *redirect_host;
@ -304,6 +339,8 @@ typedef struct {
wal_catchup_entry_t* walcatchupentrymsg;
snapshot_transfer_t* snapshottransfermsg;
xce_fetchfile_t* fetchfilemsg;
sched_admin_req_t* schedadminmsg;
probe_config_tag_req_t* probeconfigtagrequestmsg;
} body;
} xce_request_t;
@ -314,14 +351,6 @@ typedef struct {
* request_vote_req_t and request_vote_reply_t are defined above
* xce_reply_t so they can be used in the xce_request_t body union. */
typedef struct {
char *name;
} probe_config_tag_req_t;
typedef struct {
uint64_t config_tag;
} probe_config_tag_reply_t;
typedef struct {
uint64_t term;
uint64_t lsn;
@ -358,8 +387,14 @@ struct snapshot_transfer_s {
struct wal_catchup_entry {
uint64_t lsn;
uint8_t entry_type;
xce_data_t *payload; /* legacy decoded field — broken for
* raw bytes; kept NULL, do not use */
/* bug-1505 fix: List(UInt8) now, not Data (see controller.capnp's
* WalCatchupEntry.payload comment for the full root cause) — a real
* uint8_t* + wire_payload_len pair via the generic codec, same shape
* bytes_read/content already establish elsewhere. Still legacy/unused
* by production (wal_entry_to_wire() in request.c deliberately never
* sets it — see that comment); this genuinely round-trips now instead
* of being a documented-broken placeholder, but nothing reads it. */
uint8_t *payload;
int n_entries; /* batch: number of sub-entries */
struct wal_catchup_entry **entries; /* batch: array of sub-entry pointers */
/* Raw WAL entry payload bytes — same pattern as repl_config_push_req_t.
@ -394,15 +429,23 @@ struct wal_catchup_entry {
* send; used on decode to slice this entry's bytes out of the TOP-LEVEL
* container's batch_payloads blob (below). */
uint32_t wire_payload_len;
/* bug-847: generic decoded placeholder for wire field batchPayloads @10 —
* same broken-for-raw-bytes limitation as `payload` above; kept NULL, do
* not use. Only meaningful on the TOP-LEVEL container (never on a nested
* sub-entry). The real bytes are hand-extracted by xce_decode() into
* raw_batch_payloads/raw_batch_payloads_len below, then sliced per
/* bug-1505 fix: List(UInt8) now, not Data (controller.capnp's
* WalCatchupEntry.batchPayloads comment has the full root cause —
* same class as `payload` above and SnapshotData.internalState,
* bug-1504). Generic decoded field for wire field batchPayloads @10;
* genuinely round-trips now via the generic codec, but still unused by
* production — the real bytes are hand-extracted by xce_decode() into
* raw_batch_payloads/raw_batch_payloads_len below (a separate,
* already-correct code path this fix does not touch), then sliced per
* sub-entry using each entry's wire_payload_len into that entry's OWN
* raw_payload/raw_payload_len (so every consumer keeps reading
* raw_payload exactly as before — this is purely a wire-transport fix). */
xce_data_t *batch_payloads;
* raw_payload exactly as before). Only meaningful on the TOP-LEVEL
* container (never on a nested sub-entry). batch_payloads_wire_len is
* its own maplistcount companion (wire field batchPayloadsLen @11) —
* distinct from raw_batch_payloads_len below, which belongs to the
* separate hand-rolled extraction path. */
uint8_t *batch_payloads;
uint32_t batch_payloads_wire_len;
uint8_t *raw_batch_payloads;
size_t raw_batch_payloads_len;
};
@ -541,4 +584,186 @@ struct ctrl_hello_reply_s {
uint64_t config_tag; /* primary's current config tag */
};
/* ── Replay stream envelope frames (scheduler-core Group 7 task 7.1,
* Req 5 AC 20) ──────────────────────────────────────────────────────────
* Frame-only wire types for the `.replay` stream envelope -- same "no live
* C struct" convention as capnp_snap_hdr_t/capnp_persist_cfg_t/
* capnp_host_admin_state_t above (see controller.capnp's comment at
* `struct ReplayCluster` for the full Group 7 task split). The C-side
* reader/writer that actually populates/consumes these (recording,
* `.replay` file I/O) is task 9+, out of this schema-only task's scope. */
typedef struct {
char *name;
char **controllers;
int n_controllers;
char *recording_host;
} replay_cluster_t;
typedef struct {
char *recorder_version;
/* configHash is a Data field -- the generated encode_/decode_ codec is
* a documented no-op for Data fields in this codebase's capnpc-c setup
* (same limitation as e.g. upgrade_config_req_s.sha256/bytes above);
* real byte extraction is task 9+ hand-written code, same raw_sha256-
* style pattern used throughout this file. This pointer/len pair exists
* only so the mapname target type is well-formed; nothing here reads it
* yet. */
uint8_t *config_hash;
size_t config_hash_len;
replay_cluster_t *cluster;
uint64_t recording_start_ts;
char *recording_start_wall;
char *sched_policy;
} replay_header_t;
/* task 7.3 (Req 5 AC 21): SchedUser/SchedProject mirror controller.capnp's
* SchedUser/SchedProject structs. Field lists match xce_sched_user_t/
* xce_sched_project_t (src/scheduler/include/sched/types.h) exactly -- name
* only, name+priority respectively -- but these are FRESH, wire-only C
* types under different names, NOT aliases of the scheduler-internal ones.
* See controller.capnp's comment at `struct SchedUser` for the full
* rationale: sched/types.h is scheduler-private and already `#include`s
* this file, so a duplicate `xce_sched_user_t`/`xce_sched_project_t`
* typedef here would break every scheduler .c file that includes both
* headers (confirmed: core_events.c, schedobj.c already do). */
typedef struct {
char *name;
} sched_snapshot_user_t;
typedef struct {
char *name;
int32_t priority;
} sched_snapshot_project_t;
/* task 7.3 (Req 5 AC 21): the full snapshot struct, replacing the task 7.1
* placeholder wholesale. Every domain-object pointer reuses a pre-existing
* native type (already used natively throughout the scheduler/controller)
* except users/projects (sched_snapshot_user_t/sched_snapshot_project_t
* above) and the two scheduler-internal-only opaque fields. internal_version
* is a format tag for internal_state so an incompatible future internal
* layout is rejected rather than misinterpreted (this task's Observable).
*
* internal_state/internal_state_len map controller.capnp's internalState,
* a List(UInt8) (task 12.1 review finding, fixed -- was Data, whose
* generated codec is a silent no-op in this codebase's capnpc-c setup and
* left a genuinely uninitialized capn_ptr on the wire, a real reproducible
* crash; see controller.capnp's SnapshotData comment for the full story).
* List(UInt8)'s generated encode_/decode_ actually runs (proven precedent:
* xce_fetchfile_reply_t.content/bytes_read below), so internal_state_len
* must be uint32_t to match internalStateLen's UInt32 wire type -- same
* maplistcount convention as content/bytes_read. */
typedef struct {
xce_conf4sched_t *config;
xce_jobs_data_t *jobs;
xce_queue_t **queues;
int n_queues;
xce_workers_data_t *workers;
xce_resource_data_t *resources;
xce_resval_list_t *resvals;
sched_snapshot_user_t **users;
int n_users;
sched_snapshot_project_t **projects;
int n_projects;
uint64_t prng_seed;
uint8_t *internal_state;
uint32_t internal_state_len;
uint32_t internal_version;
} sched_snapshot_data_t;
typedef struct {
uint64_t ts;
sched_snapshot_data_t *data;
} replay_snapshot_entry_t;
/* task 7.2 (Req 5 AC 20): the two genuinely new event payload types -- every
* other replay_event_t.body variant above reuses a pre-existing wire type
* (see controller.capnp's comment at `struct JobStateChangeEvent`). */
typedef struct {
uint64_t jobid;
uint32_t old_state;
uint32_t new_state;
int32_t exit_code;
} job_state_change_event_t;
typedef struct {
uint64_t jobid;
/* changedFields is a UInt32 bitmask (JOB_F_PRIORITY, JOB_F_QUEUE,
* JOB_F_RESREQ per design.md) -- no JOB_F_* constants exist anywhere in
* the codebase yet (task 5.1's grep); concrete bit assignments are
* task 9+ event-translation/recording code's job, not this schema-only
* task's. */
uint32_t changed_fields;
xce_job_t *job; /* full updated record -- reuses xce_job_t (Job in
* controller.capnp) */
} job_update_event_t;
typedef struct {
uint64_t ts;
int body_which;
union {
xce_conf4sched_t *config_push;
xce_jobs_data_t *jobs_push;
xce_workers_data_t *workers_push;
xce_resource_data_t *resources_push;
xce_resval_list_t *resvals_full;
xce_resval_list_t *resvals_delta;
xce_queue_t *queue_update;
xce_job_rusages_t *job_rusage;
/* endSession: Void, no storage */
job_state_change_event_t *job_state_change;
job_update_event_t *job_update;
} body;
} replay_event_t;
typedef struct {
uint64_t duration_ns;
uint32_t n_decisions;
uint32_t n_dispatched;
uint32_t n_pended;
} session_summary_t;
/* task 7.3 (Req 5 AC 5, AC 20): the three decision payload types --
* dispatch/pend reuse existing native types (xce_job_alloc_t,
* xce_job_preason_t); preempt_victim_t/sched_preempt_decision_t are fresh,
* wire-only types, deliberately distinct from decision.h's core-internal
* xce_preempt_victim_t/xce_preempt_t (task 4.4's in-process live-decision
* representation) -- see controller.capnp's matching comment. */
typedef struct {
xce_job_alloc_t *alloc;
} sched_dispatch_decision_t;
typedef struct {
uint64_t jobid;
xce_job_preason_t *reason;
} sched_pend_decision_t;
typedef struct {
uint64_t jobid;
char *workerid;
} preempt_victim_t;
typedef struct {
uint64_t jobid;
preempt_victim_t **victims;
int n_victims;
xce_job_preason_t *reason;
} sched_preempt_decision_t;
typedef struct {
uint64_t ts;
int body_which;
union {
sched_dispatch_decision_t *dispatch;
sched_pend_decision_t *pend;
sched_preempt_decision_t *preempt;
} body;
} replay_decision_t;
typedef struct {
uint64_t begin_round;
replay_decision_t **decisions;
int n_decisions;
session_summary_t *summary;
} replay_session_t;
#endif

View file

@ -61,6 +61,55 @@
#define MK_VERSION(ver, commit) ver commit
/* SUPERSEDED for version-string composition (build-time-versioning task
* 3.1). MK_VERSION/STR (immediately above) cannot express an
* empty-value -> "unknown" fallback: an unset commit_id/build_time meson
* option makes STR(x) expand to an empty string literal ("", not a missing
* token), and a preprocessor macro has no way to branch on "is this string
* empty" at expansion time. xce_version_field()/xce_compose_version()
* (extern decls below, implementation + doc comments in utils.c) are the
* runtime replacement — they DO perform that substitution, and are what
* compose the version string everywhere it now matters. Use those, not
* MK_VERSION, for any new version-string composition.
*
* MK_VERSION/STR are kept in place here (not deleted) only because task 3.2
* — which will migrate controller/main.c's and scheduler/src/main.c's
* argp_program_version construction to xce_compose_version() — has not
* landed yet. Grep finding (2026-08-20, `grep -rn 'STR(\|MK_VERSION(' src/`,
* excluding NULLSTR/comment/definition matches): STR() has ZERO call sites
* anywhere in this codebase outside this exact MK_VERSION-based
* argp_program_version pattern — controller/main.c:23,
* scheduler/src/main.c:26, AND controller/testsuite/test.c:662 (the
* xce_test REPL tool). That third caller is NOT listed in task 3.2's
* boundary (`controller/main.c, scheduler/main.c` only, per tasks.md) —
* worth confirming whether it's in scope when 3.2 lands, or it will keep
* printing a commit-id-only, build-time-less version string after the
* other two are migrated. STR() itself is a generic single-argument
* stringification macro (not inherently version-specific), but per this
* grep it currently has no unrelated use, so it is not being kept around
* for any other live caller — only in service of the not-yet-migrated
* MK_VERSION call sites above. Once task 3.2 (and any update to
* controller/testsuite/test.c) lands and no caller of MK_VERSION remains,
* both MK_VERSION and this STR usage can be deleted outright — not done
* here, out of this task's boundary.
*
* NOTE FOR TASK 3.2 (or whoever migrates argp_program_version):
* xce_compose_version() is a REGULAR RUNTIME FUNCTION, not a constant
* expression — unlike MK_VERSION's pure string-literal concatenation, it
* CANNOT initialize a file-scope variable. `const char* argp_program_version
* = xce_compose_version(...)` at file scope will not compile (C file-scope
* initializers must be constant expressions). controller/main.c:22-23 and
* scheduler/src/main.c:25-26 currently declare argp_program_version exactly
* that way — a file-scope const initialized at compile time — so this is
* not a one-line macro-call swap. Instead: compute the version string at
* RUNTIME into a `static char buf[...]` (module scope or local to main()),
* via xce_compose_version(), early in main() and BEFORE argp_parse() runs
* (argp reads argp_program_version by pointer when --version/-V is
* processed) — then assign argp_program_version (no longer const-qualified
* at its declaration; assigned at runtime instead of initialized) to point
* at that buffer. See xce_compose_version()'s doc comment in utils.c for
* the buffer-sizing contract. */
#ifndef FREEUP
#define FREEUP(p) \
if ((p) != NULL) { \
@ -114,6 +163,28 @@ extern int mylog_init(char*, char*, int, int);
extern void mylog_fini(void);
extern int mylog_setlevel(char*);
extern char* verbose_to_loglevel(int);
/* See the SUPERSEDED/NOTE comment above MK_VERSION for what these replace
* and why, and the constraint they impose on any file-scope
* argp_program_version-style global (xce_compose_version() is not a
* constant expression). */
extern const char* xce_version_field(const char*);
extern int xce_compose_version(char*, size_t, const char*, const char*,
const char*);
/* dirty-content-fingerprint task 3.1: --version-only sibling of
* xce_compose_version() above. Same composition (prefix + commit +
* " built " + build_time), plus one more optional field: when
* dirty_fingerprint is non-empty, a trailing "-dirty" on commit is
* stripped and "-<dirty_fingerprint>" is appended in its place, producing
* "<commit>-<dirty_fingerprint>". When dirty_fingerprint is NULL or empty,
* this function delegates straight to xce_compose_version() and produces
* byte-identical output -- commit_id's own computation and its existing
* -dirty suffix are never touched by this function. The startup-log
* composition call sites (controller/scheduler/worker main.c) must keep
* calling xce_compose_version() directly, never this function -- this is
* what keeps the startup log's format unchanged per Requirement 3.2 of the
* dirty-content-fingerprint spec. */
extern int xce_compose_version_ex(char*, size_t, const char*, const char*,
const char*, const char*);
extern void set_error(char*, char*, int, int);
extern ectx_t* get_ectx(void);
extern int xce_vprintf(const char*, va_list);

View file

@ -114,6 +114,15 @@ typedef enum {
* the whole array (base spec + index range),
* distinct from WAL_JOB_SUBMIT's single-job
* format, which stays unchanged. */
WAL_JOB_DISPATCH = 12, /* scheduler-dispatch-commit: durable record of a
* job's synchronous dispatch-time transition to
* running, distinct from (and independent of)
* WAL_JOB_START's later worker-confirmed-start
* fact. Payload is wal_jobstate_t{jobid, state,
* _pad=0} -- always the legacy absolute-value
* form (no racing same-job fact to defer against,
* since this is the first transition after a job
* leaves PEND). */
WAL_QUEUE_CTRL = 20,
WAL_WORKER_CTRL = 30,
WAL_NOOP = 40, /* Internal: empty payload, replay no-op; appended only at promotion */

View file

@ -7,6 +7,7 @@
#define XCE_CLUSTER_H
#include <stddef.h>
#include "client.h" /* xce_client_config_t, N_MAX_MASTERS */
#include "config.h" /* MAX_HA_CANDIDATES, XCE_MAXHOSTNAME_LEN */
/**
@ -28,7 +29,6 @@ typedef struct {
* (encryption without client-side peer certificate verification).
*
* candidates_count must be in [1, MAX_HA_CANDIDATES].
* A WARN is logged at startup if candidates_count > 3.
*/
typedef struct {
xce_candidate_t candidates[MAX_HA_CANDIDATES];
@ -79,6 +79,24 @@ typedef struct {
*/
int xce_cluster_config_validate(const xce_cluster_config_t* cfg);
/**
* xce_cluster_config_to_client_config() — convert a validated candidate list
* into an xce_client_config_t's master fields (nmasters/masters/master_ports).
*
* Copies up to N_MAX_MASTERS candidates from cfg->candidates[] in order.
* If cfg->candidates_count > N_MAX_MASTERS, copies the first N_MAX_MASTERS and
* logs a WARN identifying the truncated count (mirrors xce_test's existing
* clamp behavior).
*
* Does NOT set out->from / out->key — caller sets those separately.
* Does NOT validate cfg — caller must have already called
* xce_cluster_config_validate() successfully.
*
* Returns 0 on success, -EINVAL if cfg or out is NULL or cfg->candidates_count < 1.
*/
int xce_cluster_config_to_client_config(const xce_cluster_config_t* cfg,
xce_client_config_t* out);
/**
* xce_connect_cluster() — connect to the cluster via candidate list.
*

View file

@ -333,6 +333,7 @@ typedef struct {
char* user_group;
char* job_description;
char* resreq;
char* from_host;
} job_submit_data_t;
typedef struct {
@ -487,6 +488,15 @@ typedef struct {
* serialized (wire type xce_job_t above is untouched). 0 = unknown /
* pre-feature = always exportable (see job_submit_committed, cluster.h). */
uint64_t submit_lsn;
/* scheduler-dispatch-commit (1.3): consecutive retryable dispatch-failure
* count for this job, with no intervening successful dispatch. In-memory
* only, following submit_lsn's exact precedent above — never serialized
* (wire type xce_job_t above is untouched), reset to 0 by a successful
* dispatch. Resetting to 0 after a controller failover is an acceptable,
* safe degradation (more retries allowed, never fewer, never a
* misrouted terminal/retry decision) — see Controller Dispatch-Failure
* Classification, design.md. */
uint32_t dispatch_fail_count;
} job_t;
typedef struct {