#pragma once /* config_repl.h — config storage, SHA-256, and replication functions */ #define _GNU_SOURCE #include #include #include #include "myconf.h" #include "clusterconfig.h" #include "netio.h" #include "request.h" /* Max config blob size — reject before allocating */ #define CONF_MAX_SIZE (1 * 1024 * 1024) /* 1 MiB */ /* Sentinel name for the synthetic lost-and-found queue and host */ #define XCE_LOST_AND_FOUND_NAME "lost_and_found" /* Compile-time gate for OP_upgradeConfig (enabled: standbys apply pushed config) */ #ifndef CONF_ENABLE_UPGRADE_CONFIG #define CONF_ENABLE_UPGRADE_CONFIG 1 #endif /* Push timeout for standby fan-out (ms) */ #ifndef CONF_PUSH_TIMEOUT_MS #define CONF_PUSH_TIMEOUT_MS 500 #endif /* Certificate expiry warning threshold (days) */ #define CERT_WARN_DAYS 30 /* ── Phase 1: Storage ────────────────────────────────────────── */ /* * conf_read_current_filename: read HEAD-style pointer file at * /conf/current. Strips trailing newline. Validates format: * must start with "cluster.bin." followed by exactly 12 hex chars. * Returns 0 on success; -ENOENT if absent; -ENODATA if empty (zero-length); * -EINVAL if malformed (path separator, wrong prefix, wrong hex length). * out must be at least NAME_MAX bytes. */ int conf_read_current_filename(const char *workdir, char *out, size_t outsz); /* * conf_save_config: atomically write versioned file and update HEAD pointer. * 1. Writes /cluster.bin..tmp, fsyncs, renames to versioned path. * 2. Writes /current..tmp, fsyncs, renames to conf/current. * 3. fsyncs the conf/ directory. * 4. Trims old versions. * 5. Updates myconf->conffile (heap-allocated via asprintf; caller must free). * 6. Updates myconf->config_version_tag and myconf->config_sha256. * Returns 0 on success, -errno on failure. */ int conf_save_config(myconf_t *myconf, const uint8_t *bytes, size_t len, const uint8_t sha256[32]); /* * conf_read_raw: read versioned file at conffile path. * Allocates *bytes (caller must free). Enforces CONF_MAX_SIZE. * Returns 0 on success; -EFBIG if exceeds limit; -errno on I/O error. */ int conf_read_raw(const char *conffile, uint8_t **bytes, size_t *len); /* * conf_get_saved_tag: return first 8 bytes of SHA-256 of the current * versioned file in host byte order. Returns 0 if conf/current absent. * This is the "no config saved" zero-tag sentinel. */ uint64_t conf_get_saved_tag(const char *workdir); /* * conf_check_stale: returns 1 if the current versioned file's mtime is * older than stale_timeout_s seconds. Returns 0 if within timeout or * if stale_timeout_s == 0 (disabled). Returns 0 if conf/current absent. */ int conf_check_stale(const char *workdir, time_t stale_timeout_s); /* * conf_trim_old_versions: keep at most keep_n versioned cluster.bin.* files * sorted by mtime (newest first). Always preserves the file named by the * current conf/current pointer. Also removes stale .tmp files. */ void conf_trim_old_versions(const char *workdir, int keep_n); /* * config_contains_reserved_name: returns 1 if ClusterConfig contains a * queue or host named "lost_and_found". Returns 0 for NULL config. * Checks queues[i]->queue (queue name) and hconfigs[i]->name (host name). */ int config_contains_reserved_name(cluster_config_t *cfg); /* * create_lost_and_found: ensure the synthetic "lost_and_found" queue and * host exist before persist_recover() runs. Stub for Task 10. */ void create_lost_and_found(cluster_t *c); /* * handle_config_request: handle an OP_replConfig request from a standby. * Reads the current versioned config blob and sends it back. * Stub for Task 8 (full implementation in Task 9). */ int handle_config_request(cluster_t *c, endpoint_t *ep, xce_request_t *req); /* * handle_repl_config_push: standby handler for OPCode_replConfigPush. * Verifies SHA-256, saves config blob to disk, and reloads cluster_config. */ int handle_repl_config_push(cluster_t *c, endpoint_t *ep, xce_request_t *req); /* * push_config_to_standbys: fan-out a new config blob to all connected standby * controllers in parallel. Best-effort — standbys that miss the push will * pull on reconnect via handle_config_request(). * Returns the number of standbys that successfully received the push. * Only meaningful when CONF_ENABLE_UPGRADE_CONFIG is set, but always declared * so callers can reference it without an #if guard. */ int push_config_to_standbys(cluster_t *c, const uint8_t *bytes, size_t blen, const uint8_t sha256[32], uint64_t new_tag); /* * handle_upgrade_config: handle an OP_upgradeConfig request from an admin tool. * xce_decode() now extracts sha256 + bytes into req->body.upgradeconfigrequestmsg * before freeing the capnp segment, so no raw packet copy is needed. * Only callable when CONF_ENABLE_UPGRADE_CONFIG is set. */ int handle_upgrade_config(cluster_t *c, endpoint_t *ep, xce_request_t *req); /* * ha_stream_wal_to_standby: batch-stream WAL entries [standby_lsn+1, committed_lsn] * to a standby endpoint via xce_request() (non-blocking). * * Accumulates WAL entries up to WAL_STREAM_BATCH_ENTRIES (256) entries or * WAL_STREAM_BATCH_BYTES (65536) bytes per batch, then enqueues each batch * as a single OPCode_walCatchupEntry message with n_entries set. * All batches for the full range are enqueued in a single invocation — never * blocks the vtaskmgr thread. * * A partial final batch (fewer entries than the batch limit) is sent immediately * without waiting for the batch to fill (Req 5.4). * * Parameters: * ep — outbound endpoint to the standby * wal_path — directory containing WAL segment files (seg-*.wal) * standby_lsn — standby's last applied LSN (stream starts at standby_lsn+1) * committed_lsn— primary's committed LSN (stream ends at committed_lsn inclusive) * * Returns 0 on success; negative on internal error (e.g., NULL ep). * Requirements: 5.1, 5.2, 5.4, 5.5 (ha-repl-hardening spec) * Boundary: WAL Streaming (config_repl.c) */ #define WAL_STREAM_BATCH_ENTRIES 256 #define WAL_STREAM_BATCH_BYTES 65536 int ha_stream_wal_to_standby(endpoint_t *ep, const char *wal_path, uint64_t standby_lsn, uint64_t committed_lsn); /* * handle_ctrlhello: primary-side handler for OP_hello from a joining standby. * * Implements the state sync sequence (Req 6.5, 6.6, 6.7): * 1. repl_add_standby() + repl_start_catchup() * 2. Reply CtrlHelloReply{committed_lsn, needs_snapshot, config_tag} * 3. If needs_snapshot: persist_checkpoint() + stream OP_snapshotTransfer * 4. Stream OP_walCatchupEntry [applied_lsn+1 → committed_lsn] * 5. On catch-up complete: repl_complete_catchup() * 6. If config_tag mismatch: push_config_to_standbys() */ int handle_ctrlhello(cluster_t *c, endpoint_t *ep, xce_request_t *req); /* * sanitise_cn: copy at most maxlen printable ASCII chars from src to a * static buffer, replacing non-printable/control chars with '?'. * Returns pointer to the static buffer. NOT thread-safe (static storage). */ char *sanitise_cn(const char *src, size_t maxlen); #ifdef XCE_TESTING #include #include #include /* * xce_probe_config_tag_hook: test-only injection point for config tag probing. * When non-NULL, xce_probe_config_tag() calls this instead of the real * network probe, allowing unit tests to return controlled tag values * without real network connections. * Set by test infrastructure (e.g. test_ha_bootstrap_support.c) before tests run. */ extern int (*xce_probe_config_tag_hook)(const char *host, uint16_t port, uint64_t *tag_out); /* * load_primary_config_for_test: test-only wrapper that exercises the path * resolution logic of load_primary_config() without calling FATAL. * Returns 0 if conf/current resolves to a valid filename, negative errno * otherwise. Does NOT perform Cap'n Proto decode or SHA-256 computation. */ int load_primary_config_for_test(cluster_t *c); /* * push_ctx_t: exposed under XCE_TESTING so unit tests can construct * synthetic push contexts to drive wait_all_with_timeout_for_test(). * In production builds this type is private to config_repl.c. */ typedef struct { endpoint_t *ep; xce_peerinfo_t *from; uint64_t new_tag; const uint8_t *bytes; size_t blen; const uint8_t *sha256; int acked; int started; int done; } push_ctx_t; /* * wait_all_with_timeout_for_test: test-only shim exposing the static * wait_all_with_timeout() helper for unit tests. * Joins all started push threads within timeout_ms (CLOCK_MONOTONIC). * Sets *done = 1 when deadline expires. Returns number of acked threads. */ int wait_all_with_timeout_for_test(pthread_t *threads, push_ctx_t *ctxs, int n, uint32_t timeout_ms, atomic_int *done); #endif