feat(raft-apply-decoupling): ordered live apply pump activates apply_committed_entry (task 6.1)
Build persist_apply_pump_run()/persist_apply_pump_kick() -- the ordered walk from last_applied_lsn+1 through committed_lsn, applying every entry via apply_committed_entry() regardless of commit mode. This is the first live (non-replay) consumer of that dispatch function. Kick is wired into commit_resolve_pending() (the single choke point all 6 internal committed_lsn-advancing paths funnel through, discovered in task 1.2), dispatched via vtask_enq_classified(VTASK_LIVE_BLOCK) with a CAS-coalesced fire-and-forget trigger. Fixes the two hazards flagged since task 4.1 as required before going live: 1. Double-append: a thread-local call-provenance guard (t_apply_seam_notifying) prevents persist_job_hook_fun/ persist_queue_hook_fun/persist_worker_hook_fun from re-appending entries the apply pump itself just applied, while preserving job_ctrl's legitimate live command-path appends (a naive detail-based filter would have broken SIGCONT/SIGSTOP). Also closes an undocumented instance of the same hazard in the queue/worker hook functions. 2. Merge-staleness: record_job_fail_fact/postdone's fact recording previously baked a receipt-time-computed final job state into the WAL entry, which goes stale under deferred apply -- a FINISH followed shortly by an ordinary post-exec FAIL could silently clobber a correct DONE completion with a stale RUN|EXIT|PERR, no guard catching it. Fixed by repurposing wal_jobstate_t's unused _pad field to carry just enough data (FAIL's stage) to recompute against the job's actual current state at apply time via the real apply_job_fail_transition()/new apply_job_postdone_transition() guard cores, instead of overwriting with a stale absolute value. Backward compatible: _pad==0 (all prior data) routes to the same legacy overwrite behavior. Also fixes bug-1409 (unbounded busy-loop in apply_pump_drain when committed_lsn advances without a corresponding on-disk entry) and a genuine test-flakiness hazard (a second, independent apply-pump kicker from the fsync_worker thread that existing tests didn't account for). Two pre-existing tests in test_commit_barrier_reply.c now fail as expected -- their own assertions state "no job may be constructed until task 6's apply pump exists," now correctly invalidated by this task landing; updating them is task 7.3's explicit job.
This commit is contained in:
parent
d3da3ca8bd
commit
2fa955b92e
11 changed files with 1687 additions and 135 deletions
|
|
@ -387,6 +387,20 @@ struct persist_s {
|
|||
* persist_register_pending_apply's doc comment for the full contract. */
|
||||
void* pending_applies;
|
||||
pthread_mutex_t pending_applies_lock;
|
||||
|
||||
/* raft-apply-decoupling task 6.1: ordered apply pump. apply_pump_lock
|
||||
* serializes concurrent walk passes -- multiple commit-advance triggers
|
||||
* (from any of the six commit.c paths that can move committed_lsn) can
|
||||
* race to kick the pump; only one walk actually runs at a time (see
|
||||
* persist_apply_pump_run's doc comment). apply_pump_scheduled is a
|
||||
* CAS-coalescing flag: at most one vtask-dispatched walk is ever
|
||||
* in-flight per persist_t regardless of how many trigger events land
|
||||
* concurrently, so a commit burst cannot flood the vtask pool with
|
||||
* mostly-redundant walk requests. Neither field is persisted -- both
|
||||
* reset fresh on process restart (restart applies via replay, not this
|
||||
* pump; see design.md "Restart Replay & Verification"). */
|
||||
pthread_mutex_t apply_pump_lock;
|
||||
_Atomic int apply_pump_scheduled;
|
||||
#ifdef XCE_TESTING
|
||||
/* Fault-injection seam (ha-commit-durability task 2.1 error-path test):
|
||||
* when non-zero, every fdatasync site treats the sync as FAILED — it
|
||||
|
|
@ -945,6 +959,93 @@ void persist_register_pending_apply(persist_t* p, uint64_t lsn,
|
|||
* and remove"), not built by this task. */
|
||||
int persist_has_pending_apply(persist_t* p, uint64_t lsn);
|
||||
|
||||
/* raft-apply-decoupling task 6.1: "take and remove" counterpart to
|
||||
* persist_register_pending_apply()/persist_has_pending_apply() (task 3.2),
|
||||
* built for task 6.2 to consume — task 3.2 explicitly left this accessor
|
||||
* unbuilt ("task 6.1 needs its own 'take and remove' accessor, not built
|
||||
* here"). Atomically finds the pending-apply hand-off node registered for
|
||||
* `lsn` (if any), unlinks and frees the list node, and returns its
|
||||
* `on_applied`/`ctx` pair via the output parameters.
|
||||
*
|
||||
* Returns 1 and populates *out_fn and *out_ctx when a registration was found
|
||||
* and removed; returns 0 and leaves *out_fn and *out_ctx untouched when no
|
||||
* registration exists for `lsn` (the common case for a fact entry, which
|
||||
* never registers one).
|
||||
*
|
||||
* Contract for task 6.2: call this AFTER apply_committed_entry() has already
|
||||
* applied `lsn` — this accessor does not itself apply anything, it is pure
|
||||
* bookkeeping over the hand-off list. When it returns 1, invoke
|
||||
* `(*out_fn)(*out_ctx, lsn)` exactly once; a second call for the same lsn is
|
||||
* guaranteed to return 0 (the node is already removed), so the hand-off
|
||||
* fires exactly once per registration. Safe to call for every applied lsn
|
||||
* unconditionally (facts and commands alike): it is a cheap linked-list scan
|
||||
* under pending_applies_lock and a correct no-op (returns 0) when nothing is
|
||||
* registered. Not yet called by anything — task 6.2's job, not this one's. */
|
||||
int persist_take_pending_apply(persist_t* p, uint64_t lsn,
|
||||
pending_apply_notify_fn* out_fn, void** out_ctx);
|
||||
|
||||
/* raft-apply-decoupling task 6.1: strict-order walk that applies every newly
|
||||
* committed entry, from persist_last_applied_lsn()+1 up to p->committed_lsn
|
||||
* inclusive, invoking apply_committed_entry() for each in ascending LSN
|
||||
* order — regardless of the entry's commit mode (WAIT or RETAIN): a command
|
||||
* entry that is *also* still tracked in the pending_applies hand-off list
|
||||
* (task 3.2) is applied exactly like any other entry here; task 6.2 (not
|
||||
* built by this function) is responsible for separately calling
|
||||
* persist_take_pending_apply() and firing that entry's reply continuation
|
||||
* after this function (or whatever drives it) applies it.
|
||||
*
|
||||
* Synchronous, single pass: walks until no further committed-but-unapplied
|
||||
* entry exists (as of the moment each check is made), then returns. Safe to
|
||||
* call directly — e.g. from a test, without ever going through
|
||||
* persist_apply_pump_kick's async dispatch — serializing against any
|
||||
* concurrent walk via a dedicated lock (never `p->lock`: this performs
|
||||
* blocking disk reads, and `p->lock` is the hot single-writer append lock —
|
||||
* see the lock-ordering invariant, persist.c ~3347-3350).
|
||||
*
|
||||
* Returns the number of entries applied (>= 0) on success, or a negative
|
||||
* errno if reading a committed entry's bytes back off the WAL failed — a
|
||||
* genuine, unexpected condition for an already-committed entry (logged
|
||||
* loud; per design.md's Error Handling, this is treated as an apply-time
|
||||
* failure, not a retry-immediately condition). The walk stops at the failed
|
||||
* LSN; last_applied_lsn is left unadvanced past it, so the SAME entry is
|
||||
* retried, from the same position, by the next kick — never silently
|
||||
* skipped. */
|
||||
int persist_apply_pump_run(persist_t* p);
|
||||
|
||||
/* raft-apply-decoupling task 6.1: async trigger for persist_apply_pump_run()
|
||||
* above. Intended to be called from every commit.c-internal path that can
|
||||
* advance p->committed_lsn (commit_resolve_pending, after ce->lock is
|
||||
* released) so the pump reacts to ALL SIX advance paths task 1.2 identified
|
||||
* (durable-advance, self-quorum commit_submit, ack-driven advance, the
|
||||
* writable-lsn gate, the raft-library commit-index feed, and membership
|
||||
* change) — not just the one raft-library-fed path
|
||||
* (ha_cb_commit_advanced_stub/on_commit_advanced) design.md names
|
||||
* explicitly, which alone would starve the pump under a single-node/
|
||||
* self-quorum cluster that never exercises that specific callback.
|
||||
*
|
||||
* No-op when p is NULL or p->cluster is NULL (mirrors apply_committed_entry's
|
||||
* own bare-persist-fixture convention) — every existing commit-engine-only /
|
||||
* persist-only unit fixture that never wires a cluster_t is completely
|
||||
* unaffected by this call; the pump only ever activates for a real cluster
|
||||
* context.
|
||||
*
|
||||
* `pool` is a `vtaskmgr_t*` (declared void* here to avoid persist.h
|
||||
* depending on vtaskmgr.h, mirroring pending_apply_notify_fn's existing
|
||||
* avoidance of a commit.h dependency) used to dispatch the walk off the
|
||||
* calling thread — never inline on what may be the replication-ACK/iomgr
|
||||
* thread — via vtask_enq_classified(VTASK_LIVE_BLOCK): same class and the
|
||||
* same inline-on-dispatch-failure fallback discipline as commit.c's own
|
||||
* dispatch_continuation, so a walk is never silently dropped even under OOM
|
||||
* or a NULL/not-yet-wired pool (a NULL pool falls back to an inline,
|
||||
* synchronous run — exactly the "commit-only unit fixture" case
|
||||
* dispatch_continuation's own doc comment already documents).
|
||||
*
|
||||
* Never blocks the caller beyond an O(1) CAS + enqueue (the actual walk
|
||||
* always runs asynchronously on a pool worker, except the documented
|
||||
* NULL-pool/allocation-failure fallback). Coalesces concurrent kicks: at
|
||||
* most one walk is ever scheduled or running at a time per persist_t. */
|
||||
void persist_apply_pump_kick(persist_t* p, void* pool);
|
||||
|
||||
int persist_job_hook_fun(int type, int situation, int detail,
|
||||
void* cls, va_list ap);
|
||||
int persist_queue_hook_fun(int type, int situation, int detail,
|
||||
|
|
@ -960,13 +1061,29 @@ int persist_queue_hook_fun(int type, int situation, int detail,
|
|||
* mutation this fact eventually drives, and the AFTER_EVENT notification
|
||||
* itself, both happen later from apply_committed_entry() once this entry
|
||||
* commits to a majority (task 2.2's existing dispatch). `state` is the
|
||||
* caller-computed resultant state (e.g. JOB_STAT_RUN for a START, or the
|
||||
* PERR-merged value for a FAIL) — this function performs no guard checks or
|
||||
* merge logic of its own, it only encodes and appends.
|
||||
* caller-computed resultant state (e.g. JOB_STAT_RUN for a START) — this
|
||||
* function performs no guard checks or merge logic of its own, it only
|
||||
* encodes and appends. Thin wrapper over persist_submit_job_fact_ex() with
|
||||
* pad = 0 (the "legacy absolute state" discriminator — see wal_types.h's
|
||||
* doc comment on wal_jobstate_t._pad) — unchanged contract, every existing
|
||||
* caller (START/FINISH/postdone's old shape) keeps working identically.
|
||||
* Returns the local-append rc (0 on success); facts have no commit timeout
|
||||
* to report, mirroring wal_hook_append_classified's own contract. */
|
||||
int persist_submit_job_fact(persist_t* p, uint32_t wal_type, uint64_t jobid,
|
||||
int state);
|
||||
|
||||
/* Full-control sibling of persist_submit_job_fact() (raft-apply-decoupling
|
||||
* task 6.1 round 2, Req 3.3/7.1-7.3): lets the caller set wal_jobstate_t's
|
||||
* `_pad` discriminator explicitly, for the two fact producers whose real
|
||||
* mutation must be re-derived at apply time against the job's CURRENT
|
||||
* state rather than trusting a receipt-time snapshot (record_job_fail_fact,
|
||||
* record_job_postdone_fact — see wal_types.h's doc comment on
|
||||
* wal_jobstate_t._pad for the full rationale). `state` is ignored by
|
||||
* apply_committed_entry() whenever `pad` selects one of these apply-time-
|
||||
* recomputed shapes (its value is not part of the wire contract in that
|
||||
* case, but is still written for forward-compatibility/diagnostics). */
|
||||
int persist_submit_job_fact_ex(persist_t* p, uint32_t wal_type,
|
||||
uint64_t jobid, int state, uint32_t pad);
|
||||
int persist_worker_hook_fun(int type, int situation, int detail,
|
||||
void* cls, va_list ap);
|
||||
|
||||
|
|
@ -990,9 +1107,27 @@ int apply_wal_entry(persist_t* p, uint32_t type, uint64_t lsn,
|
|||
* WAL_JOB_SUBMIT -> restore_job() (existing
|
||||
* idempotent, jobid-addressable construction primitive; -EEXIST is a
|
||||
* no-op re-apply, not an error)
|
||||
* WAL_JOB_{START,FINISH,FAIL,STOP,RESUME,
|
||||
* UPDATE,RUSAGE,CKPT,PREASON} -> apply_job_update_transition()
|
||||
* (the guard-only core extracted in task 2.1)
|
||||
* WAL_JOB_{START,FINISH,STOP,RESUME,
|
||||
* RUSAGE,CKPT,PREASON} -> apply_job_update_transition()
|
||||
* (the guard-only core extracted in task 2.1) — these types' payloads
|
||||
* are deterministic/state-independent, so a `state` value baked at
|
||||
* receipt time never goes stale
|
||||
* WAL_JOB_FAIL -> dispatches on wal_jobstate_t.
|
||||
* _pad (task 6.1 round 2 — see wal_types.h's doc comment on
|
||||
* wal_jobstate_t._pad): WAL_JOB_FAIL_STAGE_FLAG set -> reconstructs a
|
||||
* minimal job_failinfo_t{jobid, stage} and calls the REAL
|
||||
* apply_job_fail_transition() against the job's ACTUAL CURRENT state
|
||||
* at apply time — fixes a merge-staleness corruption where a FAIL's
|
||||
* PERR merge depended on prior state that had not actually applied
|
||||
* yet when the fact was received; _pad == 0 (legacy live path,
|
||||
* job_ctrl's mark_job_fail) -> apply_job_update_transition() as an
|
||||
* absolute overwrite, unchanged
|
||||
* WAL_JOB_UPDATE -> also dispatches on _pad:
|
||||
* WAL_JOB_UPDATE_POSTDONE_MARK -> apply_job_postdone_transition(),
|
||||
* which re-derives the postdone merge (conditional EXIT backstop + OR
|
||||
* JOB_STAT_PDONE) fresh against current state; _pad == 0 (legacy live
|
||||
* path, job_ctrl's mark_job_update) -> apply_job_update_transition()
|
||||
* as an absolute overwrite, unchanged
|
||||
* WAL_QUEUE_CTRL -> restore_queue_state()
|
||||
* WAL_WORKER_CTRL -> worker_from_capnp()
|
||||
* and fires the entry type's AFTER_EVENT notification hook exactly once,
|
||||
|
|
@ -1011,12 +1146,14 @@ int apply_wal_entry(persist_t* p, uint32_t type, uint64_t lsn,
|
|||
* `.data = { .pidinfo = info }`) inside `case HOOK_AFTER_EVENT:`, without
|
||||
* dereferencing any of its fields at this call site — unlike sched.c's
|
||||
* unconditional `info->jobid` access in a top-level struct initializer).
|
||||
* Every job-state fact type therefore routes through the
|
||||
* guard-only apply_job_update_transition() core and fires
|
||||
* Every job-state fact type therefore fires the same
|
||||
* HOOK_DETAIL_JOB_UPDATE, whose subscribers take only (cluster_t*, job_t*) —
|
||||
* matching what this wire format can actually supply without fabricating
|
||||
* data. No task in the current plan (2.3, 4.1, 4.2, 6.1, 6.2, 6.3) closes
|
||||
* this gap — task 4.1 is exclusively about removing state mutation/hook-
|
||||
* data — regardless of which of the guard-only cores above (apply_job_
|
||||
* update_transition / apply_job_fail_transition / apply_job_postdone_
|
||||
* transition) actually performed the mutation. No task in the current plan
|
||||
* (2.3, 4.1, 4.2, 6.1, 6.2, 6.3) closes this hook-granularity gap — task 4.1
|
||||
* is exclusively about removing state mutation/hook-
|
||||
* firing from worker-fact receipt-time handlers, not about enriching the
|
||||
* wire format. Closing it would require either a design.md correction
|
||||
* (formally accepting this HOOK_DETAIL_JOB_UPDATE substitution as final —
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue