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:
parent
487e7b9419
commit
89297dc825
10 changed files with 1130 additions and 0 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue