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>
289 lines
9.7 KiB
C
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();
|
|
}
|