feat(raft-apply-decoupling): build apply_committed_entry single dispatch seam (task 2.2)

Add apply_committed_entry(persist_t*, wal_type, lsn, payload, len) in
persist.c/persist.h as the single seam that turns a committed WAL
entry into an in-memory mutation plus its post-mutation notification
hook, covering job submit, job-state facts, queue control, and
worker control. Job-state facts route through 2.1's guard-only
apply_job_update_transition() core and fire a generic
HOOK_DETAIL_JOB_UPDATE, not per-type hooks — the wire format
(wal_jobstate_t {jobid, state}) cannot supply what the type-specific
cores/hooks require. This is a real, documented deviation from
design.md's per-type dispatch wording; tasks.md's Implementation
Notes flags it as unresolved and blocking for whoever wires this
into a live path (4.1/6.2/6.3).

Not yet wired into any live call site (replay wiring is 2.3).
This commit is contained in:
Rongsong Shen 2026-07-03 16:51:47 +00:00
parent 41d36204d5
commit 8c00f20cec
6 changed files with 689 additions and 1 deletions

View file

@ -2578,6 +2578,186 @@ int apply_wal_entry(persist_t* p, uint32_t type, uint64_t lsn,
return 0;
}
/* raft-apply-decoupling task 2.2 — see persist.h for the full contract doc
* (routing table, the job-state-family hook-mapping deviation, and what this
* function deliberately does NOT own). Not yet wired into any live call
* site — replay (2.3) and the live apply pump (6.1) will call this later. */
int apply_committed_entry(persist_t* p, uint32_t wal_type, uint64_t lsn,
const void* payload, uint32_t len)
{
if (!p)
return -EINVAL;
if (!p->cluster)
return 0; /* unit tests without a full cluster — nothing to dispatch into */
cluster_t* c = p->cluster;
switch (wal_type) {
case WAL_JOB_SUBMIT: {
int rc = restore_job(c, 0, payload, len, lsn);
if (rc == -EEXIST)
return 0; /* idempotent re-apply (replay / resync) — no hook */
if (rc < 0)
return rc;
/* Recover the jobid via a cheap raw capnp field read (read_Job only
* reads the fixed uint64 field — no allocation, unlike decode_Job)
* so the just-constructed job can be looked up for the notification
* hook without changing restore_job's own contract. */
struct capn jc_ctx;
capn_init_mem(&jc_ctx, (const uint8_t*)payload, len, 0);
Job_ptr jp;
jp.p = capn_getp(capn_root(&jc_ctx), 0, 1);
struct Job cj;
read_Job(&cj, jp);
uint64_t jobid = cj.jobid;
capn_free(&jc_ctx);
job_t* j = NULL;
lock(c->jobmgr, RTTI_LOCK_READ);
key_find(c->jobmgr, (void*)jobid, (void**)&j);
unlock(c->jobmgr);
if (!j)
return 0; /* defensive: should not happen after a successful restore */
void* jptr = j;
xlist_t jlist = { .count = 1, .caps = 1, .delta = 1, .data = &jptr };
xce_run_hooks(&c->hooksys, XCE_CONTROLLER_HOOK_JOB, HOOK_AFTER_EVENT,
HOOK_DETAIL_JOB_NEW, c, &jlist);
return 0;
}
case WAL_JOB_START:
case WAL_JOB_FINISH:
case WAL_JOB_FAIL:
case WAL_JOB_STOP:
case WAL_JOB_RESUME:
case WAL_JOB_UPDATE:
case WAL_JOB_RUSAGE:
case WAL_JOB_CKPT:
case WAL_JOB_PREASON: {
/* Every job-state fact type shares one wire shape — wal_jobstate_t
* {jobid, state} — and therefore one guarded core. See persist.h's
* doc comment on apply_committed_entry for why this does not
* dispatch to the type-specific apply_job_{start,finish,fail}_
* transition() cores (their richer parameter structs are not
* present on this wire format). */
if (len < sizeof(wal_jobstate_t))
return 0; /* undersized payload — skip, mirrors apply_wal_entry */
const wal_jobstate_t* js = (const wal_jobstate_t*)payload;
job_t* j = NULL;
lock(c->jobmgr, RTTI_LOCK_READ);
key_find(c->jobmgr, (void*)js->jobid, (void**)&j);
unlock(c->jobmgr);
if (!j)
return -ENOENT;
lock(j, RTTI_LOCK_WRITE);
job_transition_result_t result =
apply_job_update_transition(j, (int)js->state);
unlock(j);
if (result != JOB_TRANSITION_APPLIED)
return 0; /* rejected-backward / no-op-duplicate — no hook */
xce_run_hooks(&c->hooksys, XCE_CONTROLLER_HOOK_JOB, HOOK_AFTER_EVENT,
HOOK_DETAIL_JOB_UPDATE, c, j);
return 0;
}
case WAL_QUEUE_CTRL: {
if (len <= sizeof(wal_queuehdr_t))
return 0; /* undersized payload — skip, mirrors apply_wal_entry */
const wal_queuehdr_t* qh = (const wal_queuehdr_t*)payload;
const char* name =
(const char*)((const uint8_t*)payload + sizeof(wal_queuehdr_t));
/* restore_queue_state's -ENOENT ("queue not yet built from config")
* and -EINVAL (bad name) are both benign skips here, matching
* apply_wal_entry's existing behavior of never fencing on this
* entry type's restore outcome. */
if (restore_queue_state(c, name, qh->status) < 0)
return 0;
queue_t* q = NULL;
lock(c->qmgr, RTTI_LOCK_READ);
key_find(c->qmgr, (void*)name, (void**)&q);
unlock(c->qmgr);
if (!q)
return 0;
xce_run_hooks(&c->hooksys, XCE_CONTROLLER_HOOK_QUEUE, HOOK_AFTER_EVENT,
HOOK_DETAIL_QUEUE_UPDATE, c, q);
return 0;
}
case WAL_WORKER_CTRL: {
if (len <= WORKER_WAL_HDR_SIZE)
return 0; /* undersized payload — skip, mirrors apply_wal_entry */
uint32_t hook_detail;
memcpy(&hook_detail, payload, sizeof(hook_detail));
int rc = worker_from_capnp(c, payload, len);
if (rc == -EEXIST)
return 0; /* idempotent re-apply (replay / resync) — no hook */
if (rc < 0)
return rc;
/* Recover the worker's name via a cheap raw capnp field read
* (read_Worker's capn_get_text is a borrowed, NUL-terminated pointer
* into the decode buffer — no allocation, unlike decode_Worker) so
* the just-restored worker can be looked up for the notification
* hook without changing worker_from_capnp's own contract. */
const uint8_t* wpayload = (const uint8_t*)payload + WORKER_WAL_HDR_SIZE;
size_t wlen = len - WORKER_WAL_HDR_SIZE;
struct capn wc_ctx;
capn_init_mem(&wc_ctx, wpayload, wlen, 0);
Worker_ptr wp;
wp.p = capn_getp(capn_root(&wc_ctx), 0, 1);
struct Worker cw;
read_Worker(&cw, wp);
worker_t* w = NULL;
lock(c->workermgr, RTTI_LOCK_READ);
key_find(c->workermgr, (void*)cw.name.str, (void**)&w);
unlock(c->workermgr);
capn_free(&wc_ctx);
if (!w)
return 0; /* defensive: should not happen after a successful restore */
switch (hook_detail) {
case HOOK_DETAIL_WORKER_JOIN:
xce_run_hooks(&c->hooksys, XCE_CONTROLLER_HOOK_WORKER,
HOOK_AFTER_EVENT, HOOK_DETAIL_WORKER_JOIN, c,
w->host.name, w);
break;
case HOOK_DETAIL_WORKER_UPDATE:
xce_run_hooks(&c->hooksys, XCE_CONTROLLER_HOOK_WORKER,
HOOK_AFTER_EVENT, HOOK_DETAIL_WORKER_UPDATE, c, w);
break;
case HOOK_DETAIL_WORKER_DEAD:
xce_run_hooks(&c->hooksys, XCE_CONTROLLER_HOOK_WORKER,
HOOK_AFTER_EVENT, HOOK_DETAIL_WORKER_DEAD, c, w);
break;
default:
break; /* unrecognized discriminator — mutation already applied,
* notification skipped rather than guessed at */
}
return 0;
}
default:
/* WAL_NOOP / WAL_CONFIG_MARKER / WAL_CHECKPOINT / unknown — no
* entity effect at this seam, no hook (unchanged from
* apply_wal_entry's existing treatment of these types). */
return 0;
}
}
/* Read WAL segment files in LSN order; replay entries with lsn > from_lsn.
*
* Segment filename format: seg-<start_lsn_hex16>.wal