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

@ -13,6 +13,7 @@
#include "myconf.h"
#include "mylock.h"
#include "netio.h"
#include "typemap.h" /* xce_job_state_entry_t -- worker-rejoin-resync task 2.2 */
#include "vtaskmgr.h"
#include "worker.h"
#include "xce/cluster_def.h"
@ -211,6 +212,35 @@ extern job_transition_result_t apply_job_postdone_transition(job_t*);
* call site. */
extern int record_job_reconcile_pending_fact(cluster_t*, job_t*);
/* worker-rejoin-resync task 2.2 (Req 4.1, 4.3, 4.4, 4.7, 4.8, 7.1-7.3):
* records a job the worker reports running that the controller has no
* existing non-terminal record of ("unqueried extra", Req 3.6) as assigned
* to the reporting worker `w`, matching `report`'s reported status --
* dispatch_a_job_()'s own eager-mutation-plus-explicit-notify pattern, NOT
* record_job_finish_fact's deferred one -- see design.md's "Controller:
* job.c" Components section, and job.c's own doc comment on this function
* for the full rationale (including why job->job.allocs is deliberately
* left NULL, and why there is no separate "host assignment" field for this
* function to set beyond job->job.state).
*
* Precondition: caller holds `job` under RTTI_LOCK_WRITE on entry and does
* not release it until this call returns -- this function does not
* lock/unlock `job` itself. Unlike record_job_reconcile_pending_fact above,
* this function performs no backward-transition guard of its own: the
* caller must only invoke it when the controller has no existing
* non-terminal record of this job (Req 4.8's own trigger condition), so
* there is nothing more-final to clobber.
*
* Always returns (int)JOB_TRANSITION_APPLIED -- this outcome has no
* reject/no-op case (unlike record_job_reconcile_pending_fact's
* REJECTED_BACKWARD/NOOP_DUPLICATE). Matching that sibling's contract, the
* caller (not this function) must call
* persist_notify_job_updated_already_persisted(c, job) itself, AFTER
* releasing the write lock, exactly once -- omitting it reproduces bug-1679
* at this call site. */
extern int record_job_reconcile_assign_fact(cluster_t*, job_t*, worker_t*,
xce_job_state_entry_t*);
extern int on_job_start(cluster_t*, job_pidinfo_t*);
extern int on_job_finish(cluster_t*, xce_jobfinish_t*);
extern int on_job_fail(cluster_t*, job_failinfo_t*);