feat(job-array-single-wal-record): implement single-record array append/commit

Adds persist_submit_job_array_async(): encodes one JobSubmit-shaped
payload (base jobid, array size, one shared spec), appends exactly
one WAL_JOB_ARRAY_SUBMIT entry, and registers exactly one commit
continuation regardless of array size -- reuses the existing
job_commit_adapter_ctx_t/job_commit_resolve_adapter machinery, no
per-element loop. Not yet wired into handle_jobsubmit (task 2.2).

Also fixes a test-helper bug (WAL offset back-computed from the
post-append cursor, only valid for zero-payload entries) that
corrupted state across tests when exercising a nonzero-payload
array-submit entry.

Task 2.1 of job-array-single-wal-record.
This commit is contained in:
Rongsong Shen 2026-07-14 09:50:00 +00:00
parent 76862bd9aa
commit 7c4afbbb83
4 changed files with 559 additions and 1 deletions

View file

@ -4925,6 +4925,159 @@ int persist_submit_job_payloads_async(persist_t* p, xlist_t* payloads,
return first_rc;
}
/* job-array-single-wal-record task 2.1: serialize a job-array submission
* (base jobid + array size + one shared job spec) to a malloc'd Cap'n Proto
* buffer, reusing the SAME capnp struct (JobSubmit) the client's own submit
* request uses -- mirrors capnp_encode_job above, which mirrors encode_Job/
* new_Job/write_Job for a single job, except here via encode_JobSubmit/
* new_JobSubmit/write_JobSubmit. The encoded payload is O(1) in array_size:
* arraySize is a fixed-width uint64 field, not a per-element list -- so this
* function's output size never grows with array_size (Req 7.1). Caller must
* free(*out) on success. */
static int capnp_encode_job_array(const xce_job_submit_t* s, uint8_t** out,
size_t* out_len)
{
struct capn c;
struct capn_segment* cs;
struct JobSubmit cjs;
JobSubmit_ptr jp;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_JobSubmit(cs, &cjs, (xce_job_submit_t*)s);
jp = new_JobSubmit(cs);
write_JobSubmit(&cjs, jp);
capn_setp(capn_root(&c), 0, jp.p);
*out_len = capn_size(&c);
*out = malloc(*out_len);
if (!*out) { capn_free(&c); return -ENOMEM; }
capn_write_mem(&c, *out, *out_len, 0);
capn_free(&c);
return 0;
}
/* job-array-single-wal-record task 2.1: append-only, single-record
* counterpart of append_job_wal_entry_local for a WHOLE job array. Encodes
* ONE JobSubmit-shaped payload (base_jobid, array_size, base_spec->submit)
* and appends it as exactly one WAL_JOB_ARRAY_SUBMIT entry via the SAME
* primary-append primitive append_job_wal_entry_local already uses
* (ha_primary_log_append / persist_wal_async) -- never one entry per array
* element, regardless of array_size (Req 1.1, 1.2, 1.3). base_jobid/
* array_size are taken as explicit parameters rather than read from
* base_spec->jobid/base_spec->array_size, so the caller's own
* already-range-claimed values are always what gets committed, independent
* of whatever same-shaped fields the client's own wire request happened to
* carry. */
static int append_job_array_wal_entry_local(persist_t* p, uint64_t base_jobid,
uint64_t array_size,
const xce_job_submit_t* base_spec,
uint64_t* out_lsn)
{
xce_job_submit_t wire = { .jobid = base_jobid,
.array_size = array_size,
.submit = base_spec->submit };
uint8_t* buf = NULL;
size_t sz = 0;
int rc;
uint64_t lsn;
if (capnp_encode_job_array(&wire, &buf, &sz) != 0)
return -ENOMEM;
/* Snapshot the LSN this entry will receive BEFORE the append (same
* convention as append_job_wal_entry_local / submit_job_wal_entry_and_
* commit: persist_wal_async / ha_primary_log_append assigns
* hdr.lsn = current_lsn then increments). */
pthread_mutex_lock(&p->lock);
lsn = p->current_lsn;
pthread_mutex_unlock(&p->lock);
if (p->cluster != NULL)
rc = ha_primary_log_append(p->cluster, WAL_JOB_ARRAY_SUBMIT, buf,
(uint32_t)sz, NULL);
else
rc = persist_wal_async(p, WAL_JOB_ARRAY_SUBMIT, buf, (uint32_t)sz);
free(buf);
if (rc == 0)
*out_lsn = lsn;
return rc;
}
/* job-array-single-wal-record task 2.1 (design.md §WAL Array Submission,
* Service Interface): encodes and appends ONE array-shaped WAL entry
* (WAL_JOB_ARRAY_SUBMIT) and registers ONE commit continuation for it,
* regardless of array_size -- the single-record sibling of
* persist_submit_job_payloads_async's per-payload loop, specialized to
* exactly one payload. No job_t/xce_job_t object is constructed here or
* anywhere on this call path -- array expansion into individual job
* entries happens later, in the shared apply seam (task 3.x's
* restore_job_array), never here.
*
* Mirrors persist_submit_job_payloads_async's contract exactly:
* - a local encode/append failure resolves `fn` synchronously with that
* failure and returns the same negative rc;
* - the LSN-0 / engine-not-wired edge cases (documented on
* persist_submit_job_payloads_async above) resolve `fn` inline with
* rc=0;
* - otherwise `fn` is registered via commit_submit_async and invoked
* exactly once, asynchronously, once the entry's commit outcome is
* known. */
int persist_submit_job_array_async(persist_t* p, uint64_t base_jobid,
uint64_t array_size,
const xce_job_submit_t* base_spec,
uint64_t* out_lsn,
persist_job_commit_notify_fn fn, void* ctx)
{
uint64_t lsn = 0;
int rc;
if (p == NULL || base_spec == NULL || base_spec->submit == NULL ||
out_lsn == NULL || fn == NULL || array_size <= 1)
return -EINVAL;
rc = append_job_array_wal_entry_local(p, base_jobid, array_size,
base_spec, &lsn);
if (rc != 0) {
fn(ctx, 0, rc);
return rc;
}
*out_lsn = lsn;
/* LSN-0 / engine-not-wired edge (see persist_submit_job_payloads_async's
* doc comment above for the full rationale -- identical here,
* specialized to one entry instead of a loop). */
if (lsn == 0 || p->repl.ce == NULL) {
fn(ctx, lsn, 0);
return 0;
}
job_commit_adapter_ctx_t* a = calloc(1, sizeof(*a));
if (a == NULL) {
fn(ctx, lsn, -ENOMEM);
return -ENOMEM;
}
a->fn = fn;
a->ctx = ctx;
int reg_rc = commit_submit_async(p->repl.ce, lsn,
job_commit_resolve_adapter, a);
if (reg_rc != 0) {
free(a);
ERROR("persist_submit_job_array_async: lsn=%lu appended but "
"commit_submit_async registration failed (rc=%d) -- the "
"entry remains durable and will still be caught up "
"eventually via replay, but this caller will not be told "
"its outcome via this path",
(unsigned long)lsn, reg_rc);
fn(ctx, lsn, reg_rc);
return reg_rc;
}
return 0;
}
void persist_register_pending_apply(persist_t* p, uint64_t lsn,
pending_apply_notify_fn on_applied,
void* ctx)