feat(scheduler-dispatch-commit): add controller dispatch-failure classification (task 1.1)

Define xce_dispatch_fail_site_t/xce_dispatch_fail_class_t and
xce_classify_dispatch_fail() in job.c, mapping each of dispatch_a_job_()'s
six post-lookup failure points to retryable or terminal per Requirement
4.1/4.2. Standalone, not yet wired into dispatch_a_job_() (deferred to
tasks 3.2/3.3). Also adds this spec's requirements/design/research/tasks
docs (first commit touching this spec).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-23 06:36:51 +00:00
parent 5e34f1f571
commit 5724d1e90a
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
9 changed files with 830 additions and 0 deletions

View file

@ -1,3 +1,4 @@
#include <assert.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
@ -828,6 +829,96 @@ static int mk_launchinfo(xce_launch_t* launch, xce_job_alloc_t* alloc)
return rc;
}
/*
* scheduler-dispatch-commit task 1.1 (Req 4.1, 4.2): classifies one of
* dispatch_a_job_()'s six post-lookup failure points as retryable
* (transient/environmental -- return the job to PEND for a later retry) or
* terminal (a locally invalid decision that retrying will not fix -- resolve
* the job to a terminal failed state). Declared static/file-local: nothing
* outside job.c consumes this classification (design.md's own placement-
* correction note). dispatch_a_job_() itself is wired to call this at each
* of its six failure points by a later task; this task only adds the
* disconnected, independently-testable classification step --
* dispatch_a_job_() below is unmodified by this task.
*/
typedef enum {
XCE_DISPATCH_FAIL_SITE_WORKER_LOOKUP = 0, /* workermgr key_find() rc<0 */
XCE_DISPATCH_FAIL_SITE_WORKER_NULL = 1, /* worker not found (w==NULL) */
XCE_DISPATCH_FAIL_SITE_WORKER_NOT_READY = 2, /* E_XCE_BAD_WORKER */
XCE_DISPATCH_FAIL_SITE_INVALID_ALLOC = 3, /* nc <= 0 */
XCE_DISPATCH_FAIL_SITE_QUEUE_EXEC = 4, /* get_queue_exec() failure */
XCE_DISPATCH_FAIL_SITE_RPC_FAILURE = 5, /* xce_startjob_req() rc<0 */
} xce_dispatch_fail_site_t;
typedef enum {
XCE_DISPATCH_FAIL_RETRYABLE = 0,
XCE_DISPATCH_FAIL_TERMINAL = 1,
} xce_dispatch_fail_class_t;
/* Keyed on WHICH failure point was hit (site identity), not on the raw rc
* that point returned: workermgr's key_find() and get_queue_exec() are two
* independently-implemented failure sources not guaranteed to return
* disjoint error codes (e.g. both could plausibly return -ENOENT for
* unrelated reasons), so classifying by rc value alone would be ambiguous
* whenever two sites happen to collide.
*
* Every named site is handled explicitly by its own case. The `default:`
* arm below exists for the impossible-by-construction case of a `site` with
* no matching enumerator (every current/planned call site passes a literal
* enumerator, so this can only be reached by a genuine programming-invariant
* violation, not by any operational/environmental condition) -- a plain
* default-less switch does NOT get compiler-enforced exhaustiveness in C
* (verified directly: GCC emits only a non-fatal `-Wreturn-type` "control
* reaches end of non-void function" warning here, and this project sets no
* -Werror anywhere), so falling off the end would silently return an
* indeterminate value instead. This mirrors persist.c's wal_classify()
* default arm exactly: ERROR-log and return a safe-side classification
* (TERMINAL -- never risk an unbounded retry loop on an unrecognized site)
* unconditionally, and additionally assert() only under XCE_TESTING, so
* xce_controller itself (built without -DXCE_TESTING) can never self-
* terminate on this path (CLAUDE.md's controller fail-loud/never-self-
* terminate discipline: assert()/abort is reserved for exactly this kind of
* impossible-state bug, but production must still never exit on it), while
* controller_unit_test (built WITH -DXCE_TESTING) crashes loud and
* immediately if it's ever hit. */
static xce_dispatch_fail_class_t
xce_classify_dispatch_fail(xce_dispatch_fail_site_t site)
{
switch (site) {
case XCE_DISPATCH_FAIL_SITE_WORKER_LOOKUP:
case XCE_DISPATCH_FAIL_SITE_WORKER_NULL:
case XCE_DISPATCH_FAIL_SITE_WORKER_NOT_READY:
case XCE_DISPATCH_FAIL_SITE_RPC_FAILURE:
return XCE_DISPATCH_FAIL_RETRYABLE;
case XCE_DISPATCH_FAIL_SITE_INVALID_ALLOC:
case XCE_DISPATCH_FAIL_SITE_QUEUE_EXEC:
return XCE_DISPATCH_FAIL_TERMINAL;
default:
ERROR("xce_classify_dispatch_fail: unrecognized failure site %d -- "
"TERMINAL fallback; add it to the classification table",
(int)site);
#ifdef XCE_TESTING
assert(0
&& "unrecognized dispatch-fail site -- add it to "
"xce_classify_dispatch_fail");
#endif
return XCE_DISPATCH_FAIL_TERMINAL;
}
}
#ifdef XCE_TESTING
/* Test-only accessor: xce_classify_dispatch_fail() and its enum types are
* intentionally undeclared in any header (nothing outside job.c consumes
* them), so the unit test reaches it through this thin, int-typed wrapper
* instead -- mirroring job_array_apply_test_set_force_fail_at()'s existing
* XCE_TESTING-only-export pattern earlier in this file (xce_controller
* itself is built without -DXCE_TESTING, so this never ships). */
int xce_classify_dispatch_fail_test(int site)
{
return (int)xce_classify_dispatch_fail((xce_dispatch_fail_site_t)site);
}
#endif /* XCE_TESTING */
static int dispatch_a_job_(cluster_t* c, endpoint_t* ep, xce_job_alloc_t* alloc)
{
int rc = 0;