Merge branch 'working' into job_peek_lchen

job_peek_lchen: Add fetchfile feature (job output/error peek proxy)
working: HA/Raft infrastructure (consensus, replication, config mgmt)

Conflict resolution summary:
- controller.capnp: fetchfile @41, fetchfilereply @18, fetchfilemsg @30
- cluster.c: bifurcated OPCode_reply for fetchfile + config-push ACK
- job.c: kept full fetchfile pending-request subsystem (~183 lines)
- request.c: kept both xce_fetchfile_req() and XCE_RETRY_DEFAULTS
- helper.c: added missing preVoteRequest/preVoteReply mappings
- All 15 new test registrations (test_cbuf.c + 14 HA tests)
This commit is contained in:
Lei Chen 2026-07-22 13:57:25 +08:00
commit 3a2094a461
529 changed files with 345174 additions and 5048 deletions

35
src/include/byteorder.h Normal file
View file

@ -0,0 +1,35 @@
#pragma once
/* Portable be64toh() — big-endian to host byte order conversion. */
#if defined(__linux__)
# ifndef _DEFAULT_SOURCE
# define _DEFAULT_SOURCE 1
# endif
# ifndef _BSD_SOURCE
# define _BSD_SOURCE 1
# endif
# include <endian.h>
# ifndef be64toh
/* musl and some older glibc: fall back to builtin */
# include <stdint.h>
static inline uint64_t be64toh(uint64_t x) {
# if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return __builtin_bswap64(x);
# else
return x;
# endif
}
# endif /* be64toh */
#elif defined(__APPLE__)
# include <machine/endian.h>
# include <libkern/OSByteOrder.h>
# define be64toh(x) OSSwapBigToHostInt64(x)
#else
# include <stdint.h>
static inline uint64_t be64toh(uint64_t x) {
# if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
return __builtin_bswap64(x);
# else
return x;
# endif
}
#endif

View file

@ -2,6 +2,8 @@
#define _CLIENT_H_ 1
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "netio.h"
@ -11,6 +13,18 @@
typedef void* xcecli_ctx_t;
/*
* xcecli_reconnect_cb: consumer rejoin hook invoked once per transparent
* reconnect/failover, AFTER the reconnect/recovery and BEFORE the interrupted
* op resumes. The callback CONTENT (join handshake, worker job re-sync) belongs
* to the consumer; libxce owns only the mechanism + ordering. Runs on the
* calling thread. Return 0 to proceed with the interrupted op; <0 to treat the
* rejoin as failed (the new primary rejected the join) so the failover loop
* rotates/backs off again instead of resuming. Pure clients register NO hook
* (NULL) — the loop then resumes directly.
*/
typedef int (*xcecli_reconnect_cb)(xcecli_ctx_t ctx, void* arg);
#define N_MAX_MASTERS (3)
typedef struct {
@ -26,6 +40,8 @@ typedef struct {
extern int xcecli_ctx_init(xcecli_ctx_t*, xce_client_config_t*);
extern void xcecli_ctx_fini(xcecli_ctx_t);
extern int xcecli_set_handler(xcecli_ctx_t, xce_request_handler_t, void*);
extern int xcecli_set_reconnect_handler(xcecli_ctx_t, xcecli_reconnect_cb,
void*);
extern int xcecli_start(xcecli_ctx_t);
extern int xcecli_stop(xcecli_ctx_t);
extern int xcecli_wakeup(xcecli_ctx_t, int);
@ -39,6 +55,30 @@ extern int xcecli_workerlist(xcecli_ctx_t, xce_worker_t***);
extern int xcecli_jobdispatch(xcecli_ctx_t, xce_job_dispatch_t*);
extern int xcecli_joblist(xcecli_ctx_t, xce_job_t***);
extern int xcecli_clusterconf(xcecli_ctx_t, xce_clusterconf_t**);
extern int xcecli_ctrlstatus(xcecli_ctx_t, xce_ctrlstatus_t**);
/*
* xcecli_ctrlstatus_primary (controller-status task 3.2): AUTHORITATIVE
* controller status read. Returns ONLY a reply whose responder role is the
* primary. When the first answer is from a non-primary, it follows the redirect
* (targets the reported primary via the reply's peer list, else rotates the
* configured controller list) until the primary answers or the failover budget
* is exhausted. On budget exhaustion / no reachable primary it returns a typed
* negative and NEVER hands back a non-primary hint reply (Req 9.1, 9.2). The
* any-node hint view is still available via xcecli_ctrlstatus above.
*/
extern int xcecli_ctrlstatus_primary(xcecli_ctx_t, xce_ctrlstatus_t**);
/*
* Pure decision helpers backing the authoritative read (no I/O — unit-testable
* and exposed for tests). xcecli_ctrlstatus_is_authoritative returns true iff
* the reply's responderRole is the primary role (ctrl_role_t PRIMARY == 0).
* xcecli_ctrlstatus_primary_addr maps the reply's primaryName to its host:port
* via the reply's peers list: returns 0 and fills host/port on a hit, -ENOENT
* when the primary is unknown/absent from peers, -EINVAL on a bad argument.
*/
extern bool xcecli_ctrlstatus_is_authoritative(const xce_ctrlstatus_t*);
extern int xcecli_ctrlstatus_primary_addr(const xce_ctrlstatus_t*,
char* host_out, size_t host_sz,
uint16_t* port_out);
extern int xcecli_notify_jobfinish(xcecli_ctx_t, xce_jobfinish_t*);
extern int xcecli_notify_jobstart(xcecli_ctx_t, job_pidinfo_t*);
extern int xcecli_notify_jobfail(xcecli_ctx_t c, job_failinfo_t*);
@ -74,6 +114,10 @@ extern int xcecli_jobinfo(xcecli_ctx_t, uint64_t, xce_job_t**);
#define xcecli_free_queues(nqueues, queues) xce_free_queues(nqueues, queues)
#define xcecli_free_groups(ngroups, groups) xce_free_groups(ngroups, groups)
#define xcecli_free_clusterconf(conf) xce_free_clusterconf(conf)
/* ctrlstatus reply is freed by the generated codec free (free_CtrlStatus,
* declared in the generated controller.capnp.h). */
#define xce_free_ctrlstatus(s) free_CtrlStatus(s)
#define xcecli_free_ctrlstatus(s) xce_free_ctrlstatus(s)
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**);

View file

@ -37,7 +37,7 @@ typedef struct {
int inactive_heartbeat_count;
int fetch_job_interval;
int netio_timeout;
int vcpus;
int vcpus; /* deprecated: use --vcpus CLI flag; kept for config blob backward compatibility */
int nres;
xce_resource_t** resources;
int nqueues;
@ -57,6 +57,7 @@ typedef struct {
scaler_config_t** hproviders;
int nlimits;
xce_limit_config_t** limits;
uint64_t config_generation; /* monotonic audit counter from capnp schema */
} cluster_config_t;
#endif

View file

@ -71,4 +71,27 @@ typedef uint64_t jobid_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)
* ====================================================== */
#define MAX_STANDBYS 3
#define MAX_HA_CANDIDATES 4
#define WAL_FORMAT_VERSION 1
#define WAL_ENTRY_TYPE_MAX 100 /* WAL_CHECKPOINT = 100, highest known type */
#define MAX_WAL_PAYLOAD_BYTES (1 << 20) /* 1 MB per replication entry */
#define MAX_SNAPSHOT_BYTES (256 * 1024 * 1024) /* 256 MB max snapshot size */
#define WAL_HDR_CRC_SIZE 32 /* sizeof(wal_entry_hdr_t) - 2*sizeof(uint32_t) */
#define WAL_CRC_NONE 0xFFFFFFFFu /* sentinel: no predecessor in chain */
#define DEFAULT_WAL_SYNC_MS 10
#define DEFAULT_SNAP_INTERVAL_S 60
#define DEFAULT_SNAP_WAL_SIZE_MB 64
#define DEFAULT_SNAP_KEEP_N 2
#define DEFAULT_TRANSIENT_JOB_TIMEOUT_S 300
#define DEFAULT_FINISHED_RETAIN_S 3600
#define DEFAULT_WORKER_RETAIN_S 86400
#define DEFAULT_SYNC_TIMEOUT_MS 500
#define DEFAULT_HB_INTERVAL_MS 3000
#define DEFAULT_MAX_WAL_RECORDS 10000
#endif

View file

@ -0,0 +1,47 @@
#pragma once
/*
* ctrl_metrics.h — P3-7: Operational metrics for the XCE controller.
*
* All metrics use C11 _Atomic uint64_t for lock-free, thread-safe access.
* Metric names use the ctrl_ prefix.
*
* Design doc: Part P3-7, review A8-007.
*/
#include <stdatomic.h>
#include <stdint.h>
/* ── Counter/gauge globals ──────────────────────────────────────────── */
extern _Atomic uint64_t ctrl_repl_degraded_count; /* counter */
extern _Atomic uint64_t ctrl_split_brain_detected; /* counter */
extern _Atomic uint64_t ctrl_async_events_lost; /* counter */
extern _Atomic uint64_t ctrl_transient_jobs_stuck; /* counter */
extern _Atomic uint64_t ctrl_wal_bytes; /* gauge */
extern _Atomic uint64_t ctrl_checkpoint_duration_ms; /* gauge */
extern _Atomic uint64_t ctrl_repl_lag_lsn; /* gauge */
/* ── Macros ─────────────────────────────────────────────────────────── */
/* Increment a monotonic counter by 1 (relaxed ordering — counters are
* sampled, not used for synchronization). */
#define CTRL_METRIC_INC(name) \
atomic_fetch_add_explicit(&(name), 1, memory_order_relaxed)
/* Overwrite a gauge with a new absolute value. */
#define CTRL_METRIC_SET(name, val) \
atomic_store_explicit(&(name), (uint64_t)(val), memory_order_relaxed)
/* ── Descriptor table for metric discovery ──────────────────────────── */
typedef struct {
const char* name;
_Atomic uint64_t* value;
const char* description;
int is_counter; /* 1 = counter (monotonic), 0 = gauge */
} ctrl_metric_desc_t;
/* Terminated by a { NULL, NULL, NULL, 0 } sentinel entry. */
extern const ctrl_metric_desc_t ctrl_metric_descriptors[];
/* ── Lifecycle ──────────────────────────────────────────────────────── */
/* Reset all metrics to zero. Call from persist_start() and ha_init(). */
void ctrl_metrics_init(void);

View file

@ -14,6 +14,7 @@ typedef int (*task_run_fn_t)(delay_task_entry_fn_t, void*);
typedef struct {
time_t nextrun;
struct timespec nextrun_ts; /* non-zero tv_nsec flags an ms-task */
int run_period;
int runcount;
delay_task_entry_fn_t entry;
@ -23,7 +24,7 @@ typedef struct {
typedef struct {
pthread_t worker;
int running;
int precision;
int precision_ms;
task_run_fn_t runner_fn;
timer_t timerid;
ordq_t tasks;
@ -34,5 +35,6 @@ extern void delaymgr_stop(delaymgr_t*);
extern void delaymgr_fini(delaymgr_t*);
extern int delaymgr_start(delaymgr_t*);
extern int delay_task(delaymgr_t*, int, int, delay_task_entry_fn_t, void*);
extern int delay_task_ms(delaymgr_t*, int, int, delay_task_entry_fn_t, void*);
#endif

View file

@ -19,5 +19,6 @@ extern void* xce_notify_data_ptr(xce_notify_data_t*);
extern void* xce_jobctrl_data_ptr(xce_jobctrl_t*);
extern void* xce_reply_data_ptr(xce_reply_t*);
extern char* xce_dataset_table(int);
extern char* xce_notify_event_name(int);
#endif

View file

@ -3,6 +3,7 @@
#define __HOOK_H__ 1
#include <stdarg.h>
#include <stdatomic.h>
#include <stddef.h>
#include "xlist.h"
@ -12,8 +13,8 @@
typedef int (*hook_fn_t)(int, int, int, void*, va_list);
typedef struct {
hook_fn_t fn;
void* data;
_Atomic hook_fn_t fn;
void* data;
} hook_item_t;
typedef struct {
@ -23,6 +24,7 @@ typedef struct {
extern int xce_hook_init(hooksys_t*, int);
extern int xce_add_hook(hooksys_t*, int, hook_fn_t, void*);
extern int xce_disable_hook(hooksys_t*, int, hook_fn_t);
extern int xce_run_hooks(hooksys_t*, int, int, int, ...);
extern void xce_hook_fini(hooksys_t*);

View file

@ -30,4 +30,22 @@ extern kv_key_ops_t int_key_ops;
extern kv_key_ops_t string_key_ops;
extern int key_cmp_default(void*, void*, size_t);
/*
* kvtab_foreach — iterate every live entry in the kvtab (P0-14).
*
* For each entry, cb(key, index, arg) is called.
* - key : the raw key pointer stored in the entry (char* for
* string-keyed tables, (void*)(long)intval for int-keyed).
* - index : the slot index stored as the entry value.
* - arg : caller-supplied context.
*
* Iteration order is unspecified.
* Returns the number of entries visited, or -EINVAL if kvtab is NULL.
*/
typedef void (*kvtab_foreach_fn_t)(void* key, int index, void* arg);
extern int kvtab_foreach(var kvtab,
kvtab_foreach_fn_t cb,
void* arg);
#endif

View file

@ -7,6 +7,7 @@
#include "xce/types.h"
#include "xlist.h"
#include "zarray.h"
#include "persist.h"
typedef enum {
KVMGR_KEY_INT = 0,
@ -22,4 +23,22 @@ typedef struct {
extern typeinfo_t xkvmgr_type;
/*
* kvmgr_capture_keys — capture key→index mappings for checkpoint
* serialization (Part 3.8).
*
* Caller MUST hold the kvmgr write lock for the full duration of
* this call. The function allocates *keys_out and populates it with
* *n_keys_out entries. Each entry's key[] field is an owned deep
* copy that remains valid after the write lock is released.
*
* Caller must free(*keys_out) when done.
*
* Returns 0 on success.
* Returns -ENOMEM if allocation fails.
*/
int kvmgr_capture_keys(var kvmgr,
slot_key_t** keys_out,
size_t* n_keys_out);
#endif

View file

@ -4,9 +4,16 @@
#define MAX_MASTER_CANDIDATES (64)
#include <time.h>
#include <stdint.h>
#include <tlog.h>
#include "clusterconfig.h"
#include "xce/cluster.h"
/* Forward declaration — avoids circular include with cluster_def.h */
typedef struct cluster_s cluster_t;
struct logging {
char* logdir;
@ -14,13 +21,30 @@ struct logging {
};
typedef struct {
cluster_config_t* cluster_config;
char* conffile;
char* certfile;
char* keyfile;
cluster_config_t* cluster_config;
char* conffile;
char* certfile;
char* keyfile;
char* workdir; /* working directory for WAL/snapshot storage */
char* name; /* controller name (hostname by default) */
xce_cluster_config_t peers_cfg; /* HA peer list parsed from --peers; count==0 = single-node */
uint64_t config_version_tag; /* first 8 bytes of SHA-256, host order */
uint8_t config_sha256[32]; /* full SHA-256 of conffile bytes */
int conf_keep_n; /* versioned file retention count (default 2) */
uint64_t config_generation; /* monotonic audit counter */
time_t conf_stale_timeout; /* seconds before saved config flagged stale (default 86400) */
int conf_stale; /* 1 when saved config older than conf_stale_timeout */
int vcpus; /* Req 4.3: --vcpus CLI value; 0 = use config default */
uint32_t sync_timeout_ms; /* --sync-timeout-ms; 0 = use persist default */
uint32_t wal_sync_ms; /* --wal-sync-ms; 0 = use persist default */
} myconf_t;
extern cluster_config_t* get_default_conf(void*);
extern int add_config(void*, char*, char*);
void myconf_fini(myconf_t *myconf);
int load_primary_config(cluster_t *c);
int fetch_config_from_primary(cluster_t *c);
#endif

View file

@ -64,7 +64,7 @@ typedef struct {
size_t iocount;
} io_fsm_t;
typedef struct {
typedef struct endpoint_t {
mylock_t lock;
iomgr_t* iomgr;
SSL* ssl;
@ -85,6 +85,7 @@ typedef struct {
err_handler_t err_handler;
time_t ts_last_in;
time_t ts_last_out;
void (*on_connected)(struct endpoint_t *ep); /* fire-once; called in iomgr thread after ssl_handshake_done=1 */
} endpoint_t;
#define INVOKE_MSG_HANDLER(ep) \
@ -126,6 +127,7 @@ extern int endpoint_is_task_blocked(endpoint_t*);
extern int endpoint_set_owner(endpoint_t*, void*);
extern int endpoint_set_msg_handler(endpoint_t*, msg_handler_t);
extern int endpoint_set_err_handler(endpoint_t*, err_handler_t);
extern int endpoint_set_on_connected(endpoint_t*, void (*cb)(endpoint_t *));
extern void endpoint_fini(endpoint_t*);
extern int endpoint_set_peer(endpoint_t*, xce_peerinfo_t*);

638
src/include/persist.h Normal file
View file

@ -0,0 +1,638 @@
#pragma once
#include <pthread.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "config.h"
#include "wal_types.h" /* wal_entry_hdr_t, wal_entry_type — P2-4: shared with repl.h */
#include "repl.h" /* replicator_t — P2-4: embedded in persist_t */
/* Forward declaration */
typedef struct cluster_s cluster_t;
/* ============================================================
* Recovery-integrity verdict (ha-commit-durability task 3.1, Req 2.1)
*
* The outcome of WAL replay at startup, distinguishing a structurally
* intact log from one whose chain is broken. It is about the STRUCTURE
* of the local log, NOT about being behind the cluster: a clean log that
* simply ends below the cluster's committed high-water mark is still
* RECOVER_CLEAN (the freshness/promotion fence handles "behind"). A
* corruption-truncated recovery is NEVER reported as a clean recovery.
* ============================================================ */
typedef enum {
RECOVER_CLEAN = 0, /* WAL forms a valid CRC chain to a clean entry
* boundary: clean EOF, pre-allocated-zero tail,
* snapshot-sentinel + clean replay, or an empty
* first-boot WAL (no entries != corruption). */
RECOVER_TRUNCATED = 1, /* replay stopped on structural corruption: a CRC
* chain break, a CRC mismatch, or a short/partial
* entry read — the log cannot be reconstructed
* contiguously. Marks the member non-promotable
* (consumed by the promotion gate, task 4.3). */
} recover_verdict_t;
/* ============================================================
* Boot-policy recovery decision (ha-commit-durability task 3.3, Req 2.4)
*
* The outcome of evaluating a member's boot/promotion-eligibility state
* against the Boot Policy Decision Table (design.md "Boot Policy Decision
* Table (task 3.2)"). This task (3.3) owns the FATAL terminal; the fence +
* catch-up + release path is task 4.3.
*
* ELIGIBLE — may promote (no fence). Rows 1, 6.
* FENCE — ineligible to promote; catch up from a healthy peer, regain
* eligibility on success (Req 2.6). Rows 2, 3, 5. A reachable
* healthy peer NEVER yields FENCE_UNRECOVERABLE.
* FENCE_UNRECOVERABLE — loud, PERMANENT, NON-terminating fence: the member
* cannot recover locally AND cannot catch up (no healthy peer).
* It logs LOUDLY, stays PERMANENTLY non-promotable, and keeps
* retrying catch-up indefinitely — but the process STAYS ALIVE.
* Rows 4, 4b. This is NOT process exit: a controller MUST NEVER
* self-terminate as a designed response to an operational
* condition (user invariant, 2026-06-18). A self-exited controller
* needs MANUAL admin recovery and leaves quorum degraded
* meanwhile; a controller that stays alive and keeps retrying
* SELF-HEALS when a peer returns. The fence RELEASES (→ ELIGIBLE,
* Req 2.6) automatically when catch-up reaches a
* committed-consistent state.
*
* AUTHORITATIVE INVARIANT (design.md): a truncated/behind member resolves to
* either FENCE→ELIGIBLE (caught up) or a loud PERMANENT FENCE_UNRECOVERABLE
* (stays alive, non-promotable, keeps retrying). It is NEVER silently divergent
* (it refuses to lead) and the process NEVER self-exits.
* ============================================================ */
typedef enum {
RECOVER_DECISION_ELIGIBLE = 0,
RECOVER_DECISION_FENCE = 1,
RECOVER_DECISION_FENCE_UNRECOVERABLE = 2,
} recover_decision_t;
/* Policy inputs evaluated at boot / promotion-eligibility (design.md table).
* Modelled as a plain value so the decision is a pure, unit-testable function
* of its inputs — the live catch-up machinery (replication / task 4.3) supplies
* these; this task does not build it. */
typedef struct {
recover_verdict_t verdict; /* RECOVER_CLEAN / RECOVER_TRUNCATED (task 3.1) */
bool durable_below_hwm; /* local durable position < cluster committed HWM */
bool healthy_peer_reachable; /* a healthy peer is reachable for catch-up */
bool catch_up_succeeded; /* catch-up reached a committed-consistent state
* within the bounded window (only meaningful
* when a catch-up was attempted) */
uint64_t durable_floor_lsn; /* highest intact replayed LSN (truncation boundary) */
uint64_t cluster_hwm_lsn; /* cluster committed high-water mark, for logging */
} recover_policy_inputs_t;
/* ============================================================
* Persistence configuration (Part 2.6)
* ============================================================ */
typedef struct {
uint32_t wal_sync_ms; /* fsync interval in ms (default 10) */
uint32_t snap_interval_s; /* checkpoint interval in seconds (default 60) */
uint32_t snap_wal_size_mb; /* checkpoint when WAL exceeds this MB (default 64) */
uint32_t snap_keep_n; /* snapshots to retain (default 2) */
uint32_t transient_job_timeout_s; /* seconds before TRANSIENT job auto-terminates (default 300) */
uint32_t finished_retain_s; /* retain finished jobs N seconds (default 3600) */
uint32_t worker_retain_s; /* retain unavailable workers N seconds (default 86400) */
uint32_t sync_timeout_ms; /* sync replication ACK timeout ms (default 50) */
uint32_t max_wal_records; /* checkpoint trigger: records since last snap (default 10000) */
} persist_config_t;
static inline persist_config_t persist_config_defaults(void)
{
persist_config_t c;
c.wal_sync_ms = DEFAULT_WAL_SYNC_MS;
c.snap_interval_s = DEFAULT_SNAP_INTERVAL_S;
c.snap_wal_size_mb = DEFAULT_SNAP_WAL_SIZE_MB;
c.snap_keep_n = DEFAULT_SNAP_KEEP_N;
c.transient_job_timeout_s = DEFAULT_TRANSIENT_JOB_TIMEOUT_S;
c.finished_retain_s = DEFAULT_FINISHED_RETAIN_S;
c.worker_retain_s = DEFAULT_WORKER_RETAIN_S;
c.sync_timeout_ms = DEFAULT_SYNC_TIMEOUT_MS;
c.max_wal_records = DEFAULT_MAX_WAL_RECORDS;
return c;
}
/*
* persist_config_set_wal_sync_ms — apply the --wal-sync-ms flush-cadence
* override (ha-commit-durability Req 3.1).
*
* Single override point shared by both persist_config_defaults() callers
* (cluster.c phase2 and myapp.c step 6), mirroring the --sync-timeout-ms
* convention: override_ms == 0 means "flag not passed" and leaves the
* default flush cadence untouched (Req 3.3 — the seam changes timing
* controllability only, not the durability contract). A positive value
* sets the cadence to exactly that, applied before persistence starts.
*/
static inline void persist_config_set_wal_sync_ms(persist_config_t *c,
uint32_t override_ms)
{
if (override_ms > 0)
c->wal_sync_ms = override_ms;
}
/* ============================================================
* Key→index mapping for checkpoint serialization (Part 3.8)
* ============================================================ */
typedef struct {
int index; /* shadow slot index in the frozen zarray */
char key[XCE_MAXNAME_LEN]; /* owned deep copy — not a borrowed kvtab pointer */
size_t key_len; /* length including null terminator */
} slot_key_t;
/* ============================================================
* persist_t — concrete definition (exposed for stack/calloc allocation)
* The opaque pattern can be restored in a later refactor once the
* full API surface is stable.
* ============================================================ */
struct persist_s {
/* Back-pointer to owning cluster (set by persist_init, used in persist_start) */
cluster_t* cluster;
/* WAL state */
int wal_fd;
uint64_t current_lsn;
uint32_t last_crc;
uint32_t last_replicated_crc;
uint64_t term;
uint32_t seg_offset;
uint64_t checkpoint_lsn;
_Atomic uint64_t committed_lsn; /* highest LSN known committed; invariant: <= current_lsn - 1 */
_Atomic uint64_t durable_lsn; /* highest LSN whose bytes are fdatasync'd locally (group-commit
* prefix watermark). Advanced ONLY by the flusher after a
* successful fdatasync. Invariants: monotonic non-decreasing
* (except clamp-on-truncate); durable_lsn <= current_lsn - 1.
* The commit majority counts self at this LSN, not the
* appended tip (ha-commit-durability Req 1.1/1.2; consumed by
* task 2.2). */
uint64_t total_wal_bytes;
char seg_path[4096];
/* Per-LSN recovered/live term index (ha-term-safety task 1.3, R1.5).
* IN-MEMORY ONLY — no on-disk layout change (R1.7). Records each entry's
* hdr.term keyed by LSN so the term can be read back after recovery and
* fed to the reconcile/promotion logic (on_get_entry_term, task 2.2).
*
* term_index[lsn - 1] holds the RAW recorded term for a 1-based LSN (LSN 0
* is reserved and never indexed). term_index_count is the highest LSN
* recorded (== number of contiguous slots in use), used as the presence
* watermark so persist_entry_term distinguishes an ABSENT LSN (returns 0)
* from a PRESENT legacy-0 entry (mapped to the lowest-known term). Grown on
* demand by record_entry_term during replay_wal and live appends; freed in
* persist_fini. Guarded by p->lock (written under the append lock and the
* single-threaded recovery path; read by accessors that take p->lock). */
uint64_t* term_index; /* term_index[i] = term of LSN (i+1) */
uint64_t term_index_count; /* highest recorded LSN (slots in use) */
uint64_t term_index_cap; /* allocated slot capacity */
/* Durable committed-config reference carried in the checkpoint sentinel
* (ha-term-safety task 4.2 remediation r1 — compaction-safe config derive).
* A checkpoint trims WAL segments older than the open one (trim_wal_segments)
* and the snapshot sentinel does NOT carry entity state, so a committed
* WAL_CONFIG_MARKER in a trimmed segment would vanish and the derived
* committed config (config_committed_marker) would regress. To prevent that,
* checkpoint stamps the latest-committed marker's 40-byte REFERENCE
* ({version_tag, sha256} — a reference, NOT the config bytes / entity
* snapshot) into the sentinel; recovery reads it back here; and the derive
* falls back to this when no marker survives the live WAL scan.
* cfgref_present == 0 means "no committed config marker was known at the last
* checkpoint" (a fresh store or a pre-config-marker checkpoint). Written
* single-threaded at checkpoint/recovery; read by config_committed_marker. */
int cfgref_present; /* 1 if cfgref_tag/sha are valid */
uint64_t cfgref_tag; /* latest committed marker version_tag */
uint8_t cfgref_sha[32]; /* latest committed marker content hash */
/* Recovery-integrity verdict (ha-commit-durability task 3.1, Req 2.1).
* Set by persist_recover/replay_wal at startup: RECOVER_CLEAN when the
* local WAL forms a valid CRC chain to a clean entry boundary (incl. an
* empty first-boot WAL, a snapshot-sentinel + clean replay, and a
* pre-allocated-zero tail), RECOVER_TRUNCATED when replay stopped on a
* structural corruption — a CRC chain break, a CRC mismatch, or a
* short/partial entry read — so the on-disk log cannot be reconstructed
* contiguously. A node that cleanly ends BELOW the cluster high-water is
* still CLEAN here (the freshness/promotion fence catches "behind", task
* 4.2/4.3 — not this verdict). 3.1 only CLASSIFIES + sets this; it does
* NOT FATAL (task 3.3) and does NOT fence promotion (task 4.3, which
* CONSUMES this verdict). Defaults to RECOVER_CLEAN before recovery runs
* (a fresh store is clean). Written single-threaded during recovery
* before the node participates; read by the promotion gate thereafter. */
recover_verdict_t recovery_verdict;
/* 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
* recovery-truncated AND/OR durable below the cluster committed high-water
* mark with NO successful catch-up: the member stays ALIVE but is held
* PERMANENTLY non-promotable and keeps retrying catch-up. A controller MUST
* NEVER self-exit as a designed response to this operational condition; it
* self-heals automatically when a healthy peer returns and catch-up reaches
* a committed-consistent state — at which point this flag is CLEARED via
* persist_clear_recovery_unrecoverable (wired into the fence-release /
* catch-up path). Read by the promotion gate as belt-and-suspenders
* (a member must never lead while this is set). Atomic: set on the recovery
* path, cleared on the catch-up path, read by the promotion/heartbeat
* threads. Defaults to false before recovery runs. */
_Atomic bool recovery_unrecoverable;
/* Paths (derived from workdir + name via persist_set_workdir) */
char wal_path[4096];
char snap_path[4096];
/* Config */
persist_config_t config;
/* Concurrency */
pthread_mutex_t lock;
atomic_uint wal_record_count; /* records appended since last checkpoint */
atomic_int checkpoint_in_progress; /* CAS guard: 0=idle, 1=scheduled/running */
atomic_int fsync_stop;
atomic_int fsync_thread_running;
#ifdef XCE_TESTING
/* Fault-injection seam (ha-commit-durability task 2.1 error-path test):
* when non-zero, every fdatasync site treats the sync as FAILED — it
* still calls the real fdatasync but forces a non-zero rc, so durable_lsn
* 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;
/* 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
* before the caller can ACK). A persist-before-ACK regression test reads
* this to prove a durable sync ran inside the call — page-cache re-reads
* cannot distinguish fsync'd from cache-only, so a direct sync signal is
* required. Compiled out of production. */
atomic_uint test_replicated_fdatasync_count;
/* Liveness observability (ha-commit-durability task 5.1, Req 1.6):
* the thread id of the last flusher fdatasync that advanced durable_lsn.
* A test reads this to prove fdatasync runs on the dedicated flusher
* thread, NOT on the caller/iomgr/heartbeat thread. Compiled out of
* production. */
pthread_t test_last_fdatasync_thread;
atomic_int test_last_fdatasync_thread_set;
#endif
pthread_t fsync_thread;
pthread_mutex_t fsync_mutex; /* guards the fsync timer wait */
pthread_cond_t fsync_cond; /* signaled by persist_stop for prompt join */
/* Durable-advance wake hook (ha-commit-durability task 2.2).
* Persistence owns this slot; the commit engine REGISTERS a callback
* (ctx = the commit_engine_t) via persist_set_durable_cb. The flusher
* invokes it AFTER advancing durable_lsn and AFTER releasing p->lock,
* so a COMMIT_MODE_WAIT caller is woken promptly when its entry becomes
* durable locally — without it, a commit whose standby ACKs all arrived
* before the local fdatasync would stall until the WAIT timeout. The
* dependency direction is preserved (commit depends on persist, never
* the reverse): persist only stores+calls an opaque function pointer.
* NULL until registered (every flush is then a no-op wake). */
void (*on_durable_advance)(void* ctx);
void* durable_cb_ctx;
/* Checkpoint (stubs for now — wired in Task 10) */
atomic_int checkpoint_requested;
pthread_cond_t checkpoint_done;
pthread_mutex_t checkpoint_mutex;
pthread_t snap_thread;
/* Fan-out replicator — P2-4 */
replicator_t repl;
};
typedef struct persist_s persist_t;
/* Set workdir and controller name before persist_init().
* Derives wal_path and snap_path as <workdir>/<name>/data/{wal,snap}. */
void persist_set_workdir(persist_t* p, const char* workdir, const char* name);
/* ============================================================
* WAL term adapter (ha-term-safety task 1.3, Req 1.1/1.5/1.6/1.7/1.8)
*
* Today every WAL append stamps hdr.term = p->term, but p->term is never
* assigned, so all entries carry term 0 (the G1 bug). The on-disk
* wal_entry_hdr_t already has a 40-byte `term` field; this is a VALUE fix, NOT
* a layout change (R1.7).
* ============================================================ */
/* Legacy-zero-term rule (R1.8) — REVIEW ISSUE 2, made precise:
*
* A WAL entry that predates this feature carries hdr.term == 0. For (term,lsn)
* matching it must map to a SINGLE, well-defined "lowest-known term" so that
* (a) recovery never fails on legacy-0 entries, and
* (b) a legacy-0 entry is never spuriously treated as divergent from another
* legacy-0 entry at the same LSN.
* The rule: legacy-0 -> WAL_LEGACY_TERM (1, the lowest real Raft term — Raft
* terms start at 1; term 0 is the pre-election "no term" placeholder). It is
* applied UNIFORMLY at read-back (persist_entry_term), so every legacy-0 entry
* answers the same term 1 and matches another legacy-0 entry at the same LSN.
* The raw 0 is still stored in the index (so "present legacy-0" stays
* distinguishable from "absent"); only the read-back value is mapped. */
#define WAL_LEGACY_TERM 1u
/* Set the controller's live election term so subsequent WAL appends stamp it
* into hdr.term (R1.1/R1.6). Called from the append callback (on_persist_entry,
* task 2.2) and on term adoption, so no append records a zero placeholder while
* a valid term is in effect. No on-disk layout change; value fix only (R1.7).
* Safe on a NULL p (no-op). */
void persist_set_term(persist_t* p, uint64_t term);
/* Recovered/live per-entry term for a 1-based LSN (lsn == Raft index), feeding
* the task-2.2 on_get_entry_term callback and the library's init restore
* (R1.5). Returns 0 when the LSN is ABSENT (never recorded). A PRESENT entry
* that carries a legacy zero term is mapped to WAL_LEGACY_TERM (R1.8) so it is
* never confused with an absent LSN and matches other legacy-0 entries.
* Safe on a NULL p (returns 0). */
uint64_t persist_entry_term(const persist_t* p, uint64_t lsn);
/* Lifecycle */
int persist_init(persist_t* p, persist_config_t* config, cluster_t* c);
int persist_start(persist_t* p);
void persist_stop(persist_t* p);
void persist_fini(persist_t* p);
int persist_recover(persist_t* p);
/* WAL write paths */
int persist_wal_async(persist_t* p, uint32_t type,
const void* payload, uint32_t len);
int persist_wal_sync_append(persist_t* p, uint32_t type,
const void* payload, uint32_t len,
int* sync_rc_out);
/* Write a REPLICATED entry (received from the primary) into the local WAL
* at its GIVEN lsn — used by standbys to durably persist streamed entries
* BEFORE acking them (persist-before-ACK; an ACK must imply durability).
*
* Unlike persist_wal_async it does NOT assign a new lsn (the lsn is supplied
* by the primary) and does NOT repl_broadcast (standbys never re-replicate).
* It builds a SELF-consistent CRC chain (the streamed entry carries no crc),
* fsyncs the segment before returning, and is idempotent on re-stream: if the
* lsn is already persisted (lsn+1 <= current_lsn) it is a no-op returning 0.
*
* WAL_CHECKPOINT is rejected with -EINVAL (never streamed to standbys).
* Returns 0 on success (incl. dedup no-op), negative errno on failure. */
int persist_wal_write_replicated(persist_t* p, uint64_t lsn, uint32_t type,
const void* payload, uint32_t len);
/* ============================================================
* Write-path commit classification (ha-commit-barrier-rollout)
* ============================================================ */
typedef enum {
WAL_MODE_WAIT, /* request-shaped: barrier before requester reply */
WAL_MODE_RETAIN, /* fact-shaped: retained until committed, no timeout */
WAL_MODE_LOCAL, /* not replicated (WAL_CHECKPOINT) */
} wal_commit_mode_t;
/* Pure classification lookup. detail disambiguates WAL_WORKER_CTRL only
* (HOOK_DETAIL_WORKER_JOIN/UPDATE -> WAIT, HOOK_DETAIL_WORKER_DEAD ->
* RETAIN). Unknown type: XCE_TESTING assert (fail fast); production logs
* ERROR and returns WAL_MODE_RETAIN (never dropped, never confirmed early). */
wal_commit_mode_t wal_classify(uint32_t wal_type, int detail);
/* Commit barrier — handler-callable WAIT completion on everything appended
* so far (ha-commit-barrier-rollout). Snapshots the appended watermark
* (current_lsn - 1, under p->lock — matching persist_wal_sync_append's
* read); returns 0 without blocking when committed_lsn already covers it
* (or nothing was ever appended). Otherwise delegates to
* commit_submit(COMMIT_MODE_WAIT) and returns its code unmodified:
* 0 — committed on a majority
* -ETIMEDOUT — not committed within the deadline (entries remain
* in the log; outcome converges per umbrella 2.5)
* -XCE_ERR_NOT_READY — engine admission cap exhausted (retriable)
* timeout_ms = 0 falls back to p->config.sync_timeout_ms. n_peers <= 1
* returns 0 immediately (engine self-quorum, standalone preserved).
* Call only from vtask threads with no entity locks held. */
int persist_commit_barrier(persist_t* p, uint32_t timeout_ms);
/* Force WAL fdatasync (for tests and clean shutdown) */
int persist_flush(persist_t* p);
/* Highest LSN whose bytes are durably fdatasync'd locally — the
* group-commit prefix watermark advanced by the demand-driven flusher
* (ha-commit-durability task 2.1). The commit majority self-contribution
* caps at this value (task 2.2 consumer). Lock-free atomic read; safe from
* any thread. Returns 0 when nothing is yet durable. */
uint64_t persist_durable_lsn(persist_t* p);
/* Durable freshness floor (ha-commit-durability task 3.4, Req 2.3).
*
* The member's durably-backed last-log index (WAL tip): the highest LSN that
* is on stable storage locally. persist_recover re-derives it from the
* recovered WAL tip (the highest INTACT replayed LSN) so the post-recovery
* value is backed by durable on-disk state, NOT a volatile in-memory
* applied_lsn / last_known_committed_lsn. The Raft freshness gate (task 4.2,
* ha.c / cluster.c) consumes this so a truncated member cannot over-advertise
* a tip it does not durably hold.
*
* Precision: this is the durable LAST-LOG INDEX, NOT the commit position
* (committed_lsn). The post-election safe-commit point still comes from voter
* replies, never from this floor (CLAUDE.md). After a truncating restart the
* floor equals the lower intact tip, never a stale higher in-memory value and
* never the commit position.
*
* Thin wrapper over persist_durable_lsn: at and after recovery the durable
* watermark IS the recovered WAL tip (recovered on-disk entries are genuinely
* durable), so a separate field would be redundant. Lock-free atomic read;
* safe from any thread. Returns 0 when nothing is durable (empty first-boot
* WAL). */
uint64_t persist_durable_floor(persist_t* p);
/* Recovery-integrity verdict + flag (ha-commit-durability task 3.1, Req 2.1).
* 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. */
recover_verdict_t persist_recovery_verdict(persist_t* p);
bool persist_recovery_truncated(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).
* The interval after which a still-unrecoverable, loudly-fenced member RE-EMITS
* its operator alert (it does NOT terminate — see RECOVER_DECISION_FENCE_-
* UNRECOVERABLE). The member stays alive and keeps retrying catch-up
* indefinitely; this window only controls how often the loud operator-visible
* alert is repeated. Default 30s. */
#define DEFAULT_RECOVERY_CATCHUP_WINDOW_MS 30000u
/* persist_recover_decision (ha-commit-durability task 3.3, Req 2.4).
*
* Pure decision over the Boot Policy Decision Table: maps the policy inputs to
* RECOVER_DECISION_{ELIGIBLE,FENCE,FENCE_UNRECOVERABLE}. No side effects, no
* termination — it only classifies, so it is unit-testable in isolation. The
* FENCE_UNRECOVERABLE terminal is acted on by
* persist_recover_loud_fence_if_unrecoverable (which logs LOUDLY + sets the
* sticky non-promotable flag — it NEVER terminates the process).
*
* Contract (design.md rows): a reachable healthy peer NEVER returns
* FENCE_UNRECOVERABLE (rows 3, 5); it is returned ONLY when the member is
* truncated or behind the HWM, has NO healthy peer, and catch-up did not reach
* a committed-consistent state (rows 4, 4b). */
recover_decision_t persist_recover_decision(const recover_policy_inputs_t* in);
/* persist_recover_loud_fence_if_unrecoverable (ha-commit-durability task 3.3,
* Req 2.4; never-self-exit rework 2026-06-18).
*
* Evaluate persist_recover_decision over `in`; if the outcome is
* FENCE_UNRECOVERABLE, log LOUDLY at ERROR — INCLUDING the truncation boundary
* (durable_floor_lsn) and the cluster high-water mark — stating the member is
* staying fenced + non-promotable and will keep retrying catch-up, then SET the
* sticky `recovery_unrecoverable` flag on `p` (so the promotion gate refuses
* leadership as belt-and-suspenders). Returns the decision.
*
* CRITICAL — the process STAYS ALIVE. There is NO abort()/exit()/tlog_exit()
* here: a controller MUST NEVER self-terminate as a designed response to an
* operational condition (user invariant, 2026-06-18). The member self-heals —
* the fence releases (→ ELIGIBLE, Req 2.6) and the flag clears via
* persist_clear_recovery_unrecoverable — automatically when a healthy peer
* returns and catch-up reaches a committed-consistent state.
*
* `p` may be NULL (pure-decision callers that only want the classification +
* loud log without touching a persist_t); the flag set is then skipped. */
recover_decision_t
persist_recover_loud_fence_if_unrecoverable(persist_t* p,
const recover_policy_inputs_t* in);
/* persist_recovery_unrecoverable / persist_clear_recovery_unrecoverable
* (ha-commit-durability Req 2.4/2.6, never-self-exit rework 2026-06-18).
*
* Accessor + clear for the sticky "unrecoverable, loudly fenced" flag. The flag
* is SET by persist_recover_loud_fence_if_unrecoverable and CLEARED on the
* catch-up / fence-release path once the member reaches a committed-consistent
* state (not truncated AND durable_floor >= HWM). While set, the member is held
* permanently non-promotable but stays alive. Safe on a NULL p (returns false /
* no-op). */
bool persist_recovery_unrecoverable(persist_t* p);
void persist_clear_recovery_unrecoverable(persist_t* p);
/* Register a durable-advance wake callback (ha-commit-durability task 2.2).
* The group-commit flusher calls fn(ctx) after it advances durable_lsn and
* after releasing p->lock, so a consumer (the commit engine) can re-evaluate
* its commit majority and wake any COMMIT_MODE_WAIT caller whose entry just
* became durable locally. fn MUST NOT take p->lock (it runs after release, on
* the flusher thread) — the commit engine's callback takes only ce->lock,
* keeping the lock order p->lock → ce->lock cycle-free. Pass fn = NULL to
* unregister. Safe to call before persist_start (no flusher running yet). */
void persist_set_durable_cb(persist_t* p, void (*fn)(void* ctx), void* ctx);
#ifdef XCE_TESTING
/* ============================================================
* Durability-verification test seam (ha-commit-durability Req 3.2, 3.3)
*
* persist_test_durable_lsn_on_disk — re-read and CRC-validate the WAL
* segments from STABLE STORAGE and return the highest fully-persisted
* LSN, independent of the in-memory current_lsn / committed_lsn
* watermarks. Lets a test assert that an acknowledged operation is
* recoverable from disk after an immediate, ungraceful termination —
* without relying on the background flush timer (Req 3.2):
*
* persist_wal_async(p, ...); // append to page cache
* persist_flush(p); // fdatasync → durable on disk
* lsn = persist_test_durable_lsn_on_disk(p); // highest durable LSN
*
* It scans the same on-disk format and CRC chain as recovery, stopping
* at the first corruption / short read / pre-allocated zero region, so
* it reports only entries that are genuinely on stable storage. It
* never mutates persist_t state and fires no hooks (Req 3.3 — the seam
* is purely inspective; the acknowledgment contract is unchanged).
*
* Returns the highest durable LSN, or 0 when nothing is durable (empty
* first-boot WAL, no segments). Compiled out of production builds:
* the xce_controller target is built without -DXCE_TESTING and links
* without this symbol.
* ============================================================ */
uint64_t persist_test_durable_lsn_on_disk(persist_t* p);
/* Fault-injection toggle for the fdatasync error path (task 2.1).
* When `fail` is non-zero, every fdatasync site in persist.c forces a
* failed sync result: the real fdatasync still runs, but durable_lsn is
* left UNCHANGED and the failure is logged loudly — proving an acknowledged
* entry is never counted durable on a sync error (Req 1.1/1.2, fail-loud).
* Lock-free atomic write; safe to flip from a test thread at any time.
* Compiled out of production (xce_controller links without it). */
void persist_test_set_force_fsync_fail(persist_t* p, int fail);
/* 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
* asserts it incremented after the call returns — proving the durable sync
* ran inside the persist primitive, BEFORE the caller can send its WalAck.
* Lock-free atomic read; compiled out of production. */
unsigned persist_test_replicated_fdatasync_count(persist_t* p);
/* Invoke the legacy sync-append commit store (task 2.4) with the durable
* precondition applied — the same store persist_wal_sync_append performs on a
* majority ACK in its engine-less / LSN-0 branch. Lets a test prove
* committed_lsn never advances past durable_lsn on the ACK path without
* standing up the replication wire. Compiled out of production. */
void persist_test_sync_append_legacy_commit_store(persist_t* p,
uint64_t written_lsn);
/* Return the thread id of the last flusher fdatasync that advanced
* durable_lsn (task 5.1 / Req 1.6 liveness). A test asserts this is NOT the
* caller/iomgr thread — proving durability (fdatasync) work runs on the
* dedicated flusher thread and never stalls heartbeat/election handling.
* Returns pthread_self() if no flusher sync has advanced durable_lsn yet
* (so an unprimed read can never falsely look "off-thread"). Compiled out of
* production. */
pthread_t persist_test_last_fdatasync_thread(persist_t* p);
/* Close the active WAL segment and open a fresh one at the current LSN
* (ha-term-safety task 4.2 r1 test seam). Forces a multi-segment layout so a
* subsequent persist_checkpoint trims the older segment — exercising the
* compaction hazard where a committed marker's segment is unlinked. Returns 0
* on success, negative errno on I/O failure. Compiled out of production. */
int persist_test_force_new_segment(persist_t* p);
#endif /* XCE_TESTING */
/* Checkpoint */
int persist_checkpoint(persist_t* p);
/* WAL truncation — remove all entries with LSN > cutoff_lsn.
* Idempotent; returns 0 on success, negative errno on I/O error. */
int persist_truncate_wal_above(persist_t* p, uint64_t cutoff_lsn);
/* Logging helper — never logs payload content (security invariant) */
void persist_log_entry_metadata(const wal_entry_hdr_t* hdr);
/* ============================================================
* AFTER_EVENT hook functions — registered by persist_start()
*
* Signature matches hook_fn_t: int fn(int type, int situation,
* int detail, void* data, va_list ap)
* cls/data points to the persist_t instance.
*
* P0: writes a minimal 4-byte payload (detail value) to the WAL.
* Full Cap'n Proto serialization is wired in Task 15.
* ============================================================ */
int persist_job_hook_fun(int type, int situation, int detail,
void* cls, va_list ap);
int persist_queue_hook_fun(int type, int situation, int detail,
void* cls, va_list ap);
int persist_worker_hook_fun(int type, int situation, int detail,
void* cls, va_list ap);
/* Apply a single WAL entry to in-memory state (LOGICAL entity restore only).
* No hooks fire — hook system is empty on standbys until promotion.
*
* This restores entity state from (type, lsn, payload) ONLY. It does NOT touch
* the on-disk WAL header, the CRC chain (p->last_crc), or current_lsn — those
* are owned exclusively by the physical layer: persist_wal_write_replicated on
* the live replicated path, and replay_wal/persist_recover on recovery. (bug-778)
*
* Returns 0 on success (including skip for checkpoint/unknown types),
* negative errno on error (oversized payload, NULL persist). */
int apply_wal_entry(persist_t* p, uint32_t type, uint64_t lsn,
const void* payload, uint32_t payload_len);
/* Load a snapshot file from path, set p->checkpoint_lsn and p->current_lsn.
* lsn hint is informational — the actual LSN is read from the file header.
* Returns 0 on success, -errno or -EINVAL/-EIO on error. */
int persist_load_snapshot(persist_t* p, const char* path, uint64_t lsn);
/* GC retention predicates (P0-5a / P0-8 / P0-13) */
#include <stdbool.h>
#include "xce/types.h"
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);

167
src/include/repl.h Normal file
View file

@ -0,0 +1,167 @@
#pragma once
/*
* repl.h — P2-1: Replicator structures and public API.
* Fan-out TCP replication via existing iomgr/SSL endpoint pipeline.
* ACKs processed in io_worker read callback (NOT vtaskmgr) to prevent deadlock.
*/
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <time.h>
#include "config.h"
#include "mylock.h"
#include "wal_types.h" /* wal_entry_hdr_t — P2-4: use wal_types.h to break circular include */
/* Max queued-but-unacknowledged sends per standby before eviction to catch-up */
#define MAX_PENDING_ACKS 16
/* Endpoint is an opaque pointer — full type in netio.h (avoid heavy include) */
typedef void* repl_ep_t;
/* Commit engine (controller/include/commit.h) — forward-declared so repl.h
* keeps zero controller-header dependencies (ha-commit-engine task 5). */
struct commit_engine;
/* Per-standby connection slot */
typedef struct {
repl_ep_t ep; /* NULL = slot empty */
_Atomic uint64_t acked_lsn; /* Last WAL LSN ACKed (walAck) */
_Atomic uint64_t acked_config_lsn; /* Last config LSN ACKed (upgradeConfigAck) */
_Atomic int in_catch_up; /* Non-zero while catching up */
uint64_t join_applied_lsn; /* LSN standby reported on reconnect */
char host[XCE_MAXNAME_LEN];
uint32_t port;
uint64_t last_disconnected_lsn;
time_t last_disconnect_time;
uint32_t pending_ack_count; /* Sends not yet ACKed; evict if > MAX_PENDING_ACKS */
} standby_conn_t;
/* Replicator — embedded in persist_t or cluster_t (P2-4).
* Named tag replicator_s matches the forward declaration in persist.h. */
typedef struct replicator_s {
standby_conn_t standbys[MAX_STANDBYS];
int nstandbys; /* Active standby count */
mylock_t lock; /* Protects standbys[], nstandbys */
pthread_cond_t ack_cond; /* Signaled when any ACK arrives */
pthread_mutex_t ack_lock; /* Guards ack_cond */
/* Commit engine feed (ha-commit-engine task 5.2). NULL = not wired
* (legacy counting contexts). Set once via repl_set_commit_engine at
* cluster init, before replication traffic — read without r->lock.
* Lock ordering: engine paths take ce->lock then r->lock, so repl.c
* must NEVER call into the engine while holding r->lock. */
struct commit_engine *ce;
} replicator_t;
/* Lifecycle */
int repl_init(replicator_t *r);
void repl_fini(replicator_t *r);
/* Standby management */
int repl_add_standby(replicator_t *r, repl_ep_t ep, const char *host, uint32_t port);
void repl_remove_standby(replicator_t *r, repl_ep_t ep);
void repl_expire_disconnect_slots(replicator_t *r, time_t now_s, uint32_t snap_interval_s);
/* Write-admission quorum accounting (ha-commit-engine Req 3.1-3.3).
* Counts only standbys able to receive new log entries: slots with
* ep != NULL && !in_catch_up, evaluated under r->lock.
* NULL registry counts zero. Consumed by ha_has_write_quorum (ha.c). */
int repl_active_standby_count(replicator_t *r);
/* Replication send (stub in P2-1; wired in P2-4).
*
* ha-term-safety task 3.4 (Req 1.3/1.4): the LIVE primary append replicator.
* Carries the leader's per-entry Raft term (`hdr->term`) and the AppendEntries
* prev tuple (`prev_log_index = hdr->lsn - 1`, `prev_log_term` supplied by the
* caller from its term index) on every broadcast, so the standby intake routes
* the entry through `raft_on_append_entries_request` and persists the LEADER's
* term (not a local substitute or 0). `prev_log_term` is the term recorded for
* `hdr->lsn - 1` (0 when `hdr->lsn == 1`, i.e. no predecessor). The term-0
* legacy direct-apply receive path is thereby retired for live entries. */
int repl_broadcast(replicator_t *r, const wal_entry_hdr_t *hdr,
const void *payload, uint32_t payload_len,
uint64_t prev_log_term);
/* AppendEntries send (ha-term-safety task 2.3, Req 1.3/3.1).
*
* Build and enqueue ONE AppendEntries envelope to the standby at slot
* `peer_idx` (== the library's raft peer index, which maps 1:1 to the repl
* standby registry slot — the same index used by match_index/next_index).
* The envelope carries the AppendEntries prev tuple (`prev_log_index`,
* `prev_log_term`) + `leader_commit`; when `n_entries == 1` it also carries the
* single entry at LSN `prev_log_index + 1` whose `entry_term` is the term the
* leader recorded for that LSN (== persist_entry_term) and whose
* `entry_type`/`payload` are read from the leader's own WAL. `n_entries == 0`
* is a heartbeat (zero entries) carrying only the prev tuple + leader_commit.
*
* Non-blocking: enqueues via the existing async repl transport
* (xce_wal_batch_req_raw), never blocks. Returns 0 on enqueue, or a negative
* errno (e.g. -ENOENT when peer_idx has no live endpoint). */
int repl_send_append_entries(replicator_t *r, int peer_idx,
uint64_t prev_log_index, uint64_t prev_log_term,
uint64_t leader_commit, int n_entries,
uint64_t entry_lsn, uint8_t entry_type,
uint64_t entry_term,
const void *payload, uint32_t payload_len);
/* WAL ACK handling — call from io_worker read callback ONLY */
void repl_on_ack(replicator_t *r, repl_ep_t ep, uint64_t acked_lsn);
/* Config ACK handling — call from io_worker read callback ONLY (task 8.1) */
void repl_on_config_ack(replicator_t *r, repl_ep_t ep, uint64_t acked_config_lsn);
/* Sync replication (stub in P2-1; wired in P2-3) */
int repl_wait_ack(replicator_t *r, uint64_t target_lsn, uint32_t timeout_ms);
/* Majority-quorum ACK wait (task 7.3; ha-commit-engine task 5 adapter;
* configAck track hardened by ha-commit-barrier-rollout task 5.2).
* entry_lsn: walAck — the WAL entry the caller waits on, delegated to the
* commit engine for per-LSN evaluation; configAck — the in-flight config
* push tag, counted as acked_config_lsn >= tag with majority
* 1 (self) + count > n_peers / 2 over configured members (no second
* parameter — the configAck branch reuses this one). */
int repl_wait_majority_ack(replicator_t *r, uint64_t entry_lsn,
uint32_t timeout_ms, uint8_t ack_op);
/* Wire the commit engine into the replicator so ACK and membership events
* are fed to it (ha-commit-engine task 5.2; NULL = not wired). */
void repl_set_commit_engine(replicator_t *r, struct commit_engine *ce);
/* WAL trim guard */
uint64_t repl_trim_lsn(replicator_t *r, uint64_t checkpoint_lsn, uint32_t snap_interval_s);
/* Catch-up state management */
/* Mark standby as entering catch-up mode; stores join_applied_lsn */
void repl_start_catchup(replicator_t *r, repl_ep_t ep, uint64_t join_applied_lsn);
/* Mark standby catch-up complete (called on OP_replCatchUpDone receipt) */
void repl_complete_catchup(replicator_t *r, repl_ep_t ep);
/* Returns 1 if standby needs a full snapshot transfer (its LSN predates oldest WAL).
* oldest_wal_lsn == 0 means no WAL exists — no snapshot needed for LSN 0 standbys. */
int repl_needs_snapshot(replicator_t *r, uint64_t standby_lsn, uint64_t oldest_wal_lsn);
#ifdef XCE_TESTING
/* AppendEntries send capture seam (ha-term-safety task 2.3).
*
* When `repl_send_ae_capture_hook` is non-NULL, repl_send_append_entries
* records the FULL built envelope into this struct instead of enqueuing it on a
* real endpoint — so a unit test asserts the prev tuple / leader_commit /
* n_entries / entry term + payload without network I/O. The payload bytes are
* copied into a fixed buffer (capped at sizeof(payload)). */
typedef struct {
int called;
int peer_idx;
uint64_t prev_log_index;
uint64_t prev_log_term;
uint64_t leader_commit;
int n_entries;
uint64_t entry_lsn;
uint8_t entry_type;
uint64_t entry_term;
uint32_t payload_len;
uint8_t payload[256];
} repl_ae_capture_t;
extern repl_ae_capture_t *repl_send_ae_capture_hook;
#endif

View file

@ -6,6 +6,8 @@
#include <stdint.h>
#include <stdlib.h>
#include <openssl/ssl.h>
#include "netio.h"
#include "typemap.h"
#include "utils.h"
@ -17,12 +19,20 @@ typedef struct {
typedef int (*xce_check_fn_t)(void*);
extern int xce_request(endpoint_t *ep, xce_request_t *req);
extern int xce_encode(xce_request_t*, io_packet_t*);
extern int xce_decode(io_packet_t*, xce_request_t*);
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). */
extern int xce_ha_heartbeat_req(endpoint_t* ep, uint64_t term,
uint64_t committed_lsn,
uint64_t committed_config_tag);
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*);
@ -46,6 +56,7 @@ extern int xce_startjob_req(endpoint_t*, xce_job_exec_t*);
extern int xce_clusterconf_req(endpoint_t*);
extern int xce_reply_clusterconf(endpoint_t*, uint64_t, int, xce_peerinfo_t*,
xce_clusterconf_t*);
extern int xce_ctrlstatus_req(endpoint_t*);
extern int xce_get_req(endpoint_t*, xce_get_t*);
extern int xce_reply_get(endpoint_t*, xce_peerinfo_t*, xce_reply_t*,
xce_check_fn_t, void*, int);
@ -60,4 +71,134 @@ 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_replconfigpush_req_raw: send OP_replConfigPush carrying raw byte arrays.
* Bypasses the encode_ReplConfigPushRequest_ptr path (which maps sha256/bytes
* through xce_data_t — wrong type for raw bytes) by constructing the capnp
* message directly via capn_new_list8 + capn_setv8 + write_Request.
*/
extern int xce_replconfigpush_req_raw(endpoint_t *ep, xce_peerinfo_t *from,
uint64_t version_tag,
const uint8_t *sha256, size_t sha256_len,
const uint8_t *config_bytes,
size_t config_len);
/*
* xce_replconfig_reply_raw: primary sends an OP_replConfig reply with
* actual config bytes via endpoint (iomgr queued). Bypasses
* encode_ReplConfigReply_ptr (which maps sha256/bytes through xce_data_t).
*/
extern int xce_replconfig_reply_raw(endpoint_t *ep, xce_peerinfo_t *from,
uint64_t seq, uint64_t tag,
const uint8_t *sha256, size_t sha256_len,
const uint8_t *config, size_t config_len);
/*
* xce_replconfig_req_ssl: standby sends OP_replConfig over a raw SSL
* socket (no iomgr). Wire framing: uint64_t size + capnp payload
* (matches netio.c io_fsm_perform).
*/
extern int xce_replconfig_req_ssl(SSL *ssl, uint64_t version_tag);
/*
* xce_replconfig_reply_ssl_decode: standby reads + parses an
* OP_replConfig reply from a raw SSL socket. Output buffers are
* malloc'd and owned by the caller. Both *out_sha256 and *out_bytes
* may be NULL on the up_to_date == 1 fast path.
*/
extern int xce_replconfig_reply_ssl_decode(SSL *ssl,
int *out_up_to_date,
uint64_t *out_tag,
uint8_t **out_sha256,
uint8_t **out_bytes,
size_t *out_len);
/*
* xce_free_request: free an xce_request_t and all owned sub-fields,
* including the raw_sha256/raw_bytes fields on upgrade_config_req_t
* that the generated free_Request() does not know about.
* Use this everywhere instead of calling free_Request() directly.
*/
extern void xce_free_request(xce_request_t *req);
/*
* xce_wal_batch_encode_raw / xce_wal_batch_req_raw: encode/send an
* OPCode_walCatchupEntry batch carrying raw WAL payload bytes.
*
* Bypasses the generated encode path (payload :Data cannot be carried
* through xce_data_t — same limitation as replConfigPush). Each
* entries[i]->raw_payload/raw_payload_len is written as a capnp Data
* (List(UInt8)); the legacy xce_data_t payload field is ignored.
* The receiver's xce_decode() extracts the bytes back into
* raw_payload/raw_payload_len on each decoded sub-entry.
*
* encode_raw fills pkt (caller frees via free_packet on error paths);
* req_raw allocates the packet and enqueues it on ep (non-blocking).
*/
extern int xce_wal_batch_encode_raw(wal_catchup_entry_t **entries,
int n_entries,
xce_peerinfo_t *from,
uint64_t seq,
io_packet_t *pkt);
extern int xce_wal_batch_req_raw(endpoint_t *ep,
wal_catchup_entry_t **entries,
int n_entries);
/*
* xce_snapshot_chunk_encode_raw / xce_snapshot_chunk_req_raw: encode/send
* an OPCode_snapshotTransfer chunk carrying raw snapshot file bytes.
* Same raw-Data convention as xce_wal_batch_req_raw; chunk envelope
* fields (snapshot_lsn, chunk_index, chunk_offset, total_size, is_last)
* are taken from *chunk, payload bytes from payload/payload_len.
*/
extern int xce_snapshot_chunk_encode_raw(const snapshot_transfer_t *chunk,
const uint8_t *payload,
size_t payload_len,
xce_peerinfo_t *from,
uint64_t seq,
io_packet_t *pkt);
extern int xce_snapshot_chunk_req_raw(endpoint_t *ep,
const snapshot_transfer_t *chunk,
const uint8_t *payload,
size_t payload_len);
/*
* xce_upgrade_config_req_extract: extract sha256 + bytes from a decoded
* OP_upgradeConfig request. xce_decode() stores malloc'd copies in
* req->body.upgradeconfigrequestmsg->raw_sha256 / raw_bytes before
* capn_free() runs. Returns shallow pointers (owned by req); caller
* must NOT free them. Returns -EINVAL if fields are missing.
*/
extern int xce_upgrade_config_req_extract(const xce_request_t *req,
uint8_t **out_bytes,
size_t *out_len,
uint8_t **out_sha256);
/*
* xce_upgrade_config_req_extract_raw: parse OP_upgradeConfig request
* bytes from a raw on-wire packet buffer. Output buffers (*out_bytes,
* *out_sha256) are malloc'd and owned by the caller.
*/
extern int xce_upgrade_config_req_extract_raw(const uint8_t *pkt_data,
size_t pkt_sz,
uint8_t **out_bytes,
size_t *out_len,
uint8_t **out_sha256);
/* ── Client-Side Transparent Retry tunables (Req 6.1, 6.2) ──────────────────
*
* xce_retry_config_t: configurable retry parameters with safe production
* defaults, consumed by the libxce client failover loop (client.c /
* client_failover.c). XCE_RETRY_DEFAULTS supplies the defaults applied when no
* override is configured.
*/
typedef struct {
int max_retries; /* default 20 */
int initial_retry_ms; /* default 50 */
int max_retry_ms; /* default 5000 */
double backoff_multiplier; /* default 2.0 (exponential) */
} xce_retry_config_t;
extern const xce_retry_config_t XCE_RETRY_DEFAULTS;
#endif

View file

@ -6,6 +6,20 @@
#include "clusterconfig.h"
#include "xce/types.h"
/* Forward declarations for config-replication types used in
* xce_reply_t and xce_request_t unions below. */
typedef struct repl_config_reply_s repl_config_reply_t;
typedef struct repl_config_push_reply_s repl_config_push_reply_t;
typedef struct upgrade_config_reply_s upgrade_config_reply_t;
typedef struct repl_config_req_s repl_config_req_t;
typedef struct repl_config_push_req_s repl_config_push_req_t;
typedef struct upgrade_config_req_s upgrade_config_req_t;
typedef struct heartbeat_msg_s heartbeat_msg_t;
typedef struct ctrl_hello_req_s ctrl_hello_req_t;
typedef struct ctrl_hello_reply_s ctrl_hello_reply_t;
typedef struct wal_catchup_entry wal_catchup_entry_t;
typedef struct snapshot_transfer_s snapshot_transfer_t;
typedef struct {
char* resname;
char* hostname;
@ -46,8 +60,8 @@ typedef struct {
xce_group_config_t** ugroups;
int n_hgroups;
xce_group_config_t** hgroups;
int nresvals;
xce_val_with_idx_t** resvals;
int nresvals;
xce_name_val_t** resvals;
int nlimits;
xce_limit_config_t** limits;
int nhproviders;
@ -160,18 +174,93 @@ typedef struct {
} data;
} xce_update_data_t;
typedef struct {
uint64_t term;
char *candidate_id;
uint64_t config_tag;
uint64_t last_log_lsn;
/* Raft (lastLogTerm, lastLogIndex) freshness tuple (ha-term-safety task 1.2):
* feeds the library logOk gate, distinct from config_tag/last_log_lsn which
* drive the application freshness gate. last_log_index == candidate log_len. */
uint64_t last_log_term;
uint64_t last_log_index;
} request_vote_req_t;
typedef struct {
uint64_t term;
int vote_granted;
uint64_t peer_config_tag;
uint64_t peer_last_log_lsn;
} request_vote_reply_t;
/* Heartbeat reply state report — carries standby's applied_lsn and config_tag */
typedef struct {
uint64_t applied_lsn;
uint64_t config_tag;
} state_report_t;
/* WAL apply ACK — sent by standby to primary after applying a WAL catch-up
* entry (Task 3.1, ha-wal-replication). Must be declared before xce_reply_t
* so the walackreplymsg union arm compiles correctly. */
typedef struct {
uint64_t term;
uint64_t lsn;
int success;
} wal_ack_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")
* to the nested { count, list } member below. */
typedef struct {
char* name;
char* host;
uint32_t port;
} xce_ctrlpeer_t;
typedef struct {
char* clusterName;
char* primaryName;
char* responderName;
uint8_t responderRole;
uint64_t term;
uint64_t configVersion;
char* configChecksum;
uint64_t committedConfigVersion;
uint64_t committedLsn;
uint64_t appliedLsn;
unsigned fenced;
struct {
int count;
xce_ctrlpeer_t** list;
} peers;
uint32_t peerCount;
uint64_t responder_time_ms; /* @13: responder wall-clock at sample time (epoch ms) */
} xce_ctrlstatus_t;
typedef struct {
uint64_t seq;
uint32_t opcode;
int32_t status;
int data_which;
union {
char* debugmsg;
uint64_t jobid;
xce_data_t* getdata;
xce_clusterconf_t* clusterconf;
xce_fetchfile_reply_t* fetchfilereply;
char* debugmsg;
uint64_t jobid;
xce_data_t* getdata;
xce_clusterconf_t* clusterconf;
repl_config_reply_t* replconfigreplymsg;
repl_config_push_reply_t* replconfigpushreplymsg;
upgrade_config_reply_t* upgradeconfigreplymsg;
ctrl_hello_reply_t* ctrlhelloreplymsg;
request_vote_reply_t* requestvotereplymsg;
state_report_t* state_report;
wal_ack_reply_t* walackreplymsg;
xce_ctrlstatus_t* ctrlstatus;
xce_fetchfile_reply_t* fetchfilereply;
} data;
/* Req 7: standby redirect fields — populated when status == XCE_ERR_STANDBY */
char *redirect_host;
uint16_t redirect_port;
} xce_reply_t;
typedef struct {
@ -195,11 +284,209 @@ typedef struct {
xce_jobctrl_t* jobctrlmsg;
xce_queuectrl_t* queuectrlmsg;
xce_taskrun_t* runtaskmsg;
xce_preason_set_t* preasonmsg;
xce_clear_reason_t* clrpreasonmsg;
xce_hostctrl_t* hostctrlmsg;
xce_fetchfile_t* fetchfilemsg;
xce_preason_set_t* preasonmsg;
xce_clear_reason_t* clrpreasonmsg;
xce_hostctrl_t* hostctrlmsg;
repl_config_req_t* replconfigrequestmsg;
repl_config_push_req_t* replconfigpushrequestmsg;
upgrade_config_req_t* upgradeconfigrequestmsg;
heartbeat_msg_t* heartbeatmsg;
ctrl_hello_req_t* ctrlhellomsg;
request_vote_req_t* requestvotemsg;
wal_catchup_entry_t* walcatchupentrymsg;
snapshot_transfer_t* snapshottransfermsg;
xce_fetchfile_t* fetchfilemsg;
} body;
} xce_request_t;
/* ── HA election and replication Cap'n Proto mapped types ────────────────
* C-side counterparts for the HA protocol capnp structs added in the
* ha-config-bootstrap spec (controller.capnp structs with $C.mapname).
* Must be declared here so controller.capnp.h can reference them.
* 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;
} wal_ack_req_t;
typedef struct {
uint64_t term;
uint64_t acked_tag;
int success;
} upgrade_config_ack_t;
struct snapshot_transfer_s {
uint64_t snapshot_lsn; /* checkpoint LSN this snapshot corresponds to */
xce_data_t *payload; /* legacy decoded field — broken for raw bytes
* (decode_Data_ptr misinterprets List(UInt8));
* kept NULL on send, do not use */
uint32_t chunk_index; /* 0-based chunk number */
uint64_t chunk_offset; /* byte offset in the full snapshot */
uint64_t total_size; /* total snapshot size in bytes */
int is_last; /* true (non-zero) for the final chunk */
/* Raw chunk bytes — same pattern as repl_config_push_req_t.
* Send side: filled by the sender, encoded by xce_snapshot_chunk_req_raw.
* Receive side: extracted by xce_decode() before capn_free(); malloc'd,
* owned by the struct, freed by xce_free_request(). */
uint8_t *raw_payload;
size_t raw_payload_len;
};
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 */
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.
* Send side: filled from the WAL segment file, encoded per sub-entry by
* xce_wal_batch_req_raw. Receive side: extracted by xce_decode() before
* capn_free(); malloc'd, owned, freed by xce_free_request(). */
uint8_t *raw_payload;
size_t raw_payload_len;
/* Raft AppendEntries term fields (ha-term-safety task 1.1, Req 1.3/1.4).
* term == this entry's term stamp; prev_log_index == lsn-1; prev_log_term ==
* term of the entry at prev_log_index (0 if absent). Consumed by the log
* callbacks (tasks 2.x) for the AppendEntries logOk / truncation decision. */
uint64_t term;
uint64_t prev_log_index;
uint64_t prev_log_term;
/* ha-term-safety task 3.2 (Req 2.5/4.1): the leader's commit index carried on
* the AppendEntries so the standby intake passes it into
* raft_on_append_entries_request. Wire field leaderCommit @7. */
uint64_t leader_commit;
/* bug-1067: the streaming leader's CURRENT term — the Raft AppendEntries
* HEADER term, DISTINCT from `term` above (the entry's STORED term). Equal for
* a live append; for a HISTORICAL catch-up entry the stored term is lower than
* the leader's current term. The standby intake uses this as the AE header
* term (UpdateTerm/stale-check) so cross-term catch-up is not stale-rejected;
* 0 = unset (legacy sender) → fall back to the stored term. Wire field
* leaderCurrentTerm @8. */
uint64_t leader_current_term;
};
/* ── Persistence Cap'n Proto mapped types (P0-9) ─────────────────────
* These are the C-side counterparts for the capnp encode/decode shims.
* Named with capnp_ prefix to avoid collision with the plain-C structs
* in persist.h (persist_config_t) and the on-disk binary formats. */
typedef struct {
uint32_t walSyncMs;
uint32_t snapIntervalS;
uint32_t snapWalSizeMb;
uint32_t finishedRetainS;
uint32_t workerRetainS;
uint32_t syncTimeoutMs;
uint32_t snapKeepN;
uint32_t transientJobTimeoutS;
} capnp_persist_cfg_t;
typedef struct {
uint64_t lsn;
uint64_t term;
uint64_t timestamp;
uint32_t version;
uint32_t nJobs;
uint32_t nQueues;
uint32_t nWorkers;
uint32_t checksum;
uint32_t headerChecksum;
} capnp_snap_hdr_t;
typedef struct {
uint64_t term;
uint64_t lsn;
uint32_t entryType;
xce_data_t* payload; /* owned; decoded via decode_Data_ptr */
uint32_t crc;
uint32_t prevCrc;
} capnp_repl_envelope_t;
/* ── Config replication Cap'n Proto mapped types (Phase 1) ──────────────
* C-side counterparts for the capnp encode/decode shims for config sync.
* Struct tags match the forward declarations near the top of this file. */
struct repl_config_req_s {
uint64_t version_tag; /* local config version tag */
};
struct repl_config_reply_s {
int up_to_date; /* 1 if standby is current */
uint64_t version_tag; /* primary's current version tag */
xce_data_t *sha256; /* 32-byte SHA-256 of config blob (may be NULL) */
xce_data_t *bytes; /* serialised ClusterConfig blob (may be NULL) */
};
struct repl_config_push_req_s {
uint64_t version_tag; /* version tag of the pushed config */
xce_data_t *sha256; /* 32-byte SHA-256 (decoded, not usable as raw bytes) */
xce_data_t *bytes; /* serialised ClusterConfig blob (decoded, not usable) */
/* Pre-extracted raw fields populated by xce_decode() before capn_free(),
* mirroring the same pattern as upgrade_config_req_t. Owned by the struct. */
uint8_t *raw_sha256; /* 32-byte SHA-256, malloc'd, or NULL */
uint8_t *raw_bytes; /* serialised ClusterConfig blob, malloc'd, or NULL */
size_t raw_bytes_len; /* length of raw_bytes */
};
struct repl_config_push_reply_s {
int accepted; /* 1 if standby accepted the push */
uint64_t version_tag; /* standby's current version tag after decision */
};
struct upgrade_config_req_s {
/* Legacy decoded fields — populated by generated decode_UpgradeConfigRequest()
* via decode_Data_ptr. These are structurally broken for List(UInt8) wire
* fields (decode_Data_ptr reads a discriminant and misinterprets the bytes),
* but must remain for the generated code to compile. Do NOT use these for
* actual byte extraction. */
xce_data_t *sha256; /* broken: always a bogus xce_jobs_data_t */
xce_data_t *bytes; /* broken: always a bogus xce_jobs_data_t */
/* Correct raw bytes extracted by xce_decode() before capn_free() runs,
* using UpgradeConfigRequest_get_sha256/bytes + capn_getv8.
* Both are malloc'd and owned here; freed by free_upgrade_config_req_raw()
* called from free_Request() after free_UpgradeConfigRequest().
* raw_sha256 is exactly 32 bytes when non-NULL. */
uint8_t *raw_sha256; /* 32-byte SHA-256, malloc'd, or NULL */
uint8_t *raw_bytes; /* serialised ClusterConfig blob, malloc'd, or NULL */
size_t raw_bytes_len; /* length of raw_bytes */
};
struct upgrade_config_reply_s {
uint64_t version_tag; /* new version tag after upgrade */
char *error; /* NULL on success; error message on failure */
uint32_t acked_count; /* number of standbys that acknowledged */
};
struct heartbeat_msg_s {
uint32_t sender_priority; /* sender's HA priority */
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 */
};
/* 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 */
uint64_t applied_lsn; /* standby's last applied WAL LSN */
uint64_t config_tag; /* standby's current config tag */
};
struct ctrl_hello_reply_s {
uint64_t committed_lsn; /* primary's committed WAL LSN */
int needs_snapshot; /* 1 if standby LSN < oldest WAL segment */
uint64_t config_tag; /* primary's current config tag */
};
#endif

View file

@ -127,7 +127,7 @@ extern xce_timewindow_config_t* dup_timewindow_config(xce_timewindow_config_t*);
extern void free_resource_thresholds(xce_res_threshold_t**, int);
extern xce_res_threshold_t** dup_resource_thresholds(xce_res_threshold_t**,
int);
extern void free_resval_with_idx_list(xce_val_with_idx_t***, int);
extern void free_name_val_list(xce_name_val_t***, int*);
extern void free_strlist(xce_strlist_t*);
extern int fmt_jobid_ex(char*, size_t, uint64_t, char*, char*);
extern int fmt_jobid(char*, size_t, uint64_t);

105
src/include/wal_types.h Normal file
View file

@ -0,0 +1,105 @@
#pragma once
/*
* wal_types.h — WAL on-disk structures shared between persist.h and repl.h.
* Extracted in P2-4 to break the persist.h ↔ repl.h circular include.
*/
#include <stdint.h>
#include "config.h"
/* ── WAL payload helper structs ────────────────────────────────────────────
* These are the binary formats embedded after wal_entry_hdr_t for each type.
* All fields are native-endian (single-node WAL; replication uses capnp). */
/* WAL_JOB_START / FINISH / FAIL / STOP / RESUME / UPDATE / CKPT / PREASON / RUSAGE */
typedef struct {
uint64_t jobid;
uint32_t state; /* xce_job state flags after this transition */
uint32_t _pad;
} wal_jobstate_t;
/* WAL_QUEUE_CTRL — queue name is a NUL-terminated string immediately after
* this header. Total payload = sizeof(wal_queuehdr_t) + strlen(name)+1. */
typedef struct {
uint32_t status; /* xce_queue status flags after this transition */
} wal_queuehdr_t;
/* ── Raft adapter entry-blob envelope (ha-term-safety task 2.1) ─────────────
* The raft library (src/lib/raft/raft.c) holds NO entry bytes: it carries one
* opaque (entry, len) blob through raft_log_append / on_persist_entry, leaving
* its encoding entirely to the controller adapter. This struct IS that
* encoding: a fixed 8-byte prefix recording the WAL entry `type` and the
* `payload_len` of the bytes that immediately follow it, so on_persist_entry
* can recover (type, payload, len) for the existing persist write path.
*
* This is purely an in-flight wire/blob format between the library callback
* and persist.c — it is NEVER written to the WAL header (that stays the 40-byte
* wal_entry_hdr_t, R1.7); only the decoded `type` + `payload` reach the WAL.
*
* The append/replication driver (tasks 3.1/3.2) packs this same prefix before
* the payload when calling raft_log_append / routing an AppendEntries, so the
* encoder and decoder share one definition (no drift). */
typedef struct {
uint32_t type; /* wal_entry_type enum value for this entry */
uint32_t payload_len; /* bytes of payload immediately following this header */
/* uint8_t payload[payload_len] follows */
} raft_entry_blob_t;
/* ============================================================
* WAL entry header — 40 bytes, 8-byte aligned (Part 2.2)
* ============================================================ */
typedef struct {
uint64_t term; /* Election term (incremented on failover) */
uint64_t lsn; /* Log sequence number within this term */
uint64_t timestamp; /* Epoch microseconds */
uint32_t type; /* wal_entry_type enum value */
uint32_t payload_len; /* Bytes of payload following this header */
uint32_t crc; /* CRC32(header[0..WAL_HDR_CRC_SIZE-1] + payload) */
uint32_t prev_crc; /* CRC of previous record; WAL_CRC_NONE for first in segment */
} wal_entry_hdr_t;
_Static_assert(sizeof(wal_entry_hdr_t) == 40,
"wal_entry_hdr_t must be 40 bytes for on-disk format stability");
/* ============================================================
* WAL entry types (Part 2.3)
* ============================================================ */
typedef enum {
WAL_JOB_SUBMIT = 1, /* SYNC replication */
WAL_JOB_START = 2,
WAL_JOB_FINISH = 3,
WAL_JOB_FAIL = 4,
WAL_JOB_STOP = 5,
WAL_JOB_RESUME = 6,
WAL_JOB_UPDATE = 7,
WAL_JOB_RUSAGE = 8,
WAL_JOB_CKPT = 9,
WAL_JOB_PREASON = 10,
WAL_QUEUE_CTRL = 20,
WAL_WORKER_CTRL = 30,
WAL_NOOP = 40, /* Internal: empty payload, replay no-op; appended only at promotion */
WAL_CONFIG_MARKER = 50, /* Config-change marker: {version_tag, sha256} reference only —
* NEVER the config bytes (R7.2/R7.4). Ordered + committed through
* the same (term,lsn) log as job state (ha-term-safety). */
WAL_CHECKPOINT = 100, /* Local WAL marker only — never replicated */
} wal_entry_type;
/* ── WAL_CONFIG_MARKER payload (ha-term-safety task 4.1) ────────────────────
* The marker payload is a *reference* to a configuration — its version tag and
* full SHA-256 content hash — and carries NO configuration bytes (R7.2/R7.4).
* The configuration bytes always travel out-of-band on the existing config-push
* transport (OPCode_replConfigPush), keyed to {version_tag, sha256}; they are
* NEVER placed in a WAL payload.
*
* This struct rides INSIDE the raft_entry_blob_t envelope with
* type == WAL_CONFIG_MARKER; on persist it lands as the WAL entry payload. The
* 40-byte wal_entry_hdr_t is unchanged (R1.7) — this is a payload, not a header.
* Use config_marker_encode / config_marker_decode (config_repl.c) for a fixed,
* padding-independent on-wire form. */
typedef struct {
uint64_t version_tag; /* first 8 bytes of the config SHA-256, host order */
uint8_t sha256[32]; /* full SHA-256 of the config bytes (the reference) */
} wal_config_marker_t;
/* Fixed serialized size of a WAL_CONFIG_MARKER payload: 8-byte version_tag +
* 32-byte sha256, with NO config bytes ever appended. This is the exact byte
* count config_marker_encode emits and config_marker_decode requires. */
#define WAL_CONFIG_MARKER_PAYLOAD_LEN (8u + 32u)

View file

@ -0,0 +1,55 @@
/*
* client_failover.h — public StatusClassifier surface for the libxce-failover
* spec.
*
* Task 2.1 (StatusClassifier): pure mapping from a reply status / transport rc
* to a failover action. No I/O, no globals — see design.md
* "Components and Interfaces → StatusClassifier".
*/
#ifndef XCE_CLIENT_FAILOVER_H
#define XCE_CLIENT_FAILOVER_H
#include <stdbool.h>
/* Failover action a round-trip outcome maps to. Order mirrors the harness'
* fo_expected_action_t (failover_harness.h) and design.md. */
typedef enum {
XCE_FO_SUCCESS, /* status == 0, or idempotent-replay success
(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
— surface to caller */
} xce_fo_action_t;
/*
* Map a reply status / transport rc to a failover action.
*
* status : the controller reply status or negative -errno transport rc, as
* the failover loop observes it. Controller codes (E_XCE_*) are
* accepted in either sign.
* is_write: true for mutating ops (e.g. job submit); false for reads.
* attempt : 0 for the first send; >0 for a re-send after failover.
*
* Invariant (2.5): E_XCE_REDIRECT/STANDBY/NOMASTER/NOT_READY never map to
* XCE_FO_SUCCESS.
*
* Idempotent-replay rule (5.2): E_XCE_DUP_JOB / EEXIST on a re-sent write
* (is_write && attempt > 0) → XCE_FO_SUCCESS; otherwise → XCE_FO_TERMINAL.
*
* Unknown / unrecognized statuses are treated conservatively as
* XCE_FO_TERMINAL (surfaced, not retried).
*
* Pure function: no I/O, no globals.
*/
xce_fo_action_t xce_failover_classify(int status, bool is_write, int attempt);
/*
* EndpointRotator (xce_failover_next_target / xce_failover_rotate) operates on
* the private api_ctx_t and is therefore declared in the internal header
* src/lib/client_internal.h, not in this public surface.
*/
#endif /* XCE_CLIENT_FAILOVER_H */

128
src/include/xce/cluster.h Normal file
View file

@ -0,0 +1,128 @@
/* src/include/xce/cluster.h — cluster discovery and reconnect API (P2.5-1)
*
* xce_connect_cluster() returns endpoint_t* (cast to void*).
* The SSL* is accessible as ((endpoint_t*)result)->ssl.
*/
#ifndef XCE_CLUSTER_H
#define XCE_CLUSTER_H
#include <stddef.h>
#include "config.h" /* MAX_HA_CANDIDATES, XCE_MAXHOSTNAME_LEN */
/**
* A single controller candidate.
* name: logical name from "name=host:port" syntax (empty if omitted).
* Matching against myconf->name allows multiple controllers on the same
* dev host to distinguish themselves by name rather than IP address.
*/
typedef struct {
char name[XCE_MAXNAME_LEN];
char hostname[XCE_MAXHOSTNAME_LEN];
int port;
} xce_candidate_t;
/**
* Cluster connection configuration.
* Populated by each caller (worker/scheduler/apiserver) from --peers and
* --ca-cert CLI flags. libxce never reads config files.
*
* 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];
int candidates_count;
char ca_cert_path[4096]; /* path to CA cert for TLS verify */
char identity[256]; /* "from" name sent to controller */
char key[256]; /* authentication key */
} xce_cluster_config_t;
/**
* Reconnect state machine states.
*/
typedef enum {
XCE_RECONNECT_IDLE = 0,
XCE_RECONNECT_CONNECTING,
XCE_RECONNECT_BACKOFF,
XCE_RECONNECT_CONNECTED,
XCE_RECONNECT_FAILED
} xce_reconnect_state_t;
/**
* Reconnect context (use xce_reconnect_* functions to operate).
*/
typedef struct {
xce_cluster_config_t* config;
xce_reconnect_state_t state;
int attempt;
int backoff_ms; /* current backoff delay in ms */
void* endpoint; /* endpoint_t* when CONNECTED */
} xce_reconnect_ctx_t;
/* Redirect follow limit */
#define XCE_MAX_REDIRECTS 4
/* Backoff parameters — initial kept short so tests stay fast */
#define XCE_RECONNECT_INITIAL_BACKOFF_MS 100
#define XCE_RECONNECT_MAX_BACKOFF_MS 30000
#define XCE_RECONNECT_BACKOFF_MULTIPLIER 2
/* --- Public API --- */
/**
* xce_cluster_config_validate() — validate a populated xce_cluster_config_t.
*
* Returns 0 on success, -EINVAL if:
* - candidates_count < 1 or > MAX_HA_CANDIDATES
* - any candidate has empty hostname or port <= 0
* - ca_cert_path is empty
* - identity is empty
*/
int xce_cluster_config_validate(const xce_cluster_config_t* cfg);
/**
* xce_connect_cluster() — connect to the cluster via candidate list.
*
* Iterates candidates in order, attempting a TLS connection to each.
* Returns endpoint_t* (cast to void*) on success, NULL if all fail.
*/
void* xce_connect_cluster(xce_cluster_config_t* cfg);
/**
* xce_reconnect_init() — initialise a reconnect context.
* Returns 0 on success, -EINVAL on bad arguments.
*/
int xce_reconnect_init(xce_reconnect_ctx_t* rctx, xce_cluster_config_t* cfg);
/**
* xce_reconnect_step() — drive one step of the reconnect state machine.
*
* Call in a loop until state == XCE_RECONNECT_CONNECTED or FAILED.
* Sleeps backoff_ms in BACKOFF state (skipped when mock connect is set).
*
* Returns 0 to continue, -1 when in terminal state.
*/
int xce_reconnect_step(xce_reconnect_ctx_t* rctx);
/**
* xce_reconnect_fini() — release resources held by a reconnect context.
*/
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".
* 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.
*
* Returns 0 on success, -1 on empty/NULL input or any parse error.
*/
int xce_parse_peers(const char* peers_str, xce_cluster_config_t* cfg);
/* 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,
const char* ca_cert);
void xce_cluster_set_connect_fn(xce_connect_fn_t fn);
#endif /* XCE_CLUSTER_H */

View file

@ -14,7 +14,12 @@
#include "myconf.h"
#include "mylock.h"
#include "netio.h"
#include "persist.h"
#include "vtaskmgr.h"
/* ha.h forward-declares cluster_t; including it here is safe (no circular dep) */
#include "commit.h"
#include "ha.h"
#include "xce/types.h"
#include "xlist.h"
#include "zarray.h"
@ -55,7 +60,9 @@ enum {
HOOK_DETAIL_QUEUE_UPDATE,
HOOK_DETAIL_USERGROUP_UPDATE,
HOOK_DETAIL_HOSTGROUP_UPDATE,
HOOK_DETAIL_JOB_RUSAGE_UPDATE
HOOK_DETAIL_JOB_RUSAGE_UPDATE,
HOOK_DETAIL_JOB_STOP,
HOOK_DETAIL_JOB_RESUME
};
#define XCE_NS_CONFIG (0)
@ -88,9 +95,10 @@ typedef struct {
endpoint_t* ep;
} cluster_apiserver_t;
typedef struct {
typedef struct cluster_s {
myconf_t* myconf;
xce_peerinfo_t* myhost;
xce_peerinfo_t* myhost; /* points at myhost_info once initialised */
xce_peerinfo_t myhost_info; /* storage; name set from myconf->name */
hooksys_t hooksys;
iomgr_t iomgr;
zarray_t endpoints;
@ -112,6 +120,29 @@ typedef struct {
vtaskmgr_t vtaskmgr;
cbuf_t wbuf;
int running;
persist_t persist; /* Persistence module — P0-F */
ha_state_t ha; /* HA role management — P1-5 */
commit_engine_t commit; /* Per-entry commit engine — ha-commit-engine 5.3 */
_Atomic int restart_requested; /* set by OP_upgradeConfig handler */
pthread_t heartbeat_listener_tid; /* pre-Phase-2 SSL listener thread */
_Atomic int heartbeat_listener_running;
_Atomic int lost_and_found_job_count; /* incremented during persist_recover() */
/* Performance-tuning parameters — initialized by init_controller_tunables()
* in cluster_init_phase1 so they are available before cluster_init_phase2.
* TODO: wire to cluster_config once config generation tool supports @13-@17. */
struct {
int jobmgr_nhash; /* hash slots for job table */
int jobmgr_delta; /* growth step for job table */
int workermgr_nhash; /* hash slots for worker table */
int workermgr_delta; /* growth step for worker table */
int qmgr_nhash; /* hash slots for queue table */
int qmgr_delta; /* growth step for queue table */
int resmgr_nhash; /* hash slots for resource table */
int resmgr_delta; /* growth step for resource table */
int resmgr_inst; /* growth step for shared-instance array */
int xstrings_nhash; /* hash slots for xstring table */
} tunables;
} cluster_t;
#endif /* XCE_CLUSTER_DEF_H */

View file

@ -19,7 +19,16 @@ enum {
E_XCE_BAD_QUEUE,
E_XCE_BAD_GROUP,
E_XCE_INTERNAL,
E_XCE_NOMASTER, /* Standby has no known primary — client should retry */
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_MAX
};
/* HA dispatch gate error variables — defined in ha.c, declared here so all
* dispatch call sites that include errnox.h get the right names. */
extern int XCE_ERR_NOT_READY; /* == E_XCE_NOT_READY */
extern int XCE_ERR_STANDBY; /* == E_XCE_STANDBY */
#endif

View file

@ -172,10 +172,13 @@ typedef struct {
#define XCE_WORKER_LOCKEDW (0x00040000)
#define XCE_WORKER_BUSY (0x00080000)
#define XCE_WORKER_CLOSED (0x00100000)
#define XCE_WORKER_HIDDEN (0x00200000) /* Admin explicitly removed — invisible but not yet cleaned up */
#define XCE_WORKER_CLOSED_BY_ADMIN (0x00400000) /* Admin closed — host closed explicitly by admin */
#define IS_WORKER_UNAVAIL(status) (((status) & XCE_WORKER_UNAVAIL) != 0)
#define IS_WORKER_LOCKEDU(status) (((status) & XCE_WORKER_LOCKEDU) != 0)
#define IS_WORKER_LOCKEDW(status) (((status) & XCE_WORKER_LOCKEDW) != 0)
#define IS_WORKER_UNAVAIL(status) (((status) & XCE_WORKER_UNAVAIL) != 0)
#define IS_WORKER_LOCKEDU(status) (((status) & XCE_WORKER_LOCKEDU) != 0)
#define IS_WORKER_LOCKEDW(status) (((status) & XCE_WORKER_LOCKEDW) != 0)
#define IS_WORKER_CLOSED_BY_ADMIN(status) (((status) & XCE_WORKER_CLOSED_BY_ADMIN) != 0)
#define IS_WORKER_LOCKED(status) \
(IS_WORKER_LOCKEDU(status) || IS_WORKER_LOCKEDW(status))
#define IS_WORKER_BUSY(status) (((status) & XCE_WORKER_BUSY) != 0)
@ -212,8 +215,8 @@ typedef struct {
int nthresholds;
xce_res_threshold_t** thresholds;
/* following two vars used to push resource values to sched */
int nresvals_to_sched;
xce_val_with_idx_t** resvals_to_sched;
int nresvals_to_sched;
xce_name_val_t** resvals_to_sched;
} xce_worker_t;
typedef struct {
@ -353,7 +356,8 @@ typedef struct {
#define JOB_STAT_PDONE (0x80) /**< Post job process done successfully */
#define JOB_STAT_PERR (0x100) /**< Post job process has error */
#define JOB_STAT_WAIT (0x200) /**< Chunk job waiting its turn to exec */
#define JOB_STAT_WAIT (0x200) /**< Chunk job waiting its turn to exec */
#define JOB_STAT_TRANSIENT (0x400) /**< Job state uncertain after failover — awaiting worker re-join */
#define JOB_STAT_RUNKWN \
0x8000 /* Flag : Job status is UNKWN caused by losting contact with remote \
cluster */
@ -381,6 +385,7 @@ typedef struct {
#define QUEUE_STAT_DISC 0x10 /**< Remote queue status is disconnected. */
#define QUEUE_STAT_FAIRSHARE_DEFAULT \
0x20 /**< Fairshare queue with null user share defined*/
#define QUEUE_STAT_HIDDEN (0x0100) /**< Admin deleted — invisible but not yet cleaned up */
/* FIXME: improve following later */
#define JOB_IS_PEND(state) (((state) & JOB_STAT_PEND) != 0)
@ -472,10 +477,16 @@ typedef struct {
char* ckptinfo;
int64_t pending_ctrl;
xce_job_preason_t* pend_reasons;
uint64_t transient_since; /* epoch seconds; set when job enters JOB_STAT_TRANSIENT */
} xce_job_t;
typedef struct {
xce_job_t job;
/* ha-convergence-recovery (3.3): WAL entry LSN of this job's submit record.
* In-memory only — recomputed from the applied entry at replay; never
* 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;
} job_t;
typedef struct {

View file

@ -18,6 +18,7 @@ extern int xce_jobdispatch(xce_ctx_t*, xce_job_dispatch_t*);
extern int xce_jobsubmit(xce_ctx_t*, xce_job_submit_t*, uint64_t*);
extern int xce_joblist(xce_ctx_t*, xce_job_t***);
extern int xce_clusterconf(xce_ctx_t*, xce_clusterconf_t**);
extern int xce_ctrlstatus(xce_ctx_t*, xce_ctrlstatus_t**);
extern int xce_push(xce_ctx_t*, xce_check_fn_t, void*, int, int, int, void**);
extern int xce_update_rows(xce_ctx_t*, int, int, xlist_t*);
extern int xce_resval(xce_ctx_t*, char*, char*, xce_resval_t***);

View file

@ -2,6 +2,9 @@
#define _XCECTX_H_ 1
#include <stdint.h>
#include "config.h" /* XCE_MAXHOSTNAME_LEN */
#include "netio.h"
#include "request.h"
#include "vtaskmgr.h"
@ -28,12 +31,20 @@ typedef struct {
cbuf_t wbuf;
dataset_t* dataset;
/* Redirect-hint address captured from the last redirect reply by
* handle_reply() (which only sees xce_ctx_t) and read by the failover
* rotator on the next retry. Empty host ("") means no hint. Advisory only;
* correctness never depends on it. */
char redirect_hint_host[XCE_MAXHOSTNAME_LEN];
uint16_t redirect_hint_port;
struct {
union {
char* debugmsg;
dataset_t* get;
uint64_t jobid;
xce_clusterconf_t* clusterconf;
xce_ctrlstatus_t* ctrlstatus;
xce_fetchfile_reply_t* fetchfilereply;
} u;
} reply;
@ -47,7 +58,29 @@ extern int xce_ctx_stop(xce_ctx_t*);
extern int xce_wakeup(xce_ctx_t*, int);
extern int xce_try_wakeup(xce_ctx_t*, int);
extern int xce_ctx_set_endpoint(xce_ctx_t*, endpoint_t*);
extern int xce_ctx_reconnect(xce_ctx_t*, char*, int);
extern void xce_block_tasks(xce_ctx_t*);
extern void xce_unblock_tasks(xce_ctx_t*);
/* Reply dispatch (normally driven by the vtask reply path). Exposed for the
* libxce test suite so the reply union's type-safety can be verified without a
* live endpoint (bug-1077: the shared, untagged reply union must never be
* dereferenced/freed as the wrong type). */
extern int xce_handle_reply(xce_ctx_t*, xce_reply_t*);
/*
* xce_ctx_capture_redirect_hint: copy a redirect address carried on a reply
* into the ctx's redirect_hint_host/port fields as a best-effort failover hint
* (Req 2.2, 8.3). Called by handle_reply() BEFORE the capnp-owned reply (and
* its redirect_host string) is freed.
*
* When `host` is NULL/empty or `port` is 0 there is no usable address: the hint
* is left untouched (it stays empty). Otherwise the host is bounded-copied into
* the fixed-size field (truncated + NUL-terminated, never overflowing) and the
* port is stored. A NULL ctx is a safe no-op. Advisory only — correctness never
* depends on the hint.
*/
extern void xce_ctx_capture_redirect_hint(xce_ctx_t*, const char* host,
uint16_t port);
#endif