myxce/src/scheduler/testsuite/test_limit_host_dedup.c
Rongsong Shen ac033f4783
fix(scheduler): record config pushes, free hlimits lists, quiet benign picker WARN (bug-1847/1848/1849)
Follow-ups from the bug-1846 investigation.

bug-1847: config pushes never reached a .replay recording -- handler.c
applied them synchronously, bypassing the pending-update queue the
recorder writes from. With bug-1846's deferral they're now applied at a
round boundary on the driver thread, so sched_apply_pending_config()
writes a CONFIG_PUSH event via the existing sched_record_write_events()
(no-op when not recording), ahead of the round's other recorded events.
Test: test_config_push_deferred.c's test_applied_config_push_is_recorded
(RED: 0 events; GREEN: 1). Fidelity limit, pre-existing: replay applies
configPush through core_events.c's CONFIG_PUSH path, which doesn't re-run
groupmgr_load_configs()/plugin_config_update(), so a limits/groups change
still won't take effect in sim.

bug-1848: xce_sched_obj_t.hlimits was created with a NULL zarray killer
at all three constructors, so reset_sched_obj() and schedobj teardown
leaked the per-worker xlist_t match_job_host_limit_() callocs. New
kill_hlimits_list() (schedobj.c) frees the list, never its vres entries
(owned by the limit plugin); core_events.c and snapshot_load.c use
identical static local copies, since both avoid sharing helpers with
schedobj.c and several test binaries link them without it. Test:
test_limit_host_dedup.c's test_hlimits_zarray_releases_its_lists (RED:
killer NULL; GREEN).

bug-1849: every --sim run warned "failed to resolve job picker plugin ...
picker rebuild will be skipped". Benign: the snapshot is loaded before
the sim registers its picker, and sim_bootstrap_picker() replays the same
queue/job walk afterward. Downgraded to TRACE with an explanation.

Buglog also records bug-1850 (open): the ~100s reconnect churn behind
bug-1846 part 2 -- xcecli_retry_next() rotates/reconnects on every retry
(ETIMEDOUT after the 10s reply deadline, E_XCE_NOT_READY, redirects), so
a slow reply from a healthy primary forces a rejoin + full re-push.
Needs the live scheduler.log and a libxce spec.

Full scheduler suite 87/87 (asan). Replay of the captured file: same 184
mismatches, all attributable to the recording's pre-fix binary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 07:00:07 +00:00

289 lines
9.7 KiB
C

/*
* test_limit_host_dedup.c - bug-1846 regression: a host-scoped limit must
* be staged ONCE per (job, worker), no matter how many allocation attempts
* that job makes on that worker.
*
* limit_prepare_allocation() -> match_job_host_limit_() (plugins/limit.c)
* runs on EVERY per-host allocation attempt (plugin_prepare_allocation(),
* called by resource.c's sched_try_alloc_on_host_()), and used to
* xlist_store() the matched vres into obj->hlimits[worker->idx]
* unconditionally. obj->hlimits is only rebuilt when limit_job_election()
* sees a config-version bump, so a job that pended on the same host round
* after round accumulated one extra copy of the limit per attempt.
* plugin_prepare_allocation() then staged every copy into
* ctx->alloc_req.host via sched_fill_limit_requests(), and seqjob's
* make_host_alloc() built a host allocation carrying all of them -- live
* captured in scheduler-replay-20260923-085356.replay as a DISPATCH whose
* pc13v host alloc held mem + slots + 70 identical limit entries (72)
* where the recording held 3.
*
* This fixture calls plugin_prepare_allocation() repeatedly for one job on
* one worker -- exactly what successive pending rounds do -- and requires
* the host-limit list and the staged host request to each hold the limit
* exactly once.
*/
#include <errno.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "plugin.h"
#include "resobj.h"
#include "sched/types.h"
#include "scheduler.h"
#include "xcelimit.h"
#include "xstring.h"
#define WORKER_NAME "hdedup_worker"
#define QUEUE_NAME "normal"
#define TEST_RESREQ ((char*)"select[type==any] rusage[mem=1]")
#define LIMIT_RESNAME "hdedup_host_res"
#define LIMIT_NAME "hdedup_host_limit"
#define JOB_ID ((uint64_t)518461ULL)
#define NATTEMPTS (5)
void setUp(void) { return; }
void tearDown(void) { return; }
static void register_slots_resource(var resmgr)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup("slots");
res.valtype = XCE_VAL_NUMERIC;
res.ordtype = XCE_ORD_DECR;
res.flags = XCE_RESF_LIMIT | XCE_RESF_CONSUMABLE | XCE_RESF_RELEASE
| XCE_RESF_DEFAULT_HOST;
TEST_ASSERT_NOT_NULL(makeResource(resmgr, &res));
}
/* same bare-resource shape test_sim_contention_limit_driver_parity.c uses:
* is_host_based_limit() resolves LIMIT_NAME as host-based from
* limit->val.percent != 0, not from any flag on this resource. */
static void register_plain_resource(var resmgr, const char* name)
{
xce_resource_t res;
memset(&res, 0, sizeof(res));
res.name = strdup(name);
TEST_ASSERT_NOT_NULL(makeResource(resmgr, &res));
}
static void add_worker_resval(xce_sched_worker_t* worker,
xce_sched_core_t* core, const char* resname,
double value)
{
xce_resource_t* r = NULL;
xce_val_t* val = NULL;
int rc;
rc = key_find(core->resmgr, (void*)resname, (void**)&r);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(r);
val = (xce_val_t*)calloc(1, sizeof(*val));
TEST_ASSERT_NOT_NULL(val);
val->which = Value_u_fvalue;
val->u.fvalue = value;
zarray_put(&(worker->resvals), r->idx, val);
}
static xce_sched_worker_t* make_worker(xce_sched_core_t* core)
{
xce_sched_worker_t* w = (xce_sched_worker_t*)calloc(1, sizeof(*w));
TEST_ASSERT_NOT_NULL(w);
w->name = strdup(WORKER_NAME);
w->status = 0;
w->idx = -1;
zarray_init(&(w->resvals), 128, (zarray_item_killer_t)kill_sched_resval);
zarray_init(&(w->resInUse), 128, (zarray_item_killer_t)kill_sched_resval);
add_worker_resval(w, core, "slots", 4.0);
add_worker_resval(w, core, LIMIT_RESNAME, 1000.0);
return w;
}
static xce_sched_job_t* make_job(xce_scheduler_t* sched)
{
job_submit_data_t submit;
xce_job_t j;
xce_sched_job_t* job;
int rc;
memset(&submit, 0, sizeof(submit));
submit.queue = (char*)QUEUE_NAME;
submit.user = (char*)"hdedup_user";
submit.project_name = (char*)"default";
submit.resreq = TEST_RESREQ;
submit.mintasks = 1;
submit.maxtasks = 1;
memset(&j, 0, sizeof(j));
j.jobid = JOB_ID;
j.state = JOB_STAT_PEND;
j.sub_time = 1000ULL;
j.queue_time = 1000ULL;
j.submit = &submit;
job = make_sched_job(sched, &j);
TEST_ASSERT_NOT_NULL(job);
TEST_ASSERT_EQUAL_INT(XCE_RESREQ_PRIMARY, job->schedobj->tag);
rc = sched_add_job(&(sched->core), job);
TEST_ASSERT_TRUE(rc >= 0);
return job;
}
static int count_staged(const xvec_idval_t* v, int idx)
{
int i;
int n = 0;
for (i = 0; i < v->count; i++) {
int residx = -1;
xvec_get((xvec_t*)&(v->idxs), i, &residx);
if (residx == idx) {
n++;
}
}
return n;
}
static xce_scheduler_t g_sched;
static void test_repeated_host_attempts_stage_host_limit_once(void)
{
xce_limit_val_t limit_val = { .resname = (char*)LIMIT_RESNAME,
.percent = 1,
.val = 50.0 };
xce_limit_config_t limit_cfg = { .name = (char*)LIMIT_NAME,
.val = &limit_val };
xce_limit_config_t* limits[1] = { &limit_cfg };
xce_conf4sched_t config;
xce_sched_plugin_t* limit_plugin = NULL;
xce_sched_queue_t* queue;
xce_sched_worker_t* w;
xce_sched_job_t* job;
xce_sched_ctx_t ctx;
xlist_t* hlimits;
xce_vres_t* vres;
int vres_idx;
int attempt;
int rc;
memset(&config, 0, sizeof(config));
config.nlimits = 1;
config.limits = limits;
memset(&g_sched, 0, sizeof(g_sched));
rc = xce_sched_core_init(&(g_sched.core));
TEST_ASSERT_EQUAL_INT(0, rc);
register_slots_resource(g_sched.core.resmgr);
register_plain_resource(g_sched.core.resmgr, LIMIT_RESNAME);
limit_plugin = limit_new_plugin();
TEST_ASSERT_NOT_NULL(limit_plugin);
rc = plugin_register(&g_sched, limit_plugin);
TEST_ASSERT_TRUE(rc >= 0);
rc = limit_seed_config(limit_plugin, &(g_sched.core), &config);
TEST_ASSERT_TRUE(rc >= 0);
queue = (xce_sched_queue_t*)calloc(1, sizeof(*queue));
TEST_ASSERT_NOT_NULL(queue);
queue->name = makeString(QUEUE_NAME);
rc = sched_add_queue(&(g_sched.core), queue);
TEST_ASSERT_TRUE(rc >= 0);
w = make_worker(&(g_sched.core));
rc = sched_add_worker(&(g_sched.core), w);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_TRUE(w->idx >= 0);
job = make_job(&g_sched);
/* One allocation attempt per simulated pending round, all on the same
* worker, with no config change in between (so limit_job_election()
* never resets obj->hlimits). */
for (attempt = 0; attempt < NATTEMPTS; attempt++) {
rc = sched_ctx_init(&ctx, &(g_sched.core), job->schedobj);
TEST_ASSERT_EQUAL_INT(0, rc);
ctx.worker = w;
ctx.nhosts = 1;
ctx.nslots = 1;
rc = plugin_prepare_allocation(&(g_sched.core), &ctx);
TEST_ASSERT_TRUE(rc >= 0);
hlimits = (xlist_t*)zarray_get(&(job->schedobj->hlimits), w->idx);
TEST_ASSERT_NOT_NULL_MESSAGE(
hlimits, "the host-scoped limit must match on this worker");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, hlimits->count,
"obj->hlimits[worker] must hold the matched limit exactly once, "
"however many attempts the job has made on this worker");
vres = (xce_vres_t*)xlist_get(hlimits, 0);
TEST_ASSERT_NOT_NULL(vres);
vres_idx = vres->vres->idx;
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, count_staged(&(ctx.alloc_req.host), vres_idx),
"alloc_req.host must stage the host limit's vres exactly once "
"-- every extra copy becomes an extra entry in the DISPATCH's "
"host allocation");
sched_ctx_fini(&ctx);
}
printf("test_repeated_host_attempts_stage_host_limit_once: PASS\n");
}
/* reset_sched_obj() (limit_job_election()'s config-version rebuild) and
* schedobj teardown release obj->hlimits through zarray_fini(), which only
* frees items when the zarray has an item killer. The per-worker entries
* are xlist_t's calloc'd by match_job_host_limit_(), and the zarray used to
* be created with a NULL killer, so every rebuild/teardown leaked one list
* per worker the job had tried. (The vres entries inside each list are
* owned by the limit plugin's instance table and must not be freed.) A
* direct LeakSanitizer check isn't usable here -- the fixture's own resreq
* parsing leaks independently -- so this pins the construction contract. */
static void test_hlimits_zarray_releases_its_lists(void)
{
xce_sched_job_t* job = sched_get_job(&(g_sched.core), JOB_ID);
TEST_ASSERT_NOT_NULL_MESSAGE(job, "runs after the dedup test's fixture");
TEST_ASSERT_EQUAL_PTR_MESSAGE(
(void*)kill_hlimits_list, (void*)job->schedobj->hlimits.killer,
"obj->hlimits must be created with kill_hlimits_list so "
"reset_sched_obj()/teardown free the per-worker lists");
/* and a reset must leave it reusable with the same killer */
reset_sched_obj(job->schedobj);
TEST_ASSERT_EQUAL_PTR((void*)kill_hlimits_list,
(void*)job->schedobj->hlimits.killer);
TEST_ASSERT_NULL(zarray_get(&(job->schedobj->hlimits), 0));
printf("test_hlimits_zarray_releases_its_lists: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_repeated_host_attempts_stage_host_limit_once);
RUN_TEST(test_hlimits_zarray_releases_its_lists);
return UNITY_END();
}