feat(scheduler-allocation): clear unsatisfiable-resreq cache per session

Clear the cache inside core_begin_session_internal() so a job cached as
unsatisfiable in one session is re-evaluated fresh in the next (Req 7.2).

An initial weak-symbol cast-based approach was rejected: it corrupted
xce_sched_sim_driver_t.alt_events_file, since core_begin_session_internal()
is shared between driver.c (real xce_scheduler_t) and sim_loop.c (a
structurally different, deliberately isolated sim driver struct with no
cache field at the same byte offset) -- the strong cache_clear() symbol
won unconditionally for both, invisible to ASAN and untested by any
test_sim_*.c file.

Fixed via a driver-registered callback: xce_sched_core_t gains a
cache_clear_cb hook (NULL by default, per xce_sched_core_init()'s
full-struct memset), set only in main.c's driver-mode branch --
mirroring the existing on_decision/xce_core_set_decision_cb() pattern.
--sim mode returns before that registration is ever reached, so its hook
stays NULL and core_begin_session_internal()'s guarded call is a no-op.
The real xce_scheduler_t-casting adapter lives in alloc_cache.c rather
than driver.c, since ~11 tests white-box-include driver.c without
linking alloc_cache.c.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
Rongsong Shen 2026-09-05 00:57:46 +00:00
parent 5c022d0b4f
commit e8c2087d5e
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
9 changed files with 614 additions and 4 deletions

View file

@ -69,6 +69,46 @@ typedef struct xce_sched_core_s xce_sched_core_t;
*/
typedef int (*xce_core_decision_cb_t)(void*, xce_decision_t*);
/*
* xce_core_cache_clear_cb_t - driver-registered per-session cache-clear
* hook (scheduler-allocation spec, task 5.2 round 2, Req 7.2):
* `void (*)(xce_sched_core_t*)`. Mirrors xce_core_decision_cb_t's
* driver-registered-hook idiom just above, but generic on purpose -- no
* `void* driver_ctx` companion field, because the sole real
* implementation (alloc_cache.c's xce_alloc_cache_clear_cb() -- see that
* function's own doc comment for why it lives in alloc_cache.c rather
* than driver.c) recovers its owning xce_scheduler_t* via the
* pointer-to-first-member idiom this codebase already uses for this exact
* struct (scheduler.h: `xce_sched_core_t core` is xce_scheduler_t's first
* member), not via an opaque context pointer.
*
* WHY a hook instead of a direct call (task 5.2 round 1's CRITICAL defect):
* core_begin_session_internal() (core_session.c) is reached from BOTH
* driver.c's xce_sched_driver_run_normal_mode_round() (a real
* xce_scheduler_t*) and sim_loop.c's sched_sim_driver_run() (a
* structurally different xce_sched_sim_driver_t*, sim.h -- Req 5 AC 17
* "Simulation isolation" -- which has NO unsatisfiable_cache field at
* all). Round 1 called alloc_cache.c's real, strong
* cache_clear(xce_scheduler_t*) unconditionally for both callers (routed
* through a same-named weak fallback that the strong definition always
* overrides at link time in the production binary) -- since link-time
* weak/strong resolution picks ONE winner process-wide, not per call
* site, every simulation-mode session reinterpreted whatever field sits
* at unsatisfiable_cache's byte offset in xce_sched_sim_driver_t
* (alt_events_file, a live FILE*) as a kvtab_t* and clobbered it.
*
* A per-core-instance function pointer fixes this at the data-ownership
* level: each xce_sched_core_t only ever carries the hook ITS OWN driver
* installed via xce_core_set_cache_clear_cb() below. main.c installs
* alloc_cache.c's xce_alloc_cache_clear_cb() on the real xce_scheduler_t's
* core; sim_loop.c never calls the setter at all, so a simulation-mode core's
* hook stays NULL (xce_sched_core_init()'s memset() default) and
* core_begin_session_internal() treats NULL as "this driver has no cache
* to clear" and skips the call entirely -- a true no-op, not an
* accidental type-confused one.
*/
typedef void (*xce_core_cache_clear_cb_t)(xce_sched_core_t*);
struct xce_sched_core_s {
mylock_t lock;
int stop;
@ -110,6 +150,15 @@ struct xce_sched_core_s {
* reads these fields today. */
void* driver_ctx;
xce_core_decision_cb_t on_decision;
/* Req 7.2 (scheduler-allocation spec, task 5.2 round 2): driver-
* registered per-session cache-clear hook -- see
* xce_core_cache_clear_cb_t's own doc comment above for the full
* rationale. NULL until a driver registers one (xce_core_set_
* cache_clear_cb()); core_begin_session_internal() (core_session.c)
* treats NULL as "this driver has no cache to clear" and skips the
* call -- true for every simulation-mode xce_sched_core_t, which never
* registers this hook. */
xce_core_cache_clear_cb_t cache_clear_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 */
@ -424,6 +473,21 @@ extern int xce_core_push_events(xce_sched_core_t*, xce_core_event_t*, int);
extern int xce_core_set_decision_cb(xce_sched_core_t*, xce_core_decision_cb_t,
void*);
/*
* xce_core_set_cache_clear_cb - registers the driver's per-session
* cache-clear hook (scheduler-allocation spec, task 5.2 round 2, Req 7.2).
* Same calling convention as xce_core_set_decision_cb() above (may be
* called at any time, typically once at driver startup before the first
* xce_core_begin_session(); returns -EINVAL if core is NULL) -- no
* `driver_ctx` companion parameter, since xce_core_cache_clear_cb_t's own
* doc comment (above) explains why the real implementation needs none.
* Never called by sim_loop.c -- see that typedef's doc comment for why
* leaving this hook unset is the correct, safe default for simulation
* mode.
*/
extern int xce_core_set_cache_clear_cb(xce_sched_core_t*,
xce_core_cache_clear_cb_t);
/*
* xce_core_begin_session / xce_core_signal_go - Req 5 AC 5 (task 4.4) split
* into two exported halves (task 5.2, Req 1 AC 7 / Req 5 AC 9) so a driver