fix(controller): WAL_JOB_CKPT loses job.ckptinfo on replay (bug-1691)

Same class as bug-1678/1687/1689/1690: on_job_ckpt() sets job.ckptinfo
(a plain char*, xstring-backed via makeString()) but is purely hook-driven
-- persist_job_hook_fun's default: arm only ever captured {jobid, state}
for WAL_JOB_CKPT. Neither apply seam restored ckptinfo after a restart.

Simplest of the five field-parity fixes: ckptinfo is a bare string, so no
capnp struct is needed -- mirrors WAL_QUEUE_CTRL's existing NUL-terminated-
string-after-header wire shape instead. Adoption is unconditional (not
gated on a state transition), matching bug-1690's precedent: a checkpoint
report never changes job.state. restore_job_ckpt_state() releases the
prior value via dereference() (the correct RTTI verb for an xstring), not
free().

Found by an /xce-audit-invariants controller pass (field-complete apply
parity dimension) -- the last of five sibling gaps found together
(bug-1678, bug-1687, bug-1689, bug-1690, this one). 7 new tests in
test_wal_job_ckpt_fact.c; RED/GREEN independently verified (5 genuine
pre-fix failures, 2 backward-compat tests correctly passed unchanged).
Full controller suite: 1167 tests, 2 failures -- both the pre-existing,
unrelated bug-1561 overlayfs sandbox flake. No ASAN findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
This commit is contained in:
Rongsong Shen 2026-08-25 12:29:10 +00:00
parent b2a8fe4ce9
commit 977607615f
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 955 additions and 3 deletions

View file

@ -1023,6 +1023,50 @@ int restore_job_rusage_state(cluster_t* c, uint64_t jobid, uint32_t state,
return 0;
}
/* bug-1691: WAL_JOB_CKPT's own sibling of restore_job_state() above, for
* apply_wal_entry()'s standby catch-up / replicated-apply seam -- takes
* ownership of an optional decoded ckptinfo string (NULL for a state-only
* WAL_JOB_CKPT entry, e.g. one written before this fix, or one fired while
* job.ckptinfo was still NULL at write time) so a promoted standby recovers
* the checkpoint info the same way apply_committed_entry's WAL_JOB_CKPT
* branch already restores it on the primary's own boot replay. Absolute
* state overwrite, matching every other restore_job_*_state() sibling on
* this seam -- no transition-result guarding happens on standby catch-up.
*
* `ckptinfo` is a TRANSIENT, caller-owned decode buffer (persist.c's
* decode_ckptinfo()), NOT itself an xstring -- j->job.ckptinfo is
* xstring-backed (makeString(), xstring.c), so adoption always goes through
* makeString() (which copies its input into its own refcounted allocation)
* and the existing value is released via dereference() (the RTTI verb),
* never free()/FREEUP, mirroring on_job_ckpt()'s own
* dereference(j->job.ckptinfo) call. `ckptinfo` itself is always freed here
* before returning, regardless of whether adoption happened -- the value
* actually stored in j->job.ckptinfo is always a freshly-made makeString()
* copy, never the input pointer itself. */
int restore_job_ckpt_state(cluster_t* c, uint64_t jobid, uint32_t state,
char* ckptinfo)
{
job_t* j = NULL;
lock(c->jobmgr, RTTI_LOCK_READ);
key_find(c->jobmgr, (void*)jobid, (void**)&j);
unlock(c->jobmgr);
if (!j) {
FREEUP(ckptinfo);
return -ENOENT;
}
lock(j, RTTI_LOCK_WRITE);
j->job.state = (int)state;
if (ckptinfo != NULL) {
if (j->job.ckptinfo != NULL)
dereference(j->job.ckptinfo);
j->job.ckptinfo = makeString(ckptinfo);
}
unlock(j);
FREEUP(ckptinfo);
return 0;
}
static int get_nslots_in_alloc(xce_host_alloc_t* alloc)
{
int i;