feat(scheduler-sim-live-allocation): make allocation dispatch and decision commit core-safe (task 2.2)

Retypes sched_make_allocation()/sched_make_primary_allocation()/
sched_make_compound_allocation()/sched_make_alter_allocation()/
sched_commit_decision(), plus 3 private helpers found necessary
during migration (sched_make_primary_alloc_(), sched_make_seqjob_
allocation(), sched_make_parjob_allocation()) from xce_scheduler_t*
to xce_sched_core_t* in scheduler.c.

Wires the one genuine normal-mode-only touch point -- the
unsatisfiable-resreq cache, inside sched_make_primary_alloc_() --
through the task-1.1 NULL-safe callback seam (core->unsatisfiable_
lookup_cb/populate_cb) instead of a direct field read. No other
normal-mode-only state was found in any of the 8 migrated functions,
independently confirmed by both the implementer and reviewer against
xce_scheduler_t's own field list -- no escalation needed.

Minimal &(sched->core) call-site wraps applied inside sched_allocator()/
commit_decision() (task 3.1's home, not yet migrated), matching the
pattern established in tasks 1.2/1.3/2.1.

TDD: two-part RED -- a compile-error RED from the sched_commit_decision
retype, and a behavioral RED using spy callbacks proving the cache
seam was genuinely unconsulted before this fix.

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 14:53:59 +00:00
parent d52c78e4be
commit ecdea8bf55
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
6 changed files with 213 additions and 51 deletions

View file

@ -95,6 +95,7 @@
#include "preason.h"
#include "resobj.h"
#include "resreq.h"
#include "sched/alloc_cache.h"
#include "scheduler.h"
#include "xce/cluster.h"
#include "xstring.h"
@ -154,6 +155,42 @@ int __wrap_sched_meet_host_selection(xce_sched_core_t* core,
return __real_sched_meet_host_selection(core, w, select);
}
/* ════════════════════════════════════════════════════════════════════════
* spy_unsatisfiable_lookup_cb / spy_unsatisfiable_populate_cb -
* scheduler-sim-live-allocation spec, task 2.2 (Unsatisfiable-Cache
* Callback Seam consumer wiring): registered on sched->core in place of
* the real xce_alloc_cache_lookup_cb()/xce_alloc_cache_populate_cb()
* adapters (main.c's own normal-mode registration, sched_init() itself
* never registers either), so this fixture can count real invocations
* while still delegating to the real cache implementation for correct
* hit/miss behavior. Before this task, sched_make_primary_alloc_()
* (scheduler.c) read sched->unsatisfiable_cache directly and never
* touched core->unsatisfiable_lookup_cb/unsatisfiable_populate_cb at all
* -- these counters staying at 0 through both dispatches is the genuine
* RED signal proving the seam is not yet wired.
* ════════════════════════════════════════════════════════════════════════ */
static int g_lookup_cb_calls = 0;
static int g_populate_cb_calls = 0;
static int spy_unsatisfiable_lookup_cb(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int* out_reason_category)
{
g_lookup_cb_calls++;
return xce_alloc_cache_lookup_cb(core, obj, out_reason_category);
}
static void spy_unsatisfiable_populate_cb(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int reason_category)
{
g_populate_cb_calls++;
xce_alloc_cache_populate_cb(core, obj, reason_category);
}
void setUp(void) { return; }
void tearDown(void) { return; }
@ -248,6 +285,16 @@ static xce_scheduler_t* start_sched(void)
* unsatisfiable for any job (see file header). */
add_worker_with_slots(&(sched->core), WORKER_NAME, slots_res->idx, 0.0f);
/* scheduler-sim-live-allocation spec, task 2.2: sched_init() itself
* never registers the unsatisfiable-cache seam (main.c's normal-mode
* branch is the only production registrant) -- register the spy
* adapters here so this fixture's cache-hit proof genuinely exercises
* the new seam, not just the underlying cache_lookup()/cache_insert()
* calls a pre-migration sched_make_primary_alloc_() would reach some
* other way. */
sched->core.unsatisfiable_lookup_cb = spy_unsatisfiable_lookup_cb;
sched->core.unsatisfiable_populate_cb = spy_unsatisfiable_populate_cb;
return sched;
}
@ -372,6 +419,16 @@ static void test_standalone_primary_cache_hit_skips_allocator(void)
calls_job1 > 0,
"job 1 (cache miss) must genuinely run the allocator's host "
"selection check at least once");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, g_lookup_cb_calls,
"job 1's cache miss must be consulted through core->"
"unsatisfiable_lookup_cb (Unsatisfiable-Cache Callback Seam), not "
"a direct cache_lookup() call");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, g_populate_cb_calls,
"job 1's exhausted attempt must populate the cache through "
"core->unsatisfiable_populate_cb, not a direct cache_insert() "
"call");
/* Job 2: identical fingerprint + identity -- cache HIT. Must pend
* immediately, with zero additional sched_meet_host_selection()
@ -394,6 +451,14 @@ static void test_standalone_primary_cache_hit_skips_allocator(void)
decision2->u.reason->reasons->main_reason,
"a cache hit must attribute the SAME pend reason category as the "
"original unsatisfiable determination (Req 7.5)");
TEST_ASSERT_EQUAL_INT_MESSAGE(
2, g_lookup_cb_calls,
"job 2's cache hit must also be consulted through core->"
"unsatisfiable_lookup_cb (one more call than after job 1)");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, g_populate_cb_calls,
"a cache HIT must never re-populate the cache -- "
"unsatisfiable_populate_cb call count stays at job 1's count");
}
int main(void)

View file

@ -66,6 +66,7 @@
#include "preason.h"
#include "resobj.h"
#include "resreq.h"
#include "sched/alloc_cache.h"
#include "scheduler.h"
#include "xce/cluster.h"
#include "xstring.h"
@ -119,6 +120,36 @@ int __wrap_sched_meet_host_selection(xce_sched_core_t* core,
return __real_sched_meet_host_selection(core, w, select);
}
/* ════════════════════════════════════════════════════════════════════════
* spy_unsatisfiable_lookup_cb / spy_unsatisfiable_populate_cb -- see
* test_scheduler_cache_dispatch.c's identical spies for the full
* rationale (scheduler-sim-live-allocation spec, task 2.2). Delegates to
* the real xce_alloc_cache_lookup_cb()/xce_alloc_cache_populate_cb()
* adapters so this file's compound-nested-primary cache-hit proof stays
* functionally correct while also counting real seam invocations.
* ════════════════════════════════════════════════════════════════════════ */
static int g_lookup_cb_calls = 0;
static int g_populate_cb_calls = 0;
static int spy_unsatisfiable_lookup_cb(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int* out_reason_category)
{
g_lookup_cb_calls++;
return xce_alloc_cache_lookup_cb(core, obj, out_reason_category);
}
static void spy_unsatisfiable_populate_cb(xce_sched_core_t* core,
xce_sched_obj_t* obj,
int reason_category)
{
g_populate_cb_calls++;
xce_alloc_cache_populate_cb(core, obj, reason_category);
}
void setUp(void) { return; }
void tearDown(void) { return; }
@ -209,6 +240,12 @@ static xce_scheduler_t* start_sched(void)
* unsatisfiable for any job. */
add_worker_with_slots(&(sched->core), WORKER_NAME, slots_res->idx, 0.0f);
/* scheduler-sim-live-allocation spec, task 2.2: see
* test_scheduler_cache_dispatch.c's identical registration for why
* sched_init() alone never wires this seam. */
sched->core.unsatisfiable_lookup_cb = spy_unsatisfiable_lookup_cb;
sched->core.unsatisfiable_populate_cb = spy_unsatisfiable_populate_cb;
return sched;
}
@ -343,6 +380,16 @@ static void test_compound_nested_primary_cache_hit_skips_allocator(void)
calls_jobA > 0,
"job A (cache miss) must genuinely run the allocator's host "
"selection check at least once for its nested primary");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, g_lookup_cb_calls,
"job A's cache miss must be consulted through core->"
"unsatisfiable_lookup_cb (Unsatisfiable-Cache Callback Seam), not "
"a direct cache_lookup() call, even for a compound-nested primary");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, g_populate_cb_calls,
"job A's exhausted attempt must populate the cache through "
"core->unsatisfiable_populate_cb, not a direct cache_insert() "
"call");
/* Job B: a different job, but its one nested primary shares an
* identical fingerprint (same resreq string) and identity (same
@ -365,6 +412,14 @@ static void test_compound_nested_primary_cache_hit_skips_allocator(void)
"a cache hit must attribute the SAME pend reason category as the "
"original unsatisfiable determination (Req 7.5), even across a "
"compound-nested primary");
TEST_ASSERT_EQUAL_INT_MESSAGE(
2, g_lookup_cb_calls,
"job B's cache hit must also be consulted through core->"
"unsatisfiable_lookup_cb (one more call than after job A)");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, g_populate_cb_calls,
"a cache HIT must never re-populate the cache -- "
"unsatisfiable_populate_cb call count stays at job A's count");
FREEUP(errmsg);
}

View file

@ -162,7 +162,7 @@ static void test_dispatch_commit_transitions_job_to_run(void)
decision = make_dispatch_decision(alloc);
TEST_ASSERT_NOT_NULL(decision);
rc = sched_commit_decision(&sched, decision);
rc = sched_commit_decision(&(sched.core), decision);
TEST_ASSERT_TRUE(rc >= 0);
/* the new behavior this task adds */
@ -218,7 +218,7 @@ static void test_dispatch_commit_survives_round_boundary_before_confirmation(voi
decision = make_dispatch_decision(alloc);
TEST_ASSERT_NOT_NULL(decision);
rc = sched_commit_decision(&sched, decision);
rc = sched_commit_decision(&(sched.core), decision);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_UINT(JOB_STAT_RUN, job->state);
TEST_ASSERT_NOT_NULL(job->accounts);
@ -264,7 +264,7 @@ static void test_pend_decision_does_not_touch_job_state(void)
decision = make_pend_decision(reason);
TEST_ASSERT_NOT_NULL(decision);
rc = sched_commit_decision(&sched, decision);
rc = sched_commit_decision(&(sched.core), decision);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_UINT(JOB_STAT_PEND, job->state);
@ -316,7 +316,7 @@ static void test_dispatch_commit_survives_several_rounds_before_confirmation(voi
decision = make_dispatch_decision(alloc);
TEST_ASSERT_NOT_NULL(decision);
rc = sched_commit_decision(&sched, decision);
rc = sched_commit_decision(&(sched.core), decision);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_UINT(JOB_STAT_RUN, job->state);
@ -396,7 +396,7 @@ static float run_dispatch_then_n_rounds(int nrounds)
decision = make_dispatch_decision(alloc);
TEST_ASSERT_NOT_NULL(decision);
rc = sched_commit_decision(&sched, decision);
rc = sched_commit_decision(&(sched.core), decision);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_UINT(JOB_STAT_RUN, job->state);