feat(job-array-single-wal-record): implement restore_job_array primitive

Adds the shared N-way array expansion primitive: decodes a committed
WAL_JOB_ARRAY_SUBMIT payload, scratch-builds every element via a
throwaway kv-table (never touching c->jobmgr during construction),
then makes the whole set visible in one bulk pass under a single
write-lock acquisition -- no window where a concurrent reader can
observe a partially-constructed array. Idempotent per index (skips
already-existing jobids; whole-record no-op if all exist). Includes
an XCE_TESTING-guarded fault-injection seam. Not yet wired into any
live dispatch path (tasks 3.2/3.3).

Two review rounds: round 1 found the bulk-insert pass silently
claimed success even when key_add() itself could fail on allocation
exhaustion (c->jobmgr grows via realloc, no reservation primitive
exists anywhere in kv.c/kvmgr.c/zarray.c to pre-check this). Fixed
by checking key_add()'s return and reporting failure honestly
instead of a false success. The underlying table has no key_del, so
an already-inserted prefix on this specific failure mode can remain
genuinely visible while the call reports failure -- documented as a
residual gap in design.md and tasks.md for tasks 3.2/4.x to account
for (that prefix is never pushed to scheduler/API server, even on a
later successful retry, since it's never included in out_new_jobs).

Task 3.1 of job-array-single-wal-record.
This commit is contained in:
Rongsong Shen 2026-07-14 14:16:29 +00:00
parent 78eb983a74
commit 7c755dc4d7
7 changed files with 1346 additions and 1 deletions

View file

@ -45,6 +45,33 @@ extern int update_resvals(cluster_t*, worker_t*, int, xce_resval_t*);
extern int load_jobs(void*);
extern int restore_job(cluster_t*, uint64_t, const void*, size_t, uint64_t);
/* job-array-single-wal-record task 3.1 (design.md Array Apply Expansion):
* shared N-way expansion primitive for a committed WAL_JOB_ARRAY_SUBMIT
* payload -- see the doc comment on the definition (job.c) for the full
* atomicity/idempotency contract. out_new_jobs is optional; when non-NULL
* and the call genuinely creates new elements, it is populated (ownership
* transferred) with every newly created job_t* in index order. */
extern int restore_job_array(cluster_t*, const void*, size_t, uint64_t,
xlist_t*);
#ifdef XCE_TESTING
/* job-array-single-wal-record task 3.1: forces restore_job_array()'s
* scratch-build loop to fail construction at the given 0-based element
* index (simulating resource exhaustion), so a test can prove the
* all-or-nothing atomicity contract. Pass a negative index, or call the
* clear helper, to disable. Compiled out of production. */
extern void job_array_apply_test_set_force_fail_at(int index);
extern void job_array_apply_test_clear_force_fail(void);
/* job-array-single-wal-record task 3.1 remediation: forces
* restore_job_array()'s BULK-INSERT pass (the key_add(c->jobmgr, ...) loop,
* distinct from the scratch-build loop above) to fail at the given 0-based
* index WITHIN THAT PASS, simulating c->jobmgr's backing store hitting a
* genuine realloc() OOM while growing under the write lock. Lets a test
* prove this failure mode is reported as a negative errno, never a false
* built.count success. Pass a negative index, or call the clear helper, to
* disable. Compiled out of production. */
extern void job_array_apply_test_set_bulk_insert_force_fail_at(int index);
extern void job_array_apply_test_clear_bulk_insert_force_fail(void);
#endif
extern int update_job(void*, uint64_t, xce_kv_list_t*);
extern int do_job_started(void*, uint64_t, int, int*, int, int*);