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:
parent
10d157f8c5
commit
fd7a0746eb
5 changed files with 558 additions and 19 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue