feat(raft-apply-decoupling): resolve pending continuations on demotion/truncation (task 1.4)

commit_resolve_pending_above(ce, lsn_floor, rc) fails every registered
continuation for lsn > lsn_floor, reusing commit_resolve()'s existing
exactly-once/idempotent guarantee (no parallel bookkeeping path).

Truncation: a new on_truncate/truncate_cb_ctx callback on persist_t
(same inversion-of-control shape as the existing on_durable_advance
hook) fires from persist_truncate_wal_above after releasing p->lock,
only when entries were actually removed. This one wrapper covers all
three real truncation call sites: raft_cb_become_leader's
post-election truncate, the library's on_truncate_from
conflict-reconcile, and ha_demotion_convergence_fn's post-demotion
truncate.

Demotion: raft_cb_become_follower calls
commit_resolve_pending_above(&c->commit, 0, -XCE_ERR_STANDBY) --
lsn_floor=0 fails everything, since a demoted node can never
authoritatively confirm anything still pending (CP posture).

Without this, a demoted or truncated node would leave any pending
async command permanently unresolved -- the requester would simply
never get a reply, worse than today's bounded-timeout behavior.

Also closes bug-1407 (pre-existing test_config_repl flake, unrelated
to this feature) -- confirmed fixed as a side effect of prior tasks
in this series, 70/70 passing across 3 independent runs.
This commit is contained in:
Rongsong Shen 2026-07-03 11:58:10 +00:00
parent 530a02bda3
commit 9b4b805a27
9 changed files with 748 additions and 301 deletions

View file

@ -604,6 +604,12 @@ int persist_init(persist_t* p, persist_config_t* cfg, cluster_t* c)
p->on_durable_advance = NULL;
p->durable_cb_ctx = NULL;
/* Truncation wake hook starts unregistered (raft-apply-decoupling task
* 1.4): the commit engine registers it in commit_engine_init. Until
* then every truncation is a no-op wake. */
p->on_truncate = NULL;
p->truncate_cb_ctx = NULL;
/* Per-LSN term index starts empty (ha-term-safety 1.3, R1.5): no entries
* recorded yet. Grown on demand by record_entry_term during replay/append;
* freed in persist_fini. */
@ -1279,6 +1285,17 @@ void persist_set_durable_cb(persist_t* p, void (*fn)(void* ctx), void* ctx)
p->on_durable_advance = fn;
}
/* raft-apply-decoupling task 1.4: registration counterpart of
* persist_set_durable_cb -- see persist.h for the full contract. */
void persist_set_truncate_cb(persist_t* p, void (*fn)(void* ctx, uint64_t cutoff_lsn),
void* ctx)
{
if (!p)
return;
p->truncate_cb_ctx = ctx;
p->on_truncate = fn;
}
#ifdef XCE_TESTING
void persist_test_set_force_fsync_fail(persist_t* p, int fail)
{
@ -3681,8 +3698,25 @@ static void clamp_durable_lsn_locked(persist_t* p, uint64_t tip)
int persist_truncate_wal_above(persist_t* p, uint64_t cutoff_lsn)
{
pthread_mutex_lock(&p->lock);
int rc = truncate_wal_above_locked(p, cutoff_lsn);
uint64_t before = p->current_lsn;
int rc = truncate_wal_above_locked(p, cutoff_lsn);
uint64_t after = p->current_lsn;
pthread_mutex_unlock(&p->lock);
/* raft-apply-decoupling task 1.4: fire the truncation wake only when a
* truncation actually removed entries above cutoff_lsn (current_lsn
* strictly decreased -- both truncate_wal_above_locked branches set
* current_lsn = cutoff_lsn + 1 on a real truncation) -- mirrors the
* durable-advance wake's own before/after guard (see fsync_worker
* above) so a no-op truncate (nothing above cutoff_lsn, or an I/O
* error before current_lsn is touched) never spuriously fails a
* continuation whose entry is still safely in the log. Called AFTER
* releasing p->lock: the callback (the commit engine) takes only
* ce->lock, preserving the cycle-free p->lock -> ce->lock edge already
* established by the durable-advance wake. */
if (rc == 0 && after < before && p->on_truncate != NULL) {
p->on_truncate(p->truncate_cb_ctx, cutoff_lsn);
}
return rc;
}