feat(scheduler-allocation): add resreq fingerprinting for the cache

Adds rusage_hash to xce_primary_resreq_t (computed once by
parse_resreq_primary() via a new hash_rusages() helper, hashing
rusage content rather than raw struct bytes since resname is a
non-interned strdup'd pointer), the xce_resreq_fingerprint_t value
type, and resreq_fp on xce_primary_schedobj_t, populated once at real
job-ingest time in core_build_primary_schedobj().

Task 1.2 (scheduler-allocation spec). Requirement 7.1.

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-04 15:00:12 +00:00
parent d5d2c1caf5
commit 5a6706761d
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
9 changed files with 550 additions and 4 deletions

View file

@ -30,10 +30,26 @@ typedef struct {
struct xce_sched_obj_;
typedef struct xce_sched_obj_ xce_sched_obj_t;
/* Req 7.1 (scheduler-allocation spec): pre-computed resreq portion of the
* unsatisfiable-resreq cache key -- resource-usage shape only, independent
* of job identity (user/queue/project, which the full xce_cache_key_t adds
* at lookup time). Value object, immutable once computed, fitting in a
* single cache line. See requirements.md's Requirement 7 "New types". */
typedef struct {
xce_sched_obj_t* owner;
xce_primary_resreq_t* resreq;
xce_pendreason_t* reason;
int min_tasks;
int max_tasks;
uint32_t select_node_id; /* CSE-interned select-root node_id; 0 if select is NULL */
uint64_t rusage_hash; /* copied from xce_primary_resreq_t.rusage_hash */
} xce_resreq_fingerprint_t;
typedef struct {
xce_sched_obj_t* owner;
xce_primary_resreq_t* resreq;
xce_pendreason_t* reason;
/* Req 7.1: populated once on job ingest (core_build_primary_schedobj(),
* src/scheduler/src/core_events.c), read by every later cache lookup
* for this job so the fingerprint is never recomputed. */
xce_resreq_fingerprint_t resreq_fp;
} xce_primary_schedobj_t;
typedef struct {

View file

@ -123,6 +123,17 @@ static void core_build_primary_schedobj(xce_sched_obj_t* obj,
primary->reason = NULL;
primary->owner = obj;
/* Req 7.1 (scheduler-allocation spec): compute the resreq fingerprint
* once, here, at real job-ingest time -- the only construction site
* for xce_primary_schedobj_t. Every later unsatisfiable-resreq cache
* lookup for this job reuses this value instead of recomputing it. */
primary->resreq_fp.min_tasks = resreq->minTasks;
primary->resreq_fp.max_tasks = resreq->maxTasks;
primary->resreq_fp.select_node_id =
(resreq->select != NULL && resreq->select->root != NULL)
? resreq->select->root->node_id : 0;
primary->resreq_fp.rusage_hash = resreq->rusage_hash;
obj->u.primary = primary;
}

View file

@ -1473,6 +1473,78 @@ static void test_config_push_decomposes_queues_resources_and_resvals(void)
TEST_ASSERT_EQUAL_PTR(found_queue, g_counts.last_queue_update);
}
/*
* Req 7.1 regression (scheduler-allocation spec, task 1.2, round 2):
* "The resreq fingerprint is stored on the scheduling object
* (xce_primary_schedobj_t) and computed once on job ingest." Round 1 added
* xce_resreq_fingerprint_t (sched/types.h) and rusage_hash (resreq.h/
* resreq.c) but never actually wired population into the real job-ingest
* construction site, core_build_primary_schedobj() (this file, right below
* the white-box #include) -- obj->u.primary->resreq_fp was left calloc'd
* to all-zero forever. This test calls the REAL core_build_primary_schedobj()
* (visible here via this file's white-box #include of ../src/core_events.c,
* same convention as every other static-function test in this file) with a
* REAL xce_primary_resreq_t produced by the REAL parse_resreq_primary()
* (src/lib/resreq.c, linked from libxce) -- not a mirror struct like
* src/lib/testsuite/test_resreq_fingerprint.c's own fingerprint_from_resreq()
* helper had to use (that file cannot reach src/scheduler/include's real
* xce_resreq_fingerprint_t/xce_primary_schedobj_t -- see its header
* comment) -- so this is the first test anywhere that proves the real
* struct is actually populated at the real ingest call site, not just that
* the standalone hashing logic works in isolation.
*
* select_node_id-nonzero case intentionally NOT covered here: producing a
* real interned CSE select-root (root->node_id != 0) requires
* sel_node_table_init() + sel_optimize_set(1) + UNIT_TEST-gated
* sel_optimize_test_reset() teardown -- machinery no existing fixture in
* this file sets up, and this file's tests are never torn down by design
* (see file header comment), so enabling CSE mode here would leak into
* every other test in this binary via the process-global select intern
* table. The underlying select_node_id assignment logic itself is already
* fully covered by test_resreq_fingerprint.c's
* test_resreq_fingerprint_select_node_id_stable_for_identical_select --
* this is a documented gap, not a missed case, per task 1.2's own escape
* hatch for exactly this situation.
*/
static void
test_core_build_primary_schedobj_populates_resreq_fp_from_parsed_resreq(void)
{
xce_sched_obj_t obj;
xce_primary_resreq_t resreq;
char* errmsg = NULL;
int rc;
memset(&obj, 0, sizeof(obj));
memset(&resreq, 0, sizeof(resreq));
rc = parse_resreq_primary(&resreq,
(char*)"rusage[mem=20:duration=300:decay=1]",
NULL, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NULL(resreq.select);
/* Non-degenerate: a real hash over non-empty rusage content -- rules
* out a trivial "both sides are 0" false pass below. */
TEST_ASSERT_NOT_EQUAL_UINT64(0, resreq.rusage_hash);
core_build_primary_schedobj(&obj, &resreq);
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, obj.tag);
TEST_ASSERT_NOT_NULL(obj.u.primary);
TEST_ASSERT_EQUAL_PTR(&resreq, obj.u.primary->resreq);
/* The fix under test: resreq_fp must mirror the parsed resreq's
* min/max tasks, select_node_id (0 -- no select expression here), and
* rusage_hash -- not sit at calloc's all-zero default. */
TEST_ASSERT_EQUAL_INT(resreq.minTasks, obj.u.primary->resreq_fp.min_tasks);
TEST_ASSERT_EQUAL_INT(resreq.maxTasks, obj.u.primary->resreq_fp.max_tasks);
TEST_ASSERT_EQUAL_UINT32(0, obj.u.primary->resreq_fp.select_node_id);
TEST_ASSERT_EQUAL_UINT64(resreq.rusage_hash,
obj.u.primary->resreq_fp.rusage_hash);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
}
/* Session-active rejection (Req 5 AC 8 / design.md precondition):
* core_push_events() is only valid between sessions. */
static void
@ -1558,6 +1630,7 @@ int main(void)
RUN_TEST(test_job_rusage_applies_state_when_hook_is_null);
RUN_TEST(test_job_rusage_reconciles_mem_reservation_against_usage);
RUN_TEST(test_config_push_decomposes_queues_resources_and_resvals);
RUN_TEST(test_core_build_primary_schedobj_populates_resreq_fp_from_parsed_resreq);
RUN_TEST(test_push_events_rejects_with_ebusy_when_session_active_no_mutation);
RUN_TEST(test_push_events_null_core_returns_einval);
RUN_TEST(test_push_events_null_events_with_positive_nevents_returns_einval);