feat(worker-rejoin-resync): reconcile unqueried job assignment (task 2.2)

Add record_job_reconcile_assign_fact(cluster_t*, job_t*, worker_t*,
xce_job_state_entry_t*) to job.c: eagerly sets job->job.state to match
the worker's reported outcome (mirroring dispatch_a_job_'s inline
mutation, minus allocation fields -- there are none to set, job->job.allocs
stays NULL per Requirement 4.8's documented scope). Calls the existing
persist_submit_job_dispatch_fact(..., alloc=NULL) directly, then leaves
the post-unlock persist_notify_job_updated_already_persisted() call to
the caller, same convention as task 2.1.

Fixes bug-1746 along the way: capnp_encode_joballoc() (persist.c, part
of bug-1678's original fix, predates this feature) unconditionally
dereferenced a NULL alloc -- never triggered before since
dispatch_a_job_ (the only prior caller) always passes non-NULL. Fixed
with a minimal guard producing a true zero-length buffer, matching
apply_committed_entry's alloc_len > 0 decode gate exactly. This
directly contradicted design.md's "tested, backward-compatible on both
seams" claim (true only for decode, never actually exercised for
encode) -- corrected in 4 places.

2 tests: applies-running-no-allocs-and-notifies, and a real restart-
replay round trip (write to real disk, fresh persist_recover(), confirm
jobid/host/state/NULL-allocs survive) -- proving this task's own
"durably survives a restart replay" completion criterion literally, not
just the in-memory mutation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
This commit is contained in:
Rongsong Shen 2026-09-01 11:40:47 +00:00
parent b7ed6de2a1
commit ecfb4c942f
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 550 additions and 3 deletions

View file

@ -230,6 +230,26 @@ static int capnp_encode_joballoc(const xce_job_alloc_t* a, uint8_t** out,
struct JobAlloc cja;
JobAlloc_ptr jap;
/* worker-rejoin-resync task 2.2 (bug-1746): a NULL `a` means "no
* allocation to carry" (record_job_reconcile_assign_fact()'s Req 4.8
* case -- the controller genuinely never learned one). Produce a
* genuinely zero-length buffer, NOT a capnp-encoded null-pointer JobAlloc
* (what encode_JobAlloc_ptr()'s existing NULL branch would produce) --
* persist_submit_job_dispatch_fact()'s caller (both apply seams,
* apply_committed_entry()/apply_wal_entry()) discriminates "is there an
* allocation to decode" purely by `alloc_len > 0`, so any non-zero
* trailing byte count here would be misread as a real allocation to
* decode. encode_JobAlloc() below has no NULL guard of its own (it
* unconditionally dereferences `a` on its first line) -- this early
* return is the one and only guard needed, matching the sole documented
* call convention this function must now support (dispatch_a_job_()'s
* own call site continues to always pass non-NULL, unaffected). */
if (a == NULL) {
*out = NULL;
*out_len = 0;
return 0;
}
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_JobAlloc(cs, &cja, (xce_job_alloc_t*)a);