feat(resreq-maxtasks-wiring): add task-count derivation helper
Adds apply_job_task_count() to overwrite a standalone primary resreq's minTasks/maxTasks with a job's real requested task count, no-op for COMPOUND/ALTER nested primaries, with a defensive min>max clamp. Closes the gap that lets scheduler-allocation's multi-host allocator ever be reached (bug-1770). Wiring at live ingest call sites follows in subsequent tasks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
parent
ecfb0a3c64
commit
799a71ecb1
10 changed files with 918 additions and 0 deletions
262
src/lib/testsuite/test_resreq_task_count.c
Normal file
262
src/lib/testsuite/test_resreq_task_count.c
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/* test_resreq_task_count.c — resreq-maxtasks-wiring spec, task 1
|
||||
* (TaskCountWiring component).
|
||||
*
|
||||
* Unit tests for apply_job_task_count() (src/lib/resreq.c), the pure
|
||||
* helper that overwrites a standalone primary resreq's minTasks/maxTasks
|
||||
* with the job's real requested task count, per design.md's
|
||||
* TaskCountWiring component. Covers the three cases the task requires:
|
||||
* - a standalone primary resreq receives the applied task count
|
||||
* (Requirements 1.1, 1.2)
|
||||
* - a COMPOUND/ALTER resreq's nested primaries are left untouched
|
||||
* (Requirement 3.3)
|
||||
* - an invalid (minTasks > maxTasks) pair is defensively clamped by
|
||||
* raising maxTasks to minTasks, mirroring convert.go's clamp
|
||||
* direction (Requirement 1.5)
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "resreq.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* ── standalone primary: overwritten with the requested task count ─────── */
|
||||
|
||||
void test_apply_job_task_count_standalone_primary_override(void)
|
||||
{
|
||||
char* resreqString = "rusage[mem=20]";
|
||||
char* errmsg = NULL;
|
||||
int rc = 0;
|
||||
xce_resreq_t resreq;
|
||||
|
||||
memset(&resreq, 0x0, sizeof(resreq));
|
||||
|
||||
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_PRIMARY);
|
||||
|
||||
/* Sanity: parser's own hardcoded default before the helper ever runs. */
|
||||
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->maxTasks);
|
||||
|
||||
apply_job_task_count(&resreq, 4, 6);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(4, resreq.u.primary->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->maxTasks);
|
||||
|
||||
/* Idempotency: calling again with identical inputs must not change
|
||||
* the result (design.md's TaskCountWiring invariant). */
|
||||
apply_job_task_count(&resreq, 4, 6);
|
||||
TEST_ASSERT_EQUAL_INT(4, resreq.u.primary->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->maxTasks);
|
||||
|
||||
kill_resreq(&resreq);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
/* A default/single-task request (Requirement 1.2) must still land on 1/1 --
|
||||
* exercised explicitly rather than merely relying on the parser's own
|
||||
* pre-helper default, since the helper is expected to be applied
|
||||
* unconditionally at every live ingest call site regardless of the
|
||||
* requested count. */
|
||||
void test_apply_job_task_count_default_single_task(void)
|
||||
{
|
||||
char* resreqString = "rusage[mem=20]";
|
||||
char* errmsg = NULL;
|
||||
int rc = 0;
|
||||
xce_resreq_t resreq;
|
||||
|
||||
memset(&resreq, 0x0, sizeof(resreq));
|
||||
|
||||
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_PRIMARY);
|
||||
|
||||
apply_job_task_count(&resreq, 1, 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(1, resreq.u.primary->maxTasks);
|
||||
|
||||
kill_resreq(&resreq);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
/* ── COMPOUND/ALTER: nested primaries left exactly as parsed (no-op) ────── */
|
||||
|
||||
void test_apply_job_task_count_compound_noop(void)
|
||||
{
|
||||
char* resreqString = "2*{rusage[mem=100:duration=20:decay=1]} + "
|
||||
"4*{rusage[mem=80:duration=30:decay=1]}";
|
||||
char* errmsg = NULL;
|
||||
int rc = 0;
|
||||
xce_resreq_t resreq;
|
||||
int count;
|
||||
int before_min[8];
|
||||
int before_max[8];
|
||||
int i;
|
||||
|
||||
memset(&resreq, 0x0, sizeof(resreq));
|
||||
|
||||
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_COMPOUND);
|
||||
|
||||
count = resreq.u.compound->primaries.count;
|
||||
TEST_ASSERT_TRUE(count > 0);
|
||||
TEST_ASSERT_TRUE(count <= 8);
|
||||
|
||||
/* Snapshot whatever parse_resreq() actually produced for each nested
|
||||
* primary -- the "N*{...}" compound syntax sets a nested primary's
|
||||
* minTasks/maxTasks to N (not the bare-rusage default of 1), so the
|
||||
* no-op guarantee is "unchanged from parse", not "equal to 1". */
|
||||
for (i = 0; i < count; i++) {
|
||||
xce_primary_resreq_t* p = (xce_primary_resreq_t*)xlist_get(
|
||||
&(resreq.u.compound->primaries), i);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
before_min[i] = p->minTasks;
|
||||
before_max[i] = p->maxTasks;
|
||||
}
|
||||
|
||||
apply_job_task_count(&resreq, 4, 6);
|
||||
|
||||
/* No-op: tag unchanged, every nested primary left exactly as
|
||||
* parse_resreq() set it. Only the job's own top-level standalone
|
||||
* primary is in this helper's scope (Requirement 3.3). */
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_COMPOUND);
|
||||
for (i = 0; i < count; i++) {
|
||||
xce_primary_resreq_t* p = (xce_primary_resreq_t*)xlist_get(
|
||||
&(resreq.u.compound->primaries), i);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
TEST_ASSERT_EQUAL_INT(before_min[i], p->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(before_max[i], p->maxTasks);
|
||||
}
|
||||
|
||||
kill_resreq(&resreq);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
void test_apply_job_task_count_alter_noop(void)
|
||||
{
|
||||
char* resreqString
|
||||
= "{2*{rusage[mem=100:duration=20:decay=1]} + "
|
||||
"4*{rusage[mem=80:duration=30:decay=1]}} || "
|
||||
"{3*{rusage[mem=150:duration=15:decay=1]} + "
|
||||
"6*{rusage[mem=120:duration=20:decay=1]}} ";
|
||||
char* errmsg = NULL;
|
||||
int rc = 0;
|
||||
xce_resreq_t resreq;
|
||||
int ncompounds;
|
||||
int before_min[8][8];
|
||||
int before_max[8][8];
|
||||
int nprimaries[8];
|
||||
int i;
|
||||
|
||||
memset(&resreq, 0x0, sizeof(resreq));
|
||||
|
||||
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_ALTER);
|
||||
|
||||
ncompounds = resreq.u.alter->compounds.count;
|
||||
TEST_ASSERT_TRUE(ncompounds > 0);
|
||||
TEST_ASSERT_TRUE(ncompounds <= 8);
|
||||
|
||||
/* Snapshot whatever parse_resreq() actually produced for every nested
|
||||
* primary across every compound branch -- the "N*{...}" compound
|
||||
* syntax sets a nested primary's minTasks/maxTasks to N (not the
|
||||
* bare-rusage default of 1), so the no-op guarantee is "unchanged
|
||||
* from parse", not "equal to 1". */
|
||||
for (i = 0; i < ncompounds; i++) {
|
||||
xce_compound_resreq_t* c
|
||||
= (xce_compound_resreq_t*)xlist_get(&(resreq.u.alter->compounds), i);
|
||||
int j;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
nprimaries[i] = c->primaries.count;
|
||||
TEST_ASSERT_TRUE(nprimaries[i] <= 8);
|
||||
|
||||
for (j = 0; j < nprimaries[i]; j++) {
|
||||
xce_primary_resreq_t* p
|
||||
= (xce_primary_resreq_t*)xlist_get(&(c->primaries), j);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
before_min[i][j] = p->minTasks;
|
||||
before_max[i][j] = p->maxTasks;
|
||||
}
|
||||
}
|
||||
|
||||
apply_job_task_count(&resreq, 4, 6);
|
||||
|
||||
/* No-op: tag unchanged, every nested primary left exactly as
|
||||
* parse_resreq() set it (Requirement 3.3). */
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_ALTER);
|
||||
for (i = 0; i < ncompounds; i++) {
|
||||
xce_compound_resreq_t* c
|
||||
= (xce_compound_resreq_t*)xlist_get(&(resreq.u.alter->compounds), i);
|
||||
int j;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
for (j = 0; j < nprimaries[i]; j++) {
|
||||
xce_primary_resreq_t* p
|
||||
= (xce_primary_resreq_t*)xlist_get(&(c->primaries), j);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
TEST_ASSERT_EQUAL_INT(before_min[i][j], p->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(before_max[i][j], p->maxTasks);
|
||||
}
|
||||
}
|
||||
|
||||
kill_resreq(&resreq);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
/* ── defensive clamp: minTasks > maxTasks must not reach dispatch ───────── */
|
||||
|
||||
void test_apply_job_task_count_clamps_min_exceeds_max(void)
|
||||
{
|
||||
char* resreqString = "rusage[mem=20]";
|
||||
char* errmsg = NULL;
|
||||
int rc = 0;
|
||||
xce_resreq_t resreq;
|
||||
|
||||
memset(&resreq, 0x0, sizeof(resreq));
|
||||
|
||||
rc = parse_resreq(&resreq, resreqString, NULL, &errmsg);
|
||||
TEST_ASSERT_TRUE(rc >= 0);
|
||||
TEST_ASSERT_TRUE(resreq.tag == XCE_RESREQ_PRIMARY);
|
||||
|
||||
/* mintasks (6) > maxtasks (4): defensively raise maxtasks to
|
||||
* mintasks, mirroring convert.go's own clamp direction exactly. */
|
||||
apply_job_task_count(&resreq, 6, 4);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->minTasks);
|
||||
TEST_ASSERT_EQUAL_INT(6, resreq.u.primary->maxTasks);
|
||||
|
||||
kill_resreq(&resreq);
|
||||
FREEUP(errmsg);
|
||||
}
|
||||
|
||||
/* ── defensive: NULL resreq must be a safe no-op, never a crash ─────────── */
|
||||
|
||||
void test_apply_job_task_count_null_resreq_is_noop(void)
|
||||
{
|
||||
apply_job_task_count(NULL, 4, 6);
|
||||
}
|
||||
|
||||
void test_resreq_task_count(void)
|
||||
{
|
||||
printf("\n>> resreq task count test\n\n");
|
||||
|
||||
RUN_TEST(test_apply_job_task_count_standalone_primary_override);
|
||||
RUN_TEST(test_apply_job_task_count_default_single_task);
|
||||
RUN_TEST(test_apply_job_task_count_compound_noop);
|
||||
RUN_TEST(test_apply_job_task_count_alter_noop);
|
||||
RUN_TEST(test_apply_job_task_count_clamps_min_exceeds_max);
|
||||
RUN_TEST(test_apply_job_task_count_null_resreq_is_noop);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue