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:
parent
d5d2c1caf5
commit
5a6706761d
9 changed files with 550 additions and 4 deletions
|
|
@ -240,6 +240,12 @@ typedef struct {
|
|||
int maxTasks;
|
||||
xce_select_t* select;
|
||||
xlist_t rusages;
|
||||
/* Req 7.1 (scheduler-allocation spec): stable resource-usage
|
||||
* fingerprint over `rusages`, set once by parse_resreq_primary() and
|
||||
* stable for the life of this resreq -- every later cache lookup for
|
||||
* the job this resreq belongs to reuses this value instead of
|
||||
* recomputing it. See resreq.c's hash_rusages(). */
|
||||
uint64_t rusage_hash;
|
||||
xce_span_t* span;
|
||||
xce_same_t* same;
|
||||
xce_affinity_t* affinity;
|
||||
|
|
|
|||
|
|
@ -159,6 +159,7 @@ libxce_test_src = ['testsuite/test.c',
|
|||
'testsuite/test_limit.c',
|
||||
'testsuite/test_xvec.c',
|
||||
'testsuite/test_resreq.c',
|
||||
'testsuite/test_resreq_fingerprint.c',
|
||||
'testsuite/test_resmgr_fixture.c',
|
||||
'testsuite/test_select.c',
|
||||
'testsuite/test_bitset.c',
|
||||
|
|
|
|||
|
|
@ -304,6 +304,90 @@ cleanup_parser:
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* Req 7.1 (scheduler-allocation spec): stable resource-usage fingerprint
|
||||
* over a primary resreq's parsed `rusages` list.
|
||||
*
|
||||
* Style: FNV-1a 64-bit, consistent with this spec's cache-key hash
|
||||
* (requirements.md "New types (Req 7)", cache_key_hash_ -- FNV-1a 32-bit
|
||||
* over xce_cache_key_t, a later task's own scope). No existing hash-update
|
||||
* helper for this purpose exists anywhere in this codebase; this is new,
|
||||
* self-contained, file-local code.
|
||||
*
|
||||
* Design choice -- ORDER-DEPENDENT: each rusage_value_t is folded into the
|
||||
* hash in xlist storage order (the order its `rusage[...]` clause was
|
||||
* parsed in), rather than sorted/combined commutatively. This is simpler
|
||||
* (no auxiliary sort, no need for a commutative combine step) and is
|
||||
* sufficient for Req 7.1's actual requirement: parsing the *same* resreq
|
||||
* string twice always produces the same xlist order, so the fingerprint
|
||||
* is stable for identical input, which is all the unsatisfiable-resreq
|
||||
* cache needs (a false miss on a reordered-but-equivalent resreq costs one
|
||||
* redundant, still-correct allocation attempt; it can never cause a false
|
||||
* hit).
|
||||
*
|
||||
* Field selection -- deliberately per-field, NOT a raw memcmp/byte-hash of
|
||||
* `sizeof(rusage_value_t)`: `rusage_value_t.res` is a `char*` (owned,
|
||||
* strdup'd per parse -- see kill_rusage_value()'s plain `FREEUP(val->res)`,
|
||||
* not an xstring-interned/refcounted pointer). Hashing the raw struct
|
||||
* bytes would hash the POINTER VALUE (heap address), which differs across
|
||||
* separate parses of identical content and would make the "fingerprint"
|
||||
* unstable by construction. Each field is hashed individually: the
|
||||
* resource name's string CONTENT (not its pointer), then the raw bytes of
|
||||
* `value`/`per_flag`/`duration`/`decay_flag` (safe to hash as raw bytes --
|
||||
* none of those are pointers). */
|
||||
static uint64_t hash_rusages(xlist_t* rusages)
|
||||
{
|
||||
uint64_t h = 14695981039346656037ULL; /* FNV-1a 64-bit offset basis */
|
||||
int i;
|
||||
|
||||
if (rusages == NULL) {
|
||||
return h;
|
||||
}
|
||||
|
||||
for (i = 0; i < rusages->count; i++) {
|
||||
rusage_value_t* v = (rusage_value_t*)xlist_get(rusages, i);
|
||||
const unsigned char* p;
|
||||
size_t n;
|
||||
size_t j;
|
||||
|
||||
if (v == NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Resource name: hash the string content, not the pointer. */
|
||||
p = (v->res != NULL) ? (const unsigned char*)v->res
|
||||
: (const unsigned char*)"";
|
||||
n = (v->res != NULL) ? strlen(v->res) : 0;
|
||||
for (j = 0; j < n; j++) {
|
||||
h = (h ^ (uint64_t)p[j]) * 1099511628211ULL;
|
||||
}
|
||||
/* Field separator -- keeps a name's trailing bytes from aliasing
|
||||
* with the numeric fields that follow (e.g. "ab"+1 vs "a"+"b1"). */
|
||||
h = (h ^ 0xFFu) * 1099511628211ULL;
|
||||
|
||||
p = (const unsigned char*)&v->value;
|
||||
for (j = 0; j < sizeof(v->value); j++) {
|
||||
h = (h ^ (uint64_t)p[j]) * 1099511628211ULL;
|
||||
}
|
||||
|
||||
p = (const unsigned char*)&v->per_flag;
|
||||
for (j = 0; j < sizeof(v->per_flag); j++) {
|
||||
h = (h ^ (uint64_t)p[j]) * 1099511628211ULL;
|
||||
}
|
||||
|
||||
p = (const unsigned char*)&v->duration;
|
||||
for (j = 0; j < sizeof(v->duration); j++) {
|
||||
h = (h ^ (uint64_t)p[j]) * 1099511628211ULL;
|
||||
}
|
||||
|
||||
p = (const unsigned char*)&v->decay_flag;
|
||||
for (j = 0; j < sizeof(v->decay_flag); j++) {
|
||||
h = (h ^ (uint64_t)p[j]) * 1099511628211ULL;
|
||||
}
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
int parse_resreq_primary(xce_primary_resreq_t* primary, char* resreqString,
|
||||
var resmgr, char** errmsg)
|
||||
{
|
||||
|
|
@ -347,6 +431,10 @@ int parse_resreq_primary(xce_primary_resreq_t* primary, char* resreqString,
|
|||
primary->minTasks = 1;
|
||||
primary->maxTasks = 1;
|
||||
|
||||
/* Req 7.1: compute once, at parse time -- stable for the life of this
|
||||
* resreq, reused by every later cache lookup instead of recomputing. */
|
||||
primary->rusage_hash = hash_rusages(&(primary->rusages));
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ extern void test_xlist(void);
|
|||
extern void test_limit(void);
|
||||
extern void test_xvec(void);
|
||||
extern void test_resreq(void);
|
||||
extern void test_resreq_fingerprint(void);
|
||||
extern void test_select(void);
|
||||
extern void test_bitset(void);
|
||||
extern void test_kv(void);
|
||||
|
|
@ -145,6 +146,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_limit);
|
||||
TEST(test_xvec);
|
||||
TEST(test_resreq);
|
||||
TEST(test_resreq_fingerprint);
|
||||
TEST(test_select);
|
||||
TEST(test_bitset);
|
||||
TEST(test_kv);
|
||||
|
|
|
|||
348
src/lib/testsuite/test_resreq_fingerprint.c
Normal file
348
src/lib/testsuite/test_resreq_fingerprint.c
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
/* test_resreq_fingerprint.c — Req 7.1 (scheduler-allocation spec, task 1.2):
|
||||
* resreq fingerprinting for the unsatisfiable-resreq cache.
|
||||
*
|
||||
* Scope of THIS test file: xce_primary_resreq_t's new rusage_hash field
|
||||
* (set once by parse_resreq_primary(), src/lib/resreq.c) -- stability
|
||||
* across repeated parses of an identical resreq, and sensitivity to any
|
||||
* single differing rusage field.
|
||||
*
|
||||
* xce_resreq_fingerprint_t itself (min_tasks/max_tasks/select_node_id/
|
||||
* rusage_hash) is defined in src/scheduler/include/sched/types.h, not
|
||||
* src/include -- it is a scheduler-side value object. src/lib/testsuite
|
||||
* builds against src/lib's own include path only (src/lib/meson.build:
|
||||
* `incdir = include_directories('../include')`) and cannot reach
|
||||
* src/scheduler/include without a layering violation (the scheduler
|
||||
* depends on libxce, never the reverse). To still prove "the struct
|
||||
* correctly reflects min_tasks/max_tasks [and rusage_hash] from a parsed
|
||||
* resreq" (this task's Observable), this file declares a local mirror
|
||||
* struct with the identical field layout and populates it exactly the way
|
||||
* a future job-ingest call site will. The real struct, and its population
|
||||
* at job ingest, is exercised by the scheduler test suite once that
|
||||
* wiring lands (out of this task's boundary -- see task 1.2's notes).
|
||||
*/
|
||||
#define UNIT_TEST 1 /* sel_optimize_test_reset() visibility, see test_select.c */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "resreq.h"
|
||||
#include "test_resmgr_fixture.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Mirrors src/scheduler/include/sched/types.h's xce_resreq_fingerprint_t
|
||||
* layout exactly: int min_tasks; int max_tasks; uint32_t select_node_id;
|
||||
* uint64_t rusage_hash. See the file header comment for why this is a
|
||||
* local duplicate rather than an #include. */
|
||||
typedef struct {
|
||||
int min_tasks;
|
||||
int max_tasks;
|
||||
uint32_t select_node_id;
|
||||
uint64_t rusage_hash;
|
||||
} xce_resreq_fingerprint_t;
|
||||
|
||||
/* Builds the fingerprint the same way a real job-ingest call site
|
||||
* (core_build_primary_schedobj(), src/scheduler/src/core_events.c) will:
|
||||
* min/max tasks and the parse-time rusage_hash copied verbatim, and the
|
||||
* select root's CSE-interned node_id (0 if select is NULL). */
|
||||
static xce_resreq_fingerprint_t
|
||||
fingerprint_from_resreq(const xce_primary_resreq_t* r)
|
||||
{
|
||||
xce_resreq_fingerprint_t fp;
|
||||
|
||||
fp.min_tasks = r->minTasks;
|
||||
fp.max_tasks = r->maxTasks;
|
||||
fp.select_node_id
|
||||
= (r->select != NULL && r->select->root != NULL) ? r->select->root->node_id
|
||||
: 0;
|
||||
fp.rusage_hash = r->rusage_hash;
|
||||
|
||||
return fp;
|
||||
}
|
||||
|
||||
/* ── (a) stability across repeated computation for an identical resreq ── */
|
||||
|
||||
void test_resreq_fingerprint_stable_across_identical_parse(void)
|
||||
{
|
||||
char* resreqString
|
||||
= "rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]";
|
||||
char* errmsg1 = NULL;
|
||||
char* errmsg2 = NULL;
|
||||
int rc1;
|
||||
int rc2;
|
||||
xce_primary_resreq_t r1;
|
||||
xce_primary_resreq_t r2;
|
||||
|
||||
rc1 = parse_resreq_primary(&r1, resreqString, NULL, &errmsg1);
|
||||
rc2 = parse_resreq_primary(&r2, resreqString, NULL, &errmsg2);
|
||||
TEST_ASSERT_TRUE(rc1 >= 0);
|
||||
TEST_ASSERT_TRUE(rc2 >= 0);
|
||||
|
||||
/* A hash over non-empty rusage content should not degenerate to 0. */
|
||||
TEST_ASSERT_NOT_EQUAL_UINT64(0, r1.rusage_hash);
|
||||
|
||||
/* Stability: identical resreq string parsed twice -> identical hash. */
|
||||
TEST_ASSERT_EQUAL_UINT64(r1.rusage_hash, r2.rusage_hash);
|
||||
|
||||
{
|
||||
xce_resreq_fingerprint_t fp1 = fingerprint_from_resreq(&r1);
|
||||
xce_resreq_fingerprint_t fp2 = fingerprint_from_resreq(&r2);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(fp1.min_tasks, fp2.min_tasks);
|
||||
TEST_ASSERT_EQUAL_INT(fp1.max_tasks, fp2.max_tasks);
|
||||
TEST_ASSERT_EQUAL_UINT32(fp1.select_node_id, fp2.select_node_id);
|
||||
TEST_ASSERT_EQUAL_UINT64(fp1.rusage_hash, fp2.rusage_hash);
|
||||
}
|
||||
|
||||
kill_primary_resreq(&r1);
|
||||
kill_primary_resreq(&r2);
|
||||
FREEUP(errmsg1);
|
||||
FREEUP(errmsg2);
|
||||
}
|
||||
|
||||
/* Re-parsing the same resreq into a fresh xce_primary_resreq_t multiple
|
||||
* times in a loop must always land on the same hash -- guards against any
|
||||
* source of nondeterminism (e.g. hashing an uninitialized/pointer field)
|
||||
* that a two-shot test could miss by luck. */
|
||||
void test_resreq_fingerprint_stable_across_repeated_reparse(void)
|
||||
{
|
||||
char* resreqString = "rusage[mem=42:duration=60:decay=0]";
|
||||
uint64_t first_hash = 0;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 5; i++) {
|
||||
char* errmsg = NULL;
|
||||
int rc;
|
||||
xce_primary_resreq_t r;
|
||||
|
||||
rc = parse_resreq_primary(&r, resreqString, NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
|
||||
if (i == 0) {
|
||||
first_hash = r.rusage_hash;
|
||||
} else {
|
||||
TEST_ASSERT_EQUAL_UINT64(first_hash, r.rusage_hash);
|
||||
}
|
||||
|
||||
kill_primary_resreq(&r);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── (b)/(more): differs when any single contributing field changes ───── */
|
||||
|
||||
static void assert_rusage_hash_differs(char* base_str, char* variant_str)
|
||||
{
|
||||
char* errmsg1 = NULL;
|
||||
char* errmsg2 = NULL;
|
||||
int rc1;
|
||||
int rc2;
|
||||
xce_primary_resreq_t rbase;
|
||||
xce_primary_resreq_t rvariant;
|
||||
|
||||
rc1 = parse_resreq_primary(&rbase, base_str, NULL, &errmsg1);
|
||||
rc2 = parse_resreq_primary(&rvariant, variant_str, NULL, &errmsg2);
|
||||
TEST_ASSERT_TRUE(rc1 >= 0);
|
||||
TEST_ASSERT_TRUE(rc2 >= 0);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL_UINT64(rbase.rusage_hash, rvariant.rusage_hash);
|
||||
|
||||
kill_primary_resreq(&rbase);
|
||||
kill_primary_resreq(&rvariant);
|
||||
FREEUP(errmsg1);
|
||||
FREEUP(errmsg2);
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_differs_on_rusage_value(void)
|
||||
{
|
||||
assert_rusage_hash_differs(
|
||||
"rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]",
|
||||
"rusage[mem=21:duration=300:decay=1,lic=55:duration=20m:decay=0]");
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_differs_on_resource_name(void)
|
||||
{
|
||||
assert_rusage_hash_differs(
|
||||
"rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]",
|
||||
"rusage[cpu=20:duration=300:decay=1,lic=55:duration=20m:decay=0]");
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_differs_on_per_flag(void)
|
||||
{
|
||||
assert_rusage_hash_differs(
|
||||
"rusage[mem=20:duration=300:decay=1]",
|
||||
"rusage[mem=20/host:duration=300:decay=1]");
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_differs_on_decay_flag(void)
|
||||
{
|
||||
assert_rusage_hash_differs(
|
||||
"rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]",
|
||||
"rusage[mem=20:duration=300:decay=0,lic=55:duration=20m:decay=0]");
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_differs_on_duration(void)
|
||||
{
|
||||
assert_rusage_hash_differs(
|
||||
"rusage[mem=20:duration=300:decay=1]",
|
||||
"rusage[mem=20:duration=301:decay=1]");
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_differs_on_rusage_count(void)
|
||||
{
|
||||
assert_rusage_hash_differs(
|
||||
"rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0]",
|
||||
"rusage[mem=20:duration=300:decay=1,lic=55:duration=20m:decay=0,gpu=1]");
|
||||
}
|
||||
|
||||
/* No rusage clause at all is a legitimate resreq (test_resreq_primary2 in
|
||||
* test_resreq.c already covers the parse side); the hash of an empty
|
||||
* rusages list must still be stable and must differ from a non-empty one. */
|
||||
void test_resreq_fingerprint_empty_rusages_stable_and_distinct(void)
|
||||
{
|
||||
char* errmsg1 = NULL;
|
||||
char* errmsg2 = NULL;
|
||||
char* errmsg3 = NULL;
|
||||
int rc1;
|
||||
int rc2;
|
||||
int rc3;
|
||||
xce_primary_resreq_t r1;
|
||||
xce_primary_resreq_t r2;
|
||||
xce_primary_resreq_t r3;
|
||||
|
||||
rc1 = parse_resreq_primary(&r1, "select[type==any] ", NULL, &errmsg1);
|
||||
rc2 = parse_resreq_primary(&r2, "select[type==any] ", NULL, &errmsg2);
|
||||
rc3 = parse_resreq_primary(&r3, "rusage[mem=1]", NULL, &errmsg3);
|
||||
TEST_ASSERT_TRUE(rc1 >= 0);
|
||||
TEST_ASSERT_TRUE(rc2 >= 0);
|
||||
TEST_ASSERT_TRUE(rc3 >= 0);
|
||||
|
||||
TEST_ASSERT_TRUE(r1.rusages.count == 0);
|
||||
TEST_ASSERT_EQUAL_UINT64(r1.rusage_hash, r2.rusage_hash);
|
||||
TEST_ASSERT_NOT_EQUAL_UINT64(r1.rusage_hash, r3.rusage_hash);
|
||||
|
||||
kill_primary_resreq(&r1);
|
||||
kill_primary_resreq(&r2);
|
||||
kill_primary_resreq(&r3);
|
||||
FREEUP(errmsg1);
|
||||
FREEUP(errmsg2);
|
||||
FREEUP(errmsg3);
|
||||
}
|
||||
|
||||
/* ── (c) xce_resreq_fingerprint_t correctly reflects min_tasks/max_tasks ─ */
|
||||
|
||||
void test_resreq_fingerprint_reflects_min_max_tasks(void)
|
||||
{
|
||||
char* errmsg = NULL;
|
||||
int rc;
|
||||
xce_primary_resreq_t r;
|
||||
xce_resreq_fingerprint_t fp;
|
||||
|
||||
rc = parse_resreq_primary(&r, "rusage[mem=1]", NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
|
||||
/* parse_resreq_primary() currently hardcodes minTasks=maxTasks=1 (a
|
||||
* pre-existing FIXME unrelated to Req 7.1 -- real min/max task parsing
|
||||
* is a different requirement's scope). This test proves the
|
||||
* fingerprint construction copies whatever the parser produces
|
||||
* through faithfully, not that the parser itself computes them. */
|
||||
fp = fingerprint_from_resreq(&r);
|
||||
TEST_ASSERT_EQUAL_INT(r.minTasks, fp.min_tasks);
|
||||
TEST_ASSERT_EQUAL_INT(r.maxTasks, fp.max_tasks);
|
||||
TEST_ASSERT_EQUAL_INT(1, fp.min_tasks);
|
||||
TEST_ASSERT_EQUAL_INT(1, fp.max_tasks);
|
||||
|
||||
kill_primary_resreq(&r);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
/* ── select_node_id coverage (bonus: not required by this task's escape
|
||||
* hatch, but a clean reuse of existing CSE test fixtures/pattern from
|
||||
* test_select.c, so included rather than skipped). ────────────────────── */
|
||||
|
||||
void test_resreq_fingerprint_select_node_id_zero_when_no_select(void)
|
||||
{
|
||||
char* errmsg = NULL;
|
||||
int rc;
|
||||
xce_primary_resreq_t r;
|
||||
xce_resreq_fingerprint_t fp;
|
||||
|
||||
rc = parse_resreq_primary(&r, "rusage[mem=20]", NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_NULL(r.select);
|
||||
|
||||
fp = fingerprint_from_resreq(&r);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, fp.select_node_id);
|
||||
|
||||
kill_primary_resreq(&r);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint_select_node_id_stable_for_identical_select(void)
|
||||
{
|
||||
var resmgr = make_test_resmgr();
|
||||
char* resreqString = "select[type==any] rusage[mem=20]";
|
||||
char* errmsg1 = NULL;
|
||||
char* errmsg2 = NULL;
|
||||
int rc1;
|
||||
int rc2;
|
||||
xce_primary_resreq_t r1;
|
||||
xce_primary_resreq_t r2;
|
||||
xce_resreq_fingerprint_t fp1;
|
||||
xce_resreq_fingerprint_t fp2;
|
||||
|
||||
sel_node_table_init();
|
||||
sel_optimize_set(1);
|
||||
|
||||
rc1 = parse_resreq_primary(&r1, resreqString, resmgr, &errmsg1);
|
||||
rc2 = parse_resreq_primary(&r2, resreqString, resmgr, &errmsg2);
|
||||
TEST_ASSERT_TRUE(rc1 >= 0);
|
||||
TEST_ASSERT_TRUE(rc2 >= 0);
|
||||
TEST_ASSERT_NOT_NULL(r1.select);
|
||||
TEST_ASSERT_NOT_NULL(r2.select);
|
||||
TEST_ASSERT_NOT_NULL(r1.select->root);
|
||||
TEST_ASSERT_NOT_NULL(r2.select->root);
|
||||
|
||||
fp1 = fingerprint_from_resreq(&r1);
|
||||
fp2 = fingerprint_from_resreq(&r2);
|
||||
|
||||
/* CSE interns identical select ASTs to the same node -> same node_id.
|
||||
* This demonstrates select_node_id's contribution to fingerprint
|
||||
* stability without re-testing the CSE machinery itself (already
|
||||
* covered by test_select.c's own CSE tests). */
|
||||
TEST_ASSERT_NOT_EQUAL_UINT32(0, fp1.select_node_id);
|
||||
TEST_ASSERT_EQUAL_UINT32(fp1.select_node_id, fp2.select_node_id);
|
||||
|
||||
/* kill_primary_resreq() frees only the per-resreq select wrapper; in
|
||||
* CSE mode (optimize level > 0) it correctly leaves the interned node
|
||||
* itself alone (GC-owned) -- sel_optimize_test_reset() below tears
|
||||
* down the whole intern table. */
|
||||
kill_primary_resreq(&r1);
|
||||
kill_primary_resreq(&r2);
|
||||
FREEUP(errmsg1);
|
||||
FREEUP(errmsg2);
|
||||
|
||||
sel_optimize_test_reset();
|
||||
free_test_resmgr(resmgr);
|
||||
}
|
||||
|
||||
void test_resreq_fingerprint(void)
|
||||
{
|
||||
printf("\n>> resreq fingerprint test (Req 7.1)\n\n");
|
||||
|
||||
RUN_TEST(test_resreq_fingerprint_stable_across_identical_parse);
|
||||
RUN_TEST(test_resreq_fingerprint_stable_across_repeated_reparse);
|
||||
RUN_TEST(test_resreq_fingerprint_differs_on_rusage_value);
|
||||
RUN_TEST(test_resreq_fingerprint_differs_on_resource_name);
|
||||
RUN_TEST(test_resreq_fingerprint_differs_on_per_flag);
|
||||
RUN_TEST(test_resreq_fingerprint_differs_on_decay_flag);
|
||||
RUN_TEST(test_resreq_fingerprint_differs_on_duration);
|
||||
RUN_TEST(test_resreq_fingerprint_differs_on_rusage_count);
|
||||
RUN_TEST(test_resreq_fingerprint_empty_rusages_stable_and_distinct);
|
||||
RUN_TEST(test_resreq_fingerprint_reflects_min_max_tasks);
|
||||
RUN_TEST(test_resreq_fingerprint_select_node_id_zero_when_no_select);
|
||||
RUN_TEST(test_resreq_fingerprint_select_node_id_stable_for_identical_select);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue