feat(raft-apply-decoupling): commit engine resolve primitive + majority-ack wiring (task 1.2)

commit_resolve() delivers a registered continuation's outcome exactly
once (idempotent re-resolve), dispatched off-thread via
vtask_enq_classified(VTASK_LIVE_BLOCK) so it never runs inline on the
replication-ACK or iomgr path. Wired into all 6 commit.c-internal
paths that can advance committed_lsn/writable_lsn.

Review caught a real task-graph gap, not a defect: converting
COMMIT_MODE_WAIT away from a blocking wait also removes the only
mechanism that ever detected a commit timing out (the caller's own
cond_timedwait deadline). Nothing yet arms an equivalent timeout for
async-registered continuations, and nothing wires demotion/log-truncation
detection to commit_resolve(FAILED, ...) -- left as-is, a pending async
command during quorum loss would hang forever instead of failing,
which is worse than today. Split into new tasks 1.3 (timeout arming)
and 1.4 (demotion/truncation wiring), renumbered the old concurrency
test to 1.5, and made 3.2 depend on both since its failure/timeout
observable is meaningless without them. design.md's File Structure
Plan updated to reflect that ha.c/persist.c need a narrow touch for
1.4's detection hooks.
This commit is contained in:
Rongsong Shen 2026-07-03 09:03:50 +00:00
parent 10d157f8c5
commit fd7a0746eb
5 changed files with 558 additions and 19 deletions

View file

@ -44,6 +44,12 @@
#include <assert.h>
#endif
/* Forward declaration: raft-apply-decoupling task 1.2. commit_on_durable_advance
* (below) is defined ahead of the Continuation Resolution section further down
* this file but must call commit_resolve_pending after releasing ce->lock; see
* that section for the full definition and contract. */
static void commit_resolve_pending(commit_engine_t* ce);
/* ============================================================
* Internal helpers
* ============================================================ */
@ -290,6 +296,11 @@ static void commit_on_durable_advance(void* ctx)
* broadcasts on a committed_lsn change, so cover the no-change case here. */
pthread_cond_broadcast(&ce->cond);
pthread_mutex_unlock(&ce->lock);
/* raft-apply-decoupling task 1.2: the durable watermark moving can newly
* satisfy a registered continuation's predicate exactly like it does a
* blocked COMMIT_MODE_WAIT caller's -- resolve after ce->lock is released. */
commit_resolve_pending(ce);
}
/* ============================================================
@ -443,6 +454,11 @@ int commit_submit(commit_engine_t* ce, uint64_t lsn, commit_mode_t mode,
gate = ce->writable_lsn;
pthread_mutex_unlock(&ce->lock);
/* raft-apply-decoupling task 1.2: the n_peers<=1 self-quorum branch above
* can advance committed_lsn via this same lock section -- resolve any
* newly-satisfied registered continuation after ce->lock is released. */
commit_resolve_pending(ce);
if (rc == -ETIMEDOUT) {
WARN("commit: WAIT deadline expired without commit "
"(lsn=%lu committed_lsn=%lu writable_lsn=%lu timeout_ms=%u)",
@ -519,6 +535,179 @@ int commit_submit_async(commit_engine_t* ce, uint64_t lsn,
return 0;
}
/* ============================================================
* Continuation resolution (raft-apply-decoupling task 1.2)
* ============================================================ */
/* Heap payload for one dispatched continuation invocation. Owned by the
* enqueued vtask's vtask_data_t.u.generic; vtask_fini/run_task only free the
* vtask_t and its vtask_data_t wrapper, never this payload, so the dispatch
* entry function below frees it after invoking fn. */
typedef struct {
commit_continuation_fn fn;
void* ctx;
uint64_t lsn;
commit_outcome_t outcome;
int rc;
} commit_resolve_dispatch_t;
/* vtask entry (run on a pool worker, never the calling thread): invoke the
* continuation and free its dispatch payload. */
static int commit_resolve_dispatch_entry(vtask_t* task)
{
commit_resolve_dispatch_t* d =
(commit_resolve_dispatch_t*)task->data->u.generic;
d->fn(d->ctx, d->lsn, d->outcome, d->rc);
free(d);
return 0;
}
/* Build + enqueue a generic vtask that invokes fn(ctx, lsn, outcome, rc) off
* the calling thread. Caller must NOT hold ce->lock.
*
* Class VTASK_LIVE_BLOCK, not VTASK_REQ_BLOCK or VTASK_LIVENESS:
* - VTASK_REQ_BLOCK is admission-gated (vtask_enq_classified can fast-reject
* with -E_XCE_NOT_READY at the pool's blocking cap) -- unacceptable here:
* Req 9.4 requires the continuation be resolved exactly once, never left
* unresolved, and a rejected enqueue would silently drop it.
* - VTASK_LIVENESS is never admission-gated either, but its reservation
* (vtaskmgr.h) is sized and reserved for the narrow WAL-ack/heartbeat/
* vote receipt+send set -- not general apply/notify continuations.
* - VTASK_LIVE_BLOCK is never admission-gated (same delivery guarantee as
* LIVENESS) and is documented as "bounded mandatory park ... runs in the
* reserved headroom" -- the correct fit for mandatory, must-not-reject
* work that is not itself the narrow liveness set.
*
* On allocation/enqueue failure (OOM, or no vtaskmgr pool wired -- true only
* of commit-only unit fixtures; production always wires the pool before any
* commit activity, cluster_init_phase2) this fails loud and invokes the
* continuation inline as a last resort: "controllers never self-terminate" --
* a continuation must never be silently dropped, so a rare fallback inline
* call is preferred over an unresolved continuation. */
static void dispatch_continuation(commit_engine_t* ce, commit_continuation_fn fn,
void* ctx, uint64_t lsn,
commit_outcome_t outcome, int rc)
{
vtask_data_t* data;
vtask_t* task;
commit_resolve_dispatch_t* d;
if (fn == NULL) {
return;
}
data = calloc(1, sizeof(*data));
task = vtask_alloc();
d = calloc(1, sizeof(*d));
if (ce->pool == NULL || data == NULL || task == NULL || d == NULL) {
WARN("commit: continuation dispatch unavailable (pool=%p data=%p "
"task=%p d=%p) for lsn=%lu outcome=%d rc=%d -- invoking inline "
"(stay alive, never leave a continuation unresolved)",
(void*)ce->pool, (void*)data, (void*)task, (void*)d, lsn,
(int)outcome, rc);
free(data);
free(task);
free(d);
fn(ctx, lsn, outcome, rc);
return;
}
d->fn = fn;
d->ctx = ctx;
d->lsn = lsn;
d->outcome = outcome;
d->rc = rc;
data->vtype = VTASK_GENERIC;
data->u.generic = d;
vtask_init(task, (vtask_func_t)commit_resolve_dispatch_entry, data);
/* VTASK_LIVE_BLOCK is never admission-gated -- see the function comment
* above; this cannot fast-reject the way VTASK_REQ_BLOCK could. */
if (vtask_enq_classified(ce->pool, task, VTASK_LIVE_BLOCK) < 0) {
WARN("commit: continuation vtask enqueue failed for lsn=%lu -- "
"invoking inline (stay alive)", lsn);
free(data);
free(task);
free(d);
fn(ctx, lsn, outcome, rc);
}
}
/* Resolve lsn's registered continuation (if any) exactly once: find + clear
* its slot under ce->lock (idempotent -- a cleared slot's lsn is 0 and never
* matches again), release its admission hold (same shared budget as
* COMMIT_MODE_WAIT), then dispatch off-thread AFTER ce->lock is released
* (lock-ordering rule, design §Commit Engine Resolve). Caller must NOT hold
* ce->lock. */
void commit_resolve(commit_engine_t* ce, uint64_t lsn, commit_outcome_t outcome,
int rc)
{
commit_continuation_fn fn = NULL;
void* ctx = NULL;
if (ce == NULL || ce->p == NULL || lsn == 0) {
return;
}
pthread_mutex_lock(&ce->lock);
for (int i = 0; i < ce->n_continuations; i++) {
if (ce->continuations[i].lsn == lsn) {
fn = ce->continuations[i].fn;
ctx = ce->continuations[i].ctx;
ce->continuations[i].lsn = 0; /* free the slot -- idempotency */
ce->continuations[i].fn = NULL;
ce->continuations[i].ctx = NULL;
wait_release_locked(ce);
break;
}
}
pthread_mutex_unlock(&ce->lock);
if (fn != NULL) {
dispatch_continuation(ce, fn, ctx, lsn, outcome, rc);
}
}
/* Scan the continuation registry for entries whose WAIT-equivalent completion
* predicate (wait_done_locked -- own entry committed AND the own-term gate,
* when set, reached) is now satisfied, and resolve each with COMMITTED, rc=0
* -- the async-registration mirror of what a woken COMMIT_MODE_WAIT caller's
* own re-check observes. Caller must NOT hold ce->lock: this takes the lock
* itself for a bounded snapshot scan, then resolves each candidate from an
* unlocked context via the self-locking commit_resolve() (satisfying the
* dispatch-after-unlock ordering rule). Safe under races: a snapshotted lsn
* already resolved by a concurrent caller by the time commit_resolve() runs
* is simply a no-op (idempotent), so no continuation is ever double-resolved. */
static void commit_resolve_pending(commit_engine_t* ce)
{
int n = 0;
if (ce == NULL) {
return;
}
uint64_t ready[ce->n_continuations]; /* VLA: bounded by n_vcpus -- see the
* commit.h field-comment sizing
* invariant */
pthread_mutex_lock(&ce->lock);
for (int i = 0; i < ce->n_continuations; i++) {
uint64_t lsn = ce->continuations[i].lsn;
if (lsn != 0 && wait_done_locked(ce, lsn)) {
ready[n++] = lsn;
}
}
pthread_mutex_unlock(&ce->lock);
for (int i = 0; i < n; i++) {
commit_resolve(ce, ready[i], COMMIT_OUTCOME_COMMITTED, 0);
}
}
/* ============================================================
* ACK and membership events
* ============================================================ */
@ -538,6 +727,10 @@ void commit_on_ack(commit_engine_t* ce, int standby_idx, uint64_t acked_lsn)
pthread_mutex_lock(&ce->lock);
commit_advance_locked(ce);
pthread_mutex_unlock(&ce->lock);
/* raft-apply-decoupling task 1.2: an ACK-driven advance can newly satisfy
* a registered continuation -- resolve after ce->lock is released. */
commit_resolve_pending(ce);
}
void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn)
@ -552,6 +745,10 @@ void commit_set_writable_lsn(commit_engine_t* ce, uint64_t noop_lsn)
* committed_lsn >= writable_lsn). */
pthread_cond_broadcast(&ce->cond);
pthread_mutex_unlock(&ce->lock);
/* raft-apply-decoupling task 1.2: the gate opening can newly satisfy a
* registered continuation whose own entry already committed. */
commit_resolve_pending(ce);
}
/* ha-term-safety commit-ceiling-optA: record the raft library's commit_index
@ -588,6 +785,9 @@ void commit_set_raft_commit_index(commit_engine_t* ce, uint64_t commit_index)
commit_advance_locked(ce);
pthread_cond_broadcast(&ce->cond);
pthread_mutex_unlock(&ce->lock);
/* raft-apply-decoupling task 1.2: resolve after ce->lock is released. */
commit_resolve_pending(ce);
}
void commit_on_membership_change(commit_engine_t* ce)
@ -602,4 +802,7 @@ void commit_on_membership_change(commit_engine_t* ce)
commit_advance_locked(ce);
pthread_cond_broadcast(&ce->cond);
pthread_mutex_unlock(&ce->lock);
/* raft-apply-decoupling task 1.2: resolve after ce->lock is released. */
commit_resolve_pending(ce);
}