feat(raft-apply-decoupling): defer job construction past the commit barrier (task 3.1)
Reorder handle_jobsubmit() so job objects are constructed via create_job() only after the WAL commit succeeds, not before. Adds jobid_range_exists() for a fast-fail pre-check with no WAL work, and persist_submit_job_payloads_and_commit() (persist.c) operating on raw xce_job_t* payloads instead of pre-existing job_t*. Commit remains synchronous -- converting to the continuation-based commit API is task 3.2. Fixes bug-1408 (found during review): the post-commit array- construction loop suppressed notification for an entire array batch if any one sibling element failed to construct, stranding other durably-committed and successfully-constructed jobs invisible until the next restart's replay. Notification now fires unconditionally for every successfully-constructed element, independent of sibling failures; the client reply still separately reports the batch's construction status.
This commit is contained in:
parent
8febd663d5
commit
6f1fa97fab
6 changed files with 871 additions and 399 deletions
|
|
@ -50,36 +50,76 @@ static job_t* create_job(cluster_t* c, xce_job_t* j)
|
|||
return job;
|
||||
}
|
||||
|
||||
uint64_t job_submit(void* c, xce_job_submit_t* submit)
|
||||
/* True if any jobid in [base_jobid, base_jobid + array_size) already exists
|
||||
* in c->jobmgr (raft-apply-decoupling task 3.1). Checked BEFORE any WAL work
|
||||
* so a colliding submission is still rejected immediately with no side
|
||||
* effect, matching the fast-fail behavior create_job()'s own key_find
|
||||
* guard used to provide when job construction ran before the commit
|
||||
* barrier — now that construction is deferred to after commit, a collision
|
||||
* detected only post-commit could no longer be cleanly reported as "never
|
||||
* happened". */
|
||||
static bool jobid_range_exists(cluster_t* c, uint64_t base_jobid,
|
||||
uint64_t array_size)
|
||||
{
|
||||
job_t* job = NULL;
|
||||
uint64_t base_id = submit->jobid;
|
||||
xce_job_t j = { .submit = submit->submit };
|
||||
time_t now = time(NULL);
|
||||
int i;
|
||||
for (uint64_t i = 0; i < array_size; i++) {
|
||||
job_t* existing = NULL;
|
||||
int found;
|
||||
|
||||
j.sub_time = now;
|
||||
j.queue_time = now;
|
||||
j.start_time = 0;
|
||||
j.finish_time = 0;
|
||||
j.pidinfo = NULL;
|
||||
j.state = JOB_STAT_PEND;
|
||||
j.allocs = NULL;
|
||||
j.run_time = 0;
|
||||
j.run_usage = NULL;
|
||||
j.exit_info = NULL;
|
||||
j.ckptinfo = NULL;
|
||||
j.pending_ctrl = 0;
|
||||
lock(c->jobmgr, RTTI_LOCK_READ);
|
||||
found = key_find(c->jobmgr, (void*)(base_jobid + i),
|
||||
(void**)&existing);
|
||||
unlock(c->jobmgr);
|
||||
|
||||
for (i = 0; i < submit->array_size; i++) {
|
||||
j.jobid = base_id + i;
|
||||
job = create_job(c, &j);
|
||||
if (job == NULL) {
|
||||
return -errno;
|
||||
if (found > 0 && existing != NULL)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Build one xce_job_t submission payload per array element, WITHOUT
|
||||
* constructing or registering any job object (raft-apply-decoupling task
|
||||
* 3.1 / Req 2.1, 2.3) — these are plain heap structs carrying only the data
|
||||
* needed to build each entry's WAL payload; `free_job_submit_payloads` frees
|
||||
* them. `submit->submit` is borrowed (owned by the caller's request), never
|
||||
* duplicated or freed here. Returns 0 on success, -ENOMEM on allocation
|
||||
* failure (payloads already stored are left for the caller to free). */
|
||||
static int build_job_submit_payloads(uint64_t base_jobid,
|
||||
xce_job_submit_t* submit, xlist_t* plist)
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
|
||||
xlist_init(plist, submit->array_size);
|
||||
|
||||
for (uint64_t i = 0; i < submit->array_size; i++) {
|
||||
xce_job_t* jp = calloc(1, sizeof(*jp));
|
||||
if (jp == NULL)
|
||||
return -ENOMEM;
|
||||
if (xlist_store(plist, jp) < 0) {
|
||||
free(jp);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
jp->submit = submit->submit;
|
||||
jp->jobid = base_jobid + i;
|
||||
jp->sub_time = now;
|
||||
jp->queue_time = now;
|
||||
jp->state = JOB_STAT_PEND;
|
||||
}
|
||||
|
||||
return base_id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int xlist_free_job_payload_(xce_job_t* j, void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
free(j);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void free_job_submit_payloads(xlist_t* plist)
|
||||
{
|
||||
xlist_iter(plist, (xlist_iter_fn_t)xlist_free_job_payload_, NULL);
|
||||
xlist_fini(plist);
|
||||
}
|
||||
|
||||
int load_jobs(void* c)
|
||||
|
|
@ -562,8 +602,10 @@ int handle_jobsubmit(cluster_t* c, endpoint_t* ep, xce_request_t* request)
|
|||
uint64_t jobid;
|
||||
xce_job_submit_t* submit;
|
||||
xce_reply_t reply;
|
||||
xlist_t jlist;
|
||||
job_t* j = NULL;
|
||||
xlist_t plist; /* xce_job_t* WAL payloads, pre-commit */
|
||||
xlist_t jlist; /* job_t* for the post-commit notify hook */
|
||||
uint64_t* lsns = NULL;
|
||||
job_t* j = NULL;
|
||||
int i;
|
||||
int rc;
|
||||
|
||||
|
|
@ -574,15 +616,7 @@ int handle_jobsubmit(cluster_t* c, endpoint_t* ep, xce_request_t* request)
|
|||
if (NULLSTR(submit->submit->user) || NULLSTR(submit->submit->queue)
|
||||
|| (submit->submit->start_info == NULL)
|
||||
|| NULLSTR(submit->submit->start_info->cmdline)) {
|
||||
jobid = 0;
|
||||
errno = EINVAL;
|
||||
} else {
|
||||
xce_run_hooks(&(c->hooksys), XCE_CONTROLLER_HOOK_JOB, HOOK_BEFORE_EVENT,
|
||||
HOOK_DETAIL_JOB_NEW, c, submit);
|
||||
|
||||
jobid = job_submit(c, submit);
|
||||
}
|
||||
if (jobid == 0) {
|
||||
reply.status = -errno;
|
||||
reply.data_which = RequestReply_data_nodata;
|
||||
|
||||
|
|
@ -590,9 +624,114 @@ int handle_jobsubmit(cluster_t* c, endpoint_t* ep, xce_request_t* request)
|
|||
return -errno;
|
||||
}
|
||||
|
||||
/* Build job list for the AFTER_EVENT hook BEFORE replying to client.
|
||||
* The AFTER_EVENT hook writes WAL and waits for majority replication —
|
||||
* the client must not be acknowledged until the job is durable. */
|
||||
xce_run_hooks(&(c->hooksys), XCE_CONTROLLER_HOOK_JOB, HOOK_BEFORE_EVENT,
|
||||
HOOK_DETAIL_JOB_NEW, c, submit);
|
||||
|
||||
jobid = submit->jobid;
|
||||
|
||||
if (jobid_range_exists(c, jobid, submit->array_size)) {
|
||||
errno = EEXIST;
|
||||
reply.status = -errno;
|
||||
reply.data_which = RequestReply_data_nodata;
|
||||
|
||||
xce_reply(ep, c->myhost, &reply);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
/* Build only the WAL payload data for each array element — no job
|
||||
* object is constructed or inserted into c->jobmgr until the commit
|
||||
* below succeeds (raft-apply-decoupling task 3.1 / Req 2.1, 2.3): a
|
||||
* commit that fails or times out must leave the job table untouched,
|
||||
* and the only way to guarantee that structurally is to never
|
||||
* construct anything until commit has already returned success. */
|
||||
rc = build_job_submit_payloads(jobid, submit, &plist);
|
||||
if (rc == 0) {
|
||||
lsns = calloc((size_t)plist.count, sizeof(*lsns));
|
||||
if (lsns == NULL)
|
||||
rc = -ENOMEM;
|
||||
}
|
||||
if (rc < 0) {
|
||||
free_job_submit_payloads(&plist);
|
||||
|
||||
reply.status = rc;
|
||||
reply.data_which = RequestReply_data_nodata;
|
||||
|
||||
xce_reply(ep, c->myhost, &reply);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Explicit raft step (raft-apply-decoupling item #1 / bug-1306; task
|
||||
* 3.1): WAL-append + majority-commit barrier for every payload, reading
|
||||
* the commit verdict DIRECTLY. This used to run inside the AFTER_EVENT
|
||||
* hook (persist_job_hook_fun), but xce_run_hooks returns only the LAST
|
||||
* hook's rc, so a later notify subscriber (sched/apiserver) returning 0
|
||||
* CLOBBERED the -ETIMEDOUT — the client was acked for an entry that
|
||||
* never reached majority, which a new-term leader then legally
|
||||
* discarded (committed-job-loss). The client must not be acknowledged,
|
||||
* and no job object may exist, until the submission is durable on a
|
||||
* majority. */
|
||||
int commit_rc = persist_submit_job_payloads_and_commit(&c->persist,
|
||||
&plist, lsns);
|
||||
if (commit_rc < 0) {
|
||||
WARN("handle_jobsubmit: job not durable (rc=%d) — rejecting submission",
|
||||
commit_rc);
|
||||
free_job_submit_payloads(&plist);
|
||||
free(lsns);
|
||||
|
||||
reply.status = commit_rc;
|
||||
reply.data_which = RequestReply_data_nodata;
|
||||
|
||||
xce_reply(ep, c->myhost, &reply);
|
||||
return commit_rc;
|
||||
}
|
||||
|
||||
/* Committed on a majority — NOW construct and register each job object
|
||||
* (moved here from before the commit barrier; raft-apply-decoupling
|
||||
* task 3.1 / Req 2.1, 2.2, 2.3). A construction failure at this point
|
||||
* cannot be rolled back (the entry is already durably committed); it is
|
||||
* a genuine apply-time failure, not a retryable one (design.md Error
|
||||
* Strategy) — logged loud, and the submission is NOT reported as a
|
||||
* success. */
|
||||
int construct_rc = 0;
|
||||
for (i = 0; i < plist.count; i++) {
|
||||
xce_job_t* jp = (xce_job_t*)xlist_get(&plist, i);
|
||||
job_t* job = create_job(c, jp);
|
||||
|
||||
if (job == NULL) {
|
||||
int saved_errno = errno;
|
||||
|
||||
ERROR("handle_jobsubmit: job %lu committed at lsn %lu but "
|
||||
"could not be constructed (errno=%d)",
|
||||
(unsigned long)jp->jobid, (unsigned long)lsns[i],
|
||||
saved_errno);
|
||||
if (construct_rc == 0)
|
||||
construct_rc = -saved_errno;
|
||||
continue;
|
||||
}
|
||||
job->submit_lsn = lsns[i];
|
||||
}
|
||||
|
||||
free_job_submit_payloads(&plist);
|
||||
free(lsns);
|
||||
|
||||
/* Build the job list for the AFTER_EVENT hook — the same lookup used
|
||||
* before this task, just run after construction (which now itself runs
|
||||
* after commit) instead of before either.
|
||||
*
|
||||
* IMPORTANT (raft-apply-decoupling task 3.1 remediation): this lookup
|
||||
* and the hook it feeds run UNCONDITIONALLY here, even when
|
||||
* construct_rc < 0 for one or more array elements. Every element whose
|
||||
* create_job() succeeded above is already durably committed AND already
|
||||
* inserted into c->jobmgr — key_find below naturally finds only those
|
||||
* (a failed element was never inserted, so it is silently absent from
|
||||
* jlist with no extra bookkeeping needed). Both the scheduler and the
|
||||
* apiserver are purely event-driven off this hook with no periodic
|
||||
* reconciliation, so returning early here (as a previous version of
|
||||
* this function did whenever ANY sibling failed to construct) would
|
||||
* strand the successfully constructed siblings — real, durable jobs —
|
||||
* invisible until the next controller restart replays the WAL. A
|
||||
* whole-batch construction failure must never suppress notification for
|
||||
* the elements that individually succeeded. */
|
||||
xlist_init(&jlist, submit->array_size);
|
||||
|
||||
for (i = 0; i < submit->array_size; i++) {
|
||||
|
|
@ -615,31 +754,13 @@ int handle_jobsubmit(cluster_t* c, endpoint_t* ep, xce_request_t* request)
|
|||
}
|
||||
}
|
||||
|
||||
/* Explicit raft step (raft-apply-decoupling item #1 / bug-1306): WAL-append
|
||||
* + majority-commit barrier, reading the commit verdict DIRECTLY. This used
|
||||
* to run inside the AFTER_EVENT hook (persist_job_hook_fun), but xce_run_hooks
|
||||
* returns only the LAST hook's rc, so a later notify subscriber (sched/
|
||||
* apiserver) returning 0 CLOBBERED the -ETIMEDOUT — the client was acked for
|
||||
* an entry that never reached majority, which a new-term leader then legally
|
||||
* discarded (committed-job-loss). The client must not be acknowledged until
|
||||
* the job is durable on a majority. */
|
||||
int commit_rc = persist_submit_wal_and_commit(&c->persist, &jlist);
|
||||
if (commit_rc < 0) {
|
||||
WARN("handle_jobsubmit: job not durable (rc=%d) — rejecting submission",
|
||||
commit_rc);
|
||||
xlist_iter(&jlist, (xlist_iter_fn_t)xlist_deref_job_item_, NULL);
|
||||
xlist_fini(&jlist);
|
||||
reply.status = commit_rc;
|
||||
reply.data_which = RequestReply_data_nodata;
|
||||
|
||||
xce_reply(ep, c->myhost, &reply);
|
||||
return commit_rc;
|
||||
}
|
||||
|
||||
/* Committed on a majority — fire the AFTER_EVENT hook as NOTIFICATION only
|
||||
* (scheduler push, apiserver SSE), observing already-committed state. Its
|
||||
* return no longer gates durability: a notify-hook failure cannot un-commit
|
||||
* an already-durable job, so it is logged but never fails the client reply. */
|
||||
/* Committed + constructed — fire the AFTER_EVENT hook as NOTIFICATION
|
||||
* only (scheduler push, apiserver SSE), observing already-committed,
|
||||
* already-applied state. Its return no longer gates durability: a
|
||||
* notify-hook failure cannot un-commit an already-durable job, so it is
|
||||
* logged but never fails the client reply. Fired for every
|
||||
* successfully constructed element regardless of construct_rc (see the
|
||||
* comment above jlist's construction). */
|
||||
int notify_rc = xce_run_hooks(&(c->hooksys), XCE_CONTROLLER_HOOK_JOB,
|
||||
HOOK_AFTER_EVENT,
|
||||
HOOK_DETAIL_JOB_NEW, c, &jlist);
|
||||
|
|
@ -651,7 +772,20 @@ int handle_jobsubmit(cluster_t* c, endpoint_t* ep, xce_request_t* request)
|
|||
|
||||
xlist_fini(&jlist);
|
||||
|
||||
/* Reply success only AFTER WAL write + majority commit succeeded. */
|
||||
if (construct_rc < 0) {
|
||||
/* At least one array element durably committed but could not be
|
||||
* constructed (see the ERROR logged at the point of failure above).
|
||||
* The client learns the submission did not fully succeed; the
|
||||
* siblings that DID construct successfully were already notified
|
||||
* above and must never have that notification gated on this. */
|
||||
reply.status = construct_rc;
|
||||
reply.data_which = RequestReply_data_nodata;
|
||||
|
||||
xce_reply(ep, c->myhost, &reply);
|
||||
return construct_rc;
|
||||
}
|
||||
|
||||
/* Reply success only AFTER WAL write + majority commit + construction. */
|
||||
reply.status = 0;
|
||||
reply.data_which = RequestReply_data_jobid;
|
||||
reply.data.jobid = jobid;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue