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
|
|
@ -6,7 +6,7 @@
|
|||
- Define the plan-iterator and host-iterator contracts (initialize, produce next candidate, release resources) as reusable abstractions any allocator can adopt
|
||||
- Observable: the contracts compile and are referenced by at least a stub/no-op implementation with no allocator wired in yet
|
||||
- _Requirements: 2.1, 3.1_
|
||||
- [ ] 1.2 Add resreq fingerprinting for the unsatisfiable-resreq cache
|
||||
- [x] 1.2 Add resreq fingerprinting for the unsatisfiable-resreq cache
|
||||
- Compute a stable resource-usage fingerprint once when a resource request is parsed
|
||||
- Store that precomputed fingerprint on the job's scheduling object once at ingest, so every later cache lookup for that job reuses it instead of recomputing it (the full cache key, fingerprint plus user/queue/project identity, is still assembled fresh at each lookup)
|
||||
- Write a unit test proving the fingerprint is stable across repeated computation for an identical resource request and differs when any contributing field changes
|
||||
|
|
@ -107,3 +107,4 @@
|
|||
## Implementation Notes
|
||||
|
||||
- (1.1) `xce_host_iter_t.next()` returns `xce_sched_worker_t*` (`sched/types.h`, carries `resvals`/`resInUse`/`host_eval_ctx`) — never `xce_worker_t*` (`xce/types.h`, the wire/config type `make_sched_worker()` converts FROM). requirements.md/design.md briefly had this wrong; fixed in commit `d1a3e158`. Task 2.1 (`host_iter.c`'s real implementation) and any later task touching worker candidates must use `xce_sched_worker_t*` from the start.
|
||||
- (1.2) `resreq_fp`'s ingest-time population lives in `core_build_primary_schedobj()` (`src/scheduler/src/core_events.c`), right after `primary->resreq = resreq;` — confirmed wired (min_tasks/max_tasks/select_node_id/rusage_hash all set from the parsed `xce_primary_resreq_t`) as of task 1.2 round 2, regression-covered by `test_core_build_primary_schedobj_populates_resreq_fp_from_parsed_resreq` in `src/scheduler/testsuite/test_core_events.c`. Round 1 added the type/field/hash machinery but never wired this call site, leaving `resreq_fp` permanently zeroed — do not rediscover this gap in a later task.
|
||||
|
|
|
|||
|
|
@ -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