forked from lei.chen/myxce
Converting COMMIT_MODE_WAIT away from a blocking wait removed the only mechanism that ever detected a commit timing out (the caller's own pthread_cond_timedwait deadline). Adds a dedicated per-commit-engine sweep thread that arms an equivalent timeout for async-registered continuations, resolving FAILED/-ETIMEDOUT within the same DEFAULT_SYNC_TIMEOUT_MS window the old blocking path used, with no thread parked to detect it. Chose a private sweep thread over the shared delaymgr: wiring delaymgr would touch cluster.c/ha.c (out of boundary) and its 200ms poll granularity vs the 500ms default timeout would add jitter, breaking the "identical failure timing" no-regression bar. Round 1 review rejected a lost-wakeup race: the sweep loop computed its next-deadline snapshot under lock, unlocked to dispatch, then slept on the stale snapshot -- a registration landing mid-dispatch had its broadcast lost (no thread parked on the condvar), so the sweep thread could sleep on an absent/late deadline indefinitely. Fixed by holding the lock continuously across scan-then-wait decisions and always re-scanning immediately before ever waiting (recheck-then-wait idiom). Round 2 review independently reproduced the round-1 bug shape against the fix to confirm it actually closes the race, not just narrows it.
201 lines
12 KiB
C
201 lines
12 KiB
C
#pragma once
|
|
/*
|
|
* commit.h — Commit engine: single authority for per-entry commit evaluation,
|
|
* committed_lsn advancement, and waiter completion.
|
|
*
|
|
* Service interface is umbrella-verbatim from
|
|
* .kiro/specs/ha-consistency-invariants/design.md (sliced by ha-commit-engine).
|
|
*
|
|
* All functions thread-safe; commit_on_ack callable from iomgr (non-blocking).
|
|
*/
|
|
#include <pthread.h>
|
|
#include <stdint.h>
|
|
#include <time.h> /* struct timespec — per-continuation timeout deadline (task 1.3) */
|
|
|
|
#include "persist.h" /* persist_t (owns committed_lsn atomic + replicator_t registry) */
|
|
#include "vtaskmgr.h" /* vtaskmgr_t — LivenessAdmission blocking-admission seam */
|
|
|
|
/* Headroom kept free of blocking commit waits; cap = max(1, n_vcpus - reserved).
|
|
* Used ONLY by the local (no-pool) admission fallback. When a vtaskmgr pool is
|
|
* wired (commit_set_pool, raft-drive-async task 1.6), the pool owns the single
|
|
* reservation (cap = max(1, n_vcpus - VTASK_LIVENESS_RESERVED)) and this
|
|
* constant is not consulted. */
|
|
#define COMMIT_WAIT_RESERVED 2
|
|
|
|
typedef enum { COMMIT_MODE_WAIT, COMMIT_MODE_RETAIN } commit_mode_t;
|
|
|
|
/* raft-apply-decoupling task 1.1: event-driven resolve outcome, delivered to
|
|
* a registered continuation in place of a COMMIT_MODE_WAIT caller's blocking
|
|
* return value. COMMITTED mirrors commit_submit's rc==0; FAILED mirrors any
|
|
* of commit_submit's negative outcomes (timeout, demotion, truncation), with
|
|
* the specific cause carried in the continuation's `rc` argument. */
|
|
typedef enum { COMMIT_OUTCOME_COMMITTED, COMMIT_OUTCOME_FAILED } commit_outcome_t;
|
|
|
|
/* Invoked exactly once per registered lsn when its outcome becomes known.
|
|
* Delivered via commit_resolve (wired by a follow-up task onto
|
|
* commit_advance_locked's majority path and the existing timeout/demotion/
|
|
* truncation failure handling), dispatched off the calling thread via
|
|
* vtask_enq_classified -- never inline on the replication-ACK or iomgr
|
|
* path. */
|
|
typedef void (*commit_continuation_fn)(void* ctx, uint64_t lsn,
|
|
commit_outcome_t outcome, int rc);
|
|
|
|
/* raft-apply-decoupling task 1.1: one registered COMMIT_MODE_WAIT-equivalent
|
|
* continuation. lsn == 0 marks a free slot -- LSN 0 is the reserved
|
|
* empty/none sentinel (see persist.h reserve-lsn-0), so it is never a real
|
|
* registration and is safe as the "unused" marker. */
|
|
typedef struct commit_continuation {
|
|
uint64_t lsn;
|
|
commit_continuation_fn fn;
|
|
void* ctx;
|
|
/* raft-apply-decoupling task 1.3: absolute CLOCK_REALTIME deadline this
|
|
* registration must resolve by if majority is never reached -- computed
|
|
* at registration time via the SAME wait_deadline() helper and the SAME
|
|
* DEFAULT_SYNC_TIMEOUT_MS constant commit_submit's blocking WAIT path
|
|
* uses, so an async registration observes identical failure timing to
|
|
* today's blocking path (Req 11.1). Only meaningful while lsn != 0;
|
|
* overwritten on every fresh registration of a reused slot. Swept by the
|
|
* commit engine's dedicated timeout-sweep thread (see commit.c). */
|
|
struct timespec deadline;
|
|
} commit_continuation_t;
|
|
|
|
/*
|
|
* Commit engine state — in-memory only (commit state volatile, Raft-conformant).
|
|
* Per-standby view delegated to standby_conn_t.acked_lsn / in_catch_up via
|
|
* p->repl (no duplication).
|
|
*/
|
|
typedef struct commit_engine {
|
|
pthread_mutex_t lock;
|
|
pthread_cond_t cond; /* broadcast on committed_lsn advance / membership change */
|
|
persist_t* p; /* back-reference: committed_lsn store + repl registry */
|
|
int n_peers;
|
|
int n_vcpus;
|
|
/* raft-drive-async task 1.6: when non-NULL, commit-wait admission is folded
|
|
* onto the vtaskmgr pool's single LivenessAdmission reservation (the pool
|
|
* owns the cap). When NULL (commit-only unit fixtures), the legacy local
|
|
* cap max(1, n_vcpus - COMMIT_WAIT_RESERVED) is used. inflight_waits is
|
|
* maintained in BOTH modes as the observable in-flight commit-wait count. */
|
|
vtaskmgr_t* pool;
|
|
int inflight_waits; /* in-flight blocking commit waits */
|
|
uint64_t writable_lsn; /* own-term gate; 0 = no gate */
|
|
/* ha-term-safety commit-ceiling-optA: the raft library's commit_index,
|
|
* recorded for OBSERVABILITY / TRACE / conformance-oracle ONLY. It does NOT
|
|
* gate commit-advance: majority_candidate_locked no longer folds it into the
|
|
* commit candidate. Commit-advance is authoritative via the durable-majority
|
|
* barrier + the own-term writable_lsn gate. Kept (and still fed by
|
|
* on_commit_advanced) so the library half keeps a sink and tooling can
|
|
* compare the library current-term commit point vs committed_lsn. Monotonic
|
|
* non-decreasing, set under ce->lock via commit_set_raft_commit_index. */
|
|
uint64_t raft_commit_index;
|
|
uint64_t scratch[MAX_STANDBYS]; /* sorted matchIndex scratch array */
|
|
/* raft-apply-decoupling task 1.1: per-LSN continuation registry for
|
|
* commit_submit_async (event-driven resolve, replacing the
|
|
* COMMIT_MODE_WAIT blocking-park model). Sized to n_vcpus at init: the
|
|
* SAME admission-cap discipline (wait_admit_locked/wait_cap, the shared
|
|
* inflight_waits budget) bounds concurrent registrations to
|
|
* wait_cap(ce) <= n_vcpus, so n_vcpus slots always suffice regardless of
|
|
* the blocking/async call mix. Resolution (commit_resolve, invoked from
|
|
* commit_advance_locked and the timeout/demotion/truncation failure
|
|
* paths) is wired by a follow-up task; until then a registered slot is
|
|
* inert -- nothing reads it back. */
|
|
commit_continuation_t* continuations;
|
|
int n_continuations; /* capacity, == n_vcpus */
|
|
/* raft-apply-decoupling task 1.3: dedicated timeout-sweep thread, private
|
|
* to this commit_engine_t (NOT the shared controller delaymgr/iomgr --
|
|
* see commit.c file header for why). Sleeps on `cond` to the earliest
|
|
* pending continuation deadline (or indefinitely when none are pending)
|
|
* and resolves any still-registered continuation FAILED/-ETIMEDOUT once
|
|
* its deadline passes. Started in commit_engine_init, stopped+joined in
|
|
* commit_engine_fini before `lock`/`cond`/`continuations` are torn down.
|
|
* sweep_stop is read/written only under `lock`, alongside the broadcast
|
|
* that wakes the thread to observe it. */
|
|
pthread_t sweep_thread;
|
|
int sweep_stop;
|
|
} commit_engine_t;
|
|
|
|
/* Lifecycle. init validates n_peers >= 0, n_vcpus >= 1, non-NULL persist_t;
|
|
* returns -EINVAL otherwise. Call after n_peers and n_vcpus are final. */
|
|
int commit_engine_init(commit_engine_t* ce, persist_t* p, int n_peers, int n_vcpus);
|
|
void commit_engine_fini(commit_engine_t* ce);
|
|
|
|
/* raft-drive-async task 1.6: fold commit-wait admission onto the vtaskmgr
|
|
* pool's single LivenessAdmission reservation. After this, commit_submit(WAIT)
|
|
* acquires a pool blocking-admission slot (cap = n_vcpus - VTASK_LIVENESS_RESERVED)
|
|
* instead of using the local COMMIT_WAIT_RESERVED cap. pool == NULL restores
|
|
* the local fallback. Call once at wiring time (cluster_init_phase2), after the
|
|
* pool's liveness reservation is set. */
|
|
void commit_set_pool(commit_engine_t* ce, vtaskmgr_t* pool);
|
|
|
|
/* Register entry at append time. RETAIN returns 0 immediately.
|
|
* WAIT blocks up to timeout_ms: 0 = committed; -ETIMEDOUT = not committed
|
|
* (entry remains in log; outcome resolves per umbrella 2.5); -XCE_ERR_NOT_READY =
|
|
* admission cap exhausted (only cause). An unmet own-term gate does NOT fail
|
|
* fast: the gate is part of the completion predicate, so WAIT blocks within
|
|
* timeout_ms until the gate opens, surfacing -ETIMEDOUT if it never does. */
|
|
int commit_submit(commit_engine_t* ce, uint64_t lsn, commit_mode_t mode,
|
|
uint32_t timeout_ms);
|
|
|
|
/* raft-apply-decoupling task 1.1: register a continuation for lsn instead of
|
|
* parking the calling thread -- the event-driven equivalent of
|
|
* commit_submit(..., COMMIT_MODE_WAIT, ...). Returns 0 on successful
|
|
* registration; -EINVAL on bad args (ce/p NULL, lsn == 0, fn == NULL);
|
|
* -XCE_ERR_NOT_READY when the admission cap is exhausted (the same shared
|
|
* discipline commit_submit's WAIT mode uses -- see wait_admit_locked).
|
|
* Never blocks: the calling thread returns immediately, before lsn's
|
|
* outcome is known.
|
|
*
|
|
* raft-apply-decoupling task 1.3: registration also arms an implicit
|
|
* DEFAULT_SYNC_TIMEOUT_MS deadline (identical constant and clock the
|
|
* blocking WAIT path uses) -- if lsn has not resolved by then, the commit
|
|
* engine's own timeout-sweep thread resolves it FAILED/-ETIMEDOUT with no
|
|
* caller action required. A genuine majority ack that lands at or after the
|
|
* deadline still wins: the sweep thread always checks the majority
|
|
* predicate before the deadline in the same lock hold, mirroring
|
|
* commit_submit's own last-look-before-timeout tie-break. There is no
|
|
* per-call timeout parameter (unlike commit_submit) -- see design.md
|
|
* §Commit Engine Resolve for the fixed signature. */
|
|
int commit_submit_async(commit_engine_t* ce, uint64_t lsn,
|
|
commit_continuation_fn fn, void* ctx);
|
|
|
|
/* raft-apply-decoupling task 1.2: resolve lsn's registered continuation (if
|
|
* any) with the given outcome/rc, exactly once. Called internally on the
|
|
* majority-commit path (commit_advance_locked and its callers, via the
|
|
* wait_done_locked predicate -- mirrors COMMIT_MODE_WAIT's own completion
|
|
* check) and is also the PUBLIC failure-path hook: a future caller detecting
|
|
* timeout, demotion, or log-tail truncation for a pending lsn calls this
|
|
* directly with COMMIT_OUTCOME_FAILED and the appropriate rc (the existing
|
|
* error-code vocabulary -- e.g. -ETIMEDOUT, -XCE_ERR_STANDBY). Idempotent: a
|
|
* second resolve() for an lsn with no registered (or already-resolved)
|
|
* continuation is a silent no-op -- lsn == 0 is the free-slot sentinel, so a
|
|
* cleared slot never matches again. Never invokes the continuation inline on
|
|
* the calling thread: dispatch happens off-thread via vtask_enq_classified
|
|
* (VTASK_LIVE_BLOCK -- never admission-gated, so delivery cannot fast-reject
|
|
* the way VTASK_REQ_BLOCK could at the pool's blocking cap), falling back to
|
|
* an inline call ONLY if dispatch is unavailable (OOM, or no vtaskmgr pool
|
|
* wired -- commit-only unit fixtures), because a continuation must never be
|
|
* silently left unresolved (Req 9.4; "controllers never self-terminate" —
|
|
* fail loud and complete the work rather than drop it). Safe to call from any
|
|
* thread that does NOT already hold ce->lock. */
|
|
void commit_resolve(commit_engine_t* ce, uint64_t lsn, commit_outcome_t outcome,
|
|
int rc);
|
|
|
|
/* iomgr path: record standby ACK, evaluate majority, advance committed_lsn
|
|
* contiguously, wake satisfied waiters. Never blocks. */
|
|
void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn);
|
|
|
|
/* Promotion: set own-term gate to the no-op entry's LSN.
|
|
* (API ships here; no caller in this spec — unset = no gate.) */
|
|
void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn);
|
|
|
|
/* ha-term-safety commit-ceiling-optA: record the raft library's commit_index
|
|
* for OBSERVABILITY / TRACE / conformance-oracle ONLY (monotonic max() — never
|
|
* regresses). This does NOT gate commit-advance: the durable-majority barrier +
|
|
* own-term writable gate are authoritative. Still fed by on_commit_advanced so
|
|
* the library half keeps a sink. Re-evaluates the (commit_index-independent)
|
|
* durable majority and wakes waiters as a convenient ACK-path wake point. Never
|
|
* blocks. */
|
|
void commit_set_raft_commit_index(commit_engine_t* ce, uint64_t commit_index);
|
|
|
|
/* Quorum-membership change (standby add/remove/catch-up transition):
|
|
* re-evaluate pending entries; standbys in catch-up are excluded. */
|
|
void commit_on_membership_change(commit_engine_t* ce);
|