feat(scheduler-sim-live-allocation): add unsatisfiable-cache callback seam (task 1.1)

Adds a NULL-by-default lookup/populate callback seam on xce_sched_core_t,
mirroring cache_clear_cb's proven shape, so the allocation dispatch
chokepoint can consult the unsatisfiable-resreq cache without a
normal-mode-only field access in shared code. Adapters live in
alloc_cache.c and are registered only during normal-mode startup;
simulation mode leaves both callbacks NULL and always computes fresh.

Adds the scheduler-sim-live-allocation spec (requirements/design/
research/tasks) as the planning artifact this and subsequent tasks
implement against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
Rongsong Shen 2026-09-18 10:14:04 +00:00
parent 487e7b9419
commit 89297dc825
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
10 changed files with 1130 additions and 0 deletions

View file

@ -109,6 +109,61 @@ typedef int (*xce_core_decision_cb_t)(void*, xce_decision_t*);
*/
typedef void (*xce_core_cache_clear_cb_t)(xce_sched_core_t*);
/*
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t -
* driver-registered, OPTIONAL per-session unsatisfiable-resreq cache
* consult/populate seam (scheduler-sim-live-allocation spec, task 1.1,
* Req 5.2). Mirrors xce_core_cache_clear_cb_t's own shape and rationale
* immediately above exactly: a bare function pointer taking
* xce_sched_core_t* directly, no wrapper struct, no opaque "impl"/
* user-data pointer -- single registrant (main.c's normal-mode branch),
* no foreseeable second implementation.
*
* Parameter shape mirrors the REAL cache's own existing entry points
* (alloc_cache.c's cache_lookup()/cache_insert(), design.md's
* "Unsatisfiable-Cache Callback Seam" component) rather than inventing a
* new bare-integer "fingerprint" type: xce_sched_obj_t* is the exact
* object cache_lookup()/cache_insert() already key on (its embedded
* xce_resreq_fingerprint_t plus user/queue/project identity, sched/
* alloc_cache.h) -- this seam adds no new type, only a NULL-safe
* indirection over the existing one. xce_sched_obj_t is already visible
* here as a forward-declared pointer type (sched/types.h, included
* above); only a pointer, never the full struct, is needed in this
* header.
*
* WHY a hook instead of a direct call (mirrors xce_core_cache_clear_cb_t's
* own documented incident above): the allocation dispatch chokepoint this
* seam wraps (sched_make_primary_alloc_(), scheduler.c) is reached
* identically by both drivers once a later task's Core-Safe Allocation
* Chain migration lands -- but the real cache
* (xce_scheduler_t.unsatisfiable_cache) exists ONLY on a normal-mode
* xce_scheduler_t, never on a simulation-mode xce_sched_sim_driver_t. A
* NULL-checked, driver-registered callback keeps that difference behind
* an explicit seam instead of a struct-prefix cast into mode-specific
* state from code both drivers share -- the same anti-pattern
* xce_core_cache_clear_cb_t's own doc comment documents a real, prior
* memory-corruption incident from.
*
* NULL (the xce_sched_core_init() memset() default, unless a driver
* registers one) means "always compute fresh" for BOTH callbacks -- never
* an error, never a degraded-correctness path, only a degraded-
* performance one. Wiring the actual consult/populate call sites into
* sched_make_primary_alloc_() is a LATER task's job (tasks.md 2.2,
* boundary "Core-Safe Allocation Chain, Unsatisfiable-Cache Callback
* Seam") -- this task adds only the seam (these two fields, their
* adapters in alloc_cache.c, and main.c's normal-mode-only registration),
* not that call site. Every simulation-mode core leaves both fields NULL
* forever (sim_loop.c never registers them, Req 5 AC 17 "Simulation
* isolation"), so a simulation-mode session never touches the real cache
* implementation at all.
*/
typedef int (*xce_unsatisfiable_lookup_cb_t)(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int* out_reason_category);
typedef void (*xce_unsatisfiable_populate_cb_t)(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int reason_category);
struct xce_sched_core_s {
mylock_t lock;
int stop;
@ -159,6 +214,18 @@ struct xce_sched_core_s {
* call -- true for every simulation-mode xce_sched_core_t, which never
* registers this hook. */
xce_core_cache_clear_cb_t cache_clear_cb;
/* Req 5.2 (scheduler-sim-live-allocation spec, task 1.1): driver-
* registered, OPTIONAL unsatisfiable-resreq cache consult/populate
* seam -- see xce_unsatisfiable_lookup_cb_t/xce_unsatisfiable_
* populate_cb_t's own doc comment above for the full rationale. NULL
* (xce_sched_core_init()'s memset() default) until main.c's
* normal-mode branch registers both, alongside cache_clear_cb just
* above. A simulation-mode core never registers either, so
* consulting/populating this seam is always a true no-op for sim
* mode ("always compute fresh"), never a fallback onto real cache
* state. */
xce_unsatisfiable_lookup_cb_t unsatisfiable_lookup_cb;
xce_unsatisfiable_populate_cb_t unsatisfiable_populate_cb;
_Atomic int session_active; /* 1 during scheduling session, 0 between */
_Atomic int eval_in_progress; /* 1 while eval_select loop runs */
_Atomic int staging_merge_complete; /* 1 after staging merge completes */

View file

@ -141,4 +141,22 @@ extern void cache_clear(xce_scheduler_t* sched);
*/
extern void xce_alloc_cache_clear_cb(xce_sched_core_t* core);
/*
* xce_alloc_cache_lookup_cb / xce_alloc_cache_populate_cb - the
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t (core.h)
* adapters for cache_lookup()/cache_insert() above (scheduler-sim-live-
* allocation spec, task 1.1, Req 5.2). Registered on the real
* xce_scheduler_t's core via direct field assignment in main.c's
* normal-mode-only startup branch, alongside xce_alloc_cache_clear_cb()
* just above. See alloc_cache.c's own doc comment on these functions for
* the full rationale, including why they live here rather than in
* driver.c.
*/
extern int xce_alloc_cache_lookup_cb(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int* out_reason_category);
extern void xce_alloc_cache_populate_cb(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int reason_category);
#endif

View file

@ -266,3 +266,51 @@ void xce_alloc_cache_clear_cb(xce_sched_core_t* core)
{
cache_clear((xce_scheduler_t*)core);
}
/*
* xce_alloc_cache_lookup_cb / xce_alloc_cache_populate_cb -
* scheduler-sim-live-allocation spec, task 1.1 (Req 5.2): the
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t (core.h)
* adapters for cache_lookup()/cache_insert() above, mirroring
* xce_alloc_cache_clear_cb()'s own shape and rationale exactly.
*
* Deliberately defined HERE (alloc_cache.c), not in driver.c or
* scheduler.c, for the IDENTICAL white-box-test-linkage reason
* xce_alloc_cache_clear_cb()'s own doc comment above documents: several
* white-box test TUs #include ../src/driver.c directly WITHOUT linking
* alloc_cache.c. Had either adapter lived anywhere those TUs don't
* already link, every one of them would fail to LINK the moment a future
* task wires a call site (main.c/a shared allocation-chain function)
* against it. Keeping both adapters here means those TUs stay entirely
* unaffected: none of them link alloc_cache.c, none of them reference
* either adapter, and none of them ever assign core->unsatisfiable_
* lookup_cb/unsatisfiable_populate_cb either -- both fields stay NULL,
* matching production simulation-mode behavior exactly (see
* xce_unsatisfiable_lookup_cb_t/xce_unsatisfiable_populate_cb_t's own doc
* comment, core.h, for the full "why NULL is correct for sim mode"
* rationale).
*
* The (xce_scheduler_t*)core cast below is safe for the identical reason
* xce_alloc_cache_clear_cb()'s own cast is safe just above: these two
* adapters are normal-mode-exclusive -- only ever assigned onto
* core->unsatisfiable_lookup_cb/unsatisfiable_populate_cb by main.c's
* normal-mode branch, only ever invoked with a `core` pointer genuinely
* embedded in an xce_scheduler_t (xce_sched_core_t core is
* xce_scheduler_t's first member, scheduler.h) -- a simulation-mode core
* never registers either callback, so this cast is never reached with a
* xce_sched_sim_driver_t*-embedded core. This is exactly the "small
* adapter function... itself normal-mode-exclusive" allowance design.md's
* "Unsatisfiable-Cache Callback Seam" component documents -- the cast
* must never appear in code shared by both drivers, but is safe here.
*/
int xce_alloc_cache_lookup_cb(xce_sched_core_t* core, xce_sched_obj_t* obj,
int* out_reason_category)
{
return cache_lookup((xce_scheduler_t*)core, obj, out_reason_category);
}
void xce_alloc_cache_populate_cb(xce_sched_core_t* core, xce_sched_obj_t* obj,
int reason_category)
{
cache_insert((xce_scheduler_t*)core, obj, reason_category);
}

View file

@ -471,6 +471,28 @@ int main(int argc, char* argv[])
* "Simulation isolation"). */
xce_core_set_cache_clear_cb(&(sched.core), xce_alloc_cache_clear_cb);
/* Req 5.2 (scheduler-sim-live-allocation spec, task 1.1): register the
* optional unsatisfiable-resreq cache consult/populate seam -- see
* xce_unsatisfiable_lookup_cb_t/xce_unsatisfiable_populate_cb_t's own
* doc comment (core.h) for the full rationale. Direct field
* assignment, not a setter call (unlike cache_clear_cb just above):
* xce_sched_core_t is not opaque, and these two fields carry no
* locking/session-active discipline beyond the plain NULL-check the
* seam's own contract already gives them -- main.c already writes
* xce_sched_core_t fields directly elsewhere (e.g. sig_handler()'s
* `sched->core.stop = 1;`, above), so this is a precedented idiom, not
* a new one. Registered once, alongside every other driver-registered
* core hook, before the first scheduling round. The simulation driver
* (sim_loop.c) deliberately never sets either field -- a
* simulation-mode core has no cache to consult or populate (Req 5 AC
* 17 "Simulation isolation", same rationale as cache_clear_cb just
* above). Wiring the actual consult/populate call site into
* sched_make_primary_alloc_() (scheduler.c) is a LATER task's job
* (tasks.md 2.2) -- until then, both fields being non-NULL here has
* no observable effect, since nothing yet reads them. */
sched.core.unsatisfiable_lookup_cb = xce_alloc_cache_lookup_cb;
sched.core.unsatisfiable_populate_cb = xce_alloc_cache_populate_cb;
signal(SIGINT, sig_handler);
INFO("xce scheduler is running");

View file

@ -420,6 +420,140 @@ static void test_argument_validation(void)
fini_sched(&sched);
}
/* ---------------------------------------------------------------------- */
/* Task 1.1 (scheduler-sim-live-allocation spec, Req 5.2): the */
/* unsatisfiable-cache callback seam. */
/* ---------------------------------------------------------------------- */
/*
* A freshly-zeroed xce_sched_core_t (the same memset() default
* xce_sched_core_init() applies -- core.c is not linked into this
* white-box TU, so this test zeroes a bare instance directly rather than
* calling that function) leaves BOTH new callback fields NULL. This is
* the exact state every simulation-mode xce_sched_core_t is in forever --
* sim_loop.c never registers either callback (Req 5 AC 17 "Simulation
* isolation").
*/
static void test_seam_fields_default_null_on_zeroed_core(void)
{
xce_sched_core_t core;
memset(&core, 0, sizeof(core));
TEST_ASSERT_NULL(core.unsatisfiable_lookup_cb);
TEST_ASSERT_NULL(core.unsatisfiable_populate_cb);
}
/*
* THE Observable this task's own TDD note names: "with both callbacks
* left NULL, the allocation dispatch chokepoint always computes fresh
* with no crash and no incorrect skip." The real chokepoint
* (sched_make_primary_alloc_(), scheduler.c) is migrated to actually
* consult this seam by a LATER task (tasks.md 2.2, boundary "Core-Safe
* Allocation Chain, Unsatisfiable-Cache Callback Seam") -- this task adds
* only the seam itself, not that call site. This test proves the seam's
* own NULL-safety contract in isolation, using the identical NULL-check
* shape design.md's own invariant specifies for that future call site
* ("the allocation chain ... only ever NULL-checks and calls these"):
* with both fields NULL, a NULL-checked consult never dereferences either
* function pointer (no crash), is never mistaken for a cache HIT (no
* incorrect skip -- `hit` stays 0, meaning "compute fresh"), and a
* NULL-checked populate attempt is silently skipped, not a fatal error.
*/
static void test_null_callbacks_mean_always_compute_fresh(void)
{
xce_sched_core_t core;
xce_sched_obj_t obj;
xce_primary_schedobj_t primary;
int category = -1;
int hit = -1; /* poison: neither 0 nor 1 yet */
memset(&core, 0, sizeof(core));
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
BASE_QUEUE, BASE_PROJECT);
/* Prove the guard's PRECONDITION explicitly, immediately before relying
* on it below -- not just that the end result happens to be "compute
* fresh", but that the NULL-ness this test's two guarded calls depend
* on genuinely holds at the exact point each guard is evaluated. A
* design that accidentally called through a non-NULL stale/garbage
* pointer here (e.g. an uninitialized field instead of a true NULL)
* could still coincidentally return 0/not crash on some platforms;
* asserting NULL directly, right where each guard fires, rules that
* out. */
TEST_ASSERT_NULL(core.unsatisfiable_lookup_cb);
if (core.unsatisfiable_lookup_cb != NULL) {
hit = core.unsatisfiable_lookup_cb(&core, &obj, &category);
} else {
hit = 0; /* no cache registered => always a miss => compute fresh */
}
TEST_ASSERT_EQUAL_INT(0, hit);
/* A NULL-checked populate attempt must be a true no-op, not a crash --
* same "assert the guard's own precondition, right at the guard"
* discipline as the lookup check just above. */
TEST_ASSERT_NULL(core.unsatisfiable_populate_cb);
if (core.unsatisfiable_populate_cb != NULL) {
core.unsatisfiable_populate_cb(&core, &obj, 7);
}
}
/*
* Normal-mode observable (this task's own note): "a normal-mode test
* confirming the cache is still consulted/populated exactly as before
* this change." xce_alloc_cache_lookup_cb()/xce_alloc_cache_populate_cb()
* (alloc_cache.c) are the two adapters main.c registers on the real
* xce_scheduler_t's core (xce_sched_core_t core is xce_scheduler_t's
* first member, scheduler.h -- the same pointer-to-first-member idiom
* xce_alloc_cache_clear_cb() already uses). This test calls them through
* core.unsatisfiable_lookup_cb/unsatisfiable_populate_cb exactly as a
* normal-mode driver would, and confirms the result is identical to
* calling cache_lookup()/cache_insert() directly -- proving the adapters
* are a transparent passthrough, not a behavior change.
*/
static void test_lookup_and_populate_cb_adapters_match_direct_cache_calls(void)
{
xce_scheduler_t sched;
xce_sched_obj_t obj;
xce_primary_schedobj_t primary;
int out_direct = -1;
int out_via_cb = -1;
int rc;
init_sched(&sched);
sched.core.unsatisfiable_lookup_cb = xce_alloc_cache_lookup_cb;
sched.core.unsatisfiable_populate_cb = xce_alloc_cache_populate_cb;
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
BASE_QUEUE, BASE_PROJECT);
/* Miss before any insert, via the callback -- same as a direct
* cache_lookup() call (test_lookup_before_any_insert_is_a_miss
* above). */
TEST_ASSERT_NOT_NULL(sched.core.unsatisfiable_lookup_cb);
rc = sched.core.unsatisfiable_lookup_cb(&(sched.core), &obj, &out_via_cb);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Populate via the callback... */
TEST_ASSERT_NOT_NULL(sched.core.unsatisfiable_populate_cb);
sched.core.unsatisfiable_populate_cb(&(sched.core), &obj, 55);
/* ...then confirm a DIRECT cache_lookup() call sees the SAME entry --
* the adapter genuinely populated the real cache, not a shadow copy. */
rc = cache_lookup(&sched, &obj, &out_direct);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_EQUAL_INT(55, out_direct);
/* And the callback-based lookup now also reports the same hit. */
rc = sched.core.unsatisfiable_lookup_cb(&(sched.core), &obj, &out_via_cb);
TEST_ASSERT_EQUAL_INT(1, rc);
TEST_ASSERT_EQUAL_INT(55, out_via_cb);
fini_sched(&sched);
}
int main(void)
{
UNITY_BEGIN();
@ -436,5 +570,8 @@ int main(void)
RUN_TEST(test_clear_empties_all_entries);
RUN_TEST(test_insert_duplicate_key_returns_eexist);
RUN_TEST(test_argument_validation);
RUN_TEST(test_seam_fields_default_null_on_zeroed_core);
RUN_TEST(test_null_callbacks_mean_always_compute_fresh);
RUN_TEST(test_lookup_and_populate_cb_adapters_match_direct_cache_calls);
return UNITY_END();
}