xce_scheduler --sim started with no shared resource pool when the shared values came only from the recording's SnapshotEntry (license value=(none)), so jobs needing a shared resource pended instead of dispatching. Two defects caused it: - Load side (snapshot_load.c): a resval was treated as shared only when rv->host == NULL. The capnp round trip (write_snapshot_entry -> decode_SnapshotEntry) reads a NULL host back as "", so every shared resval in a .replay file was skipped. A NULL or empty host now means the shared pool; only a named host marks per-worker data. - Save side (snapshot.c, snapshot_wire.c): core->sharedInst and each worker's resvals were walked with i < .count. zarray_t.count is the number of filled slots, not the highest idx + 1, and both arrays are sparse by resource idx, so higher-index values were dropped from the snapshot. Both loops are now bounded by .caps, as repl_info.c already is. New test test_snapshot_sparse_resvals.c covers the in-memory save/load, the empty-host load, and the real sched_record_start() -> .replay -> sched_sim_driver_start() round trip. Regenerated fixtures/repl_trace_baseline/set_output.replay: its mid-run snapshot now carries license, limit=cluster_slots and each worker's limit=host_slots, which the old writer dropped (all other fixtures unchanged). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
401 lines
14 KiB
C
401 lines
14 KiB
C
/*
|
|
* test_snapshot_sparse_resvals.c - bug-1923: resource values in a snapshot
|
|
* must survive save and load, both in memory and through the real `.replay`
|
|
* encode/decode seam that `xce_scheduler --sim` reads.
|
|
*
|
|
* Two defects are covered:
|
|
*
|
|
* 1. Save side: xce_sched_core_save_snapshot() (snapshot.c) and
|
|
* build_wire_worker() (snapshot_wire.c) walked core->sharedInst and
|
|
* w->resvals with `i < .count`. zarray_t.count is the number of filled
|
|
* slots, not the highest index plus one, so a sparse zarray (a value
|
|
* at idx 2 with idx 0/1 empty, or a worker whose idx 2 belongs to a
|
|
* global resource it never reports) lost its higher-index values.
|
|
*
|
|
* 2. Load side: xce_sched_core_load_snapshot() (snapshot_load.c) kept a
|
|
* shared resval only when rv->host == NULL. The capnp round trip
|
|
* decodes a host written as NULL as "", so every shared resval read
|
|
* back from a `.replay` file was skipped.
|
|
*
|
|
* Resource layout used throughout (makeResource() assigns idx in
|
|
* registration order): slots=0, mem=1, license=2 (GLOBAL|SHARED, cluster
|
|
* pool only), gpu=3 (per host). Only the license has a shared value, so
|
|
* sharedInst holds one value at idx 2 with count 1. Worker w01 reports
|
|
* slots, mem and gpu, so its resvals have a hole at idx 2 and count 3.
|
|
*
|
|
* Link style: the real scheduler_srcs list (minus main.c), same as
|
|
* test_sim_e2e_limit_visibility.c, so the round-trip test runs the real
|
|
* sched_record_start() writer and the real sched_sim_driver_start() reader.
|
|
*/
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "controller.capnp.h"
|
|
#include "recording.h"
|
|
#include "resobj.h"
|
|
#include "scheduler.h"
|
|
#include "sim.h"
|
|
#include "xstring.h"
|
|
|
|
#define LICENSE_TOTAL 2.0f
|
|
#define GPU_TOTAL 8.0f
|
|
|
|
void setUp(void) { return; }
|
|
void tearDown(void) { return; }
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Fixture helpers */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static xce_resource_t* bake_resource(var resmgr, const char* name, int flags)
|
|
{
|
|
xce_resource_t res;
|
|
xce_resource_t* r;
|
|
|
|
memset(&res, 0, sizeof(res));
|
|
res.name = strdup(name);
|
|
res.valtype = XCE_VAL_NUMERIC;
|
|
res.flags = flags;
|
|
|
|
r = makeResource(resmgr, &res);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
return r;
|
|
}
|
|
|
|
static xce_val_t* make_fval(float v)
|
|
{
|
|
xce_val_t* val = (xce_val_t*)calloc(1, sizeof(*val));
|
|
|
|
TEST_ASSERT_NOT_NULL(val);
|
|
val->which = Value_u_fvalue;
|
|
val->u.fvalue = v;
|
|
return val;
|
|
}
|
|
|
|
static xce_name_val_t* make_nv(const char* name, float v)
|
|
{
|
|
xce_name_val_t* nv = (xce_name_val_t*)calloc(1, sizeof(*nv));
|
|
|
|
TEST_ASSERT_NOT_NULL(nv);
|
|
nv->name = strdup(name);
|
|
nv->val = make_fval(v);
|
|
return nv;
|
|
}
|
|
|
|
static xce_resource_t* lookup_resource(xce_sched_core_t* core,
|
|
const char* name)
|
|
{
|
|
xce_resource_t* r = NULL;
|
|
|
|
TEST_ASSERT_TRUE(key_find(core->resmgr, (void*)name, (void**)&r) >= 0);
|
|
TEST_ASSERT_NOT_NULL(r);
|
|
return r;
|
|
}
|
|
|
|
/*
|
|
* populate_core - registers slots/mem/license/gpu (idx 0..3), puts the
|
|
* license's cluster value into sharedInst at idx 2 only, and pushes worker
|
|
* w01 reporting slots, mem and gpu (no license: it is a cluster resource).
|
|
* Asserts the sparse shape the bug depends on.
|
|
*/
|
|
static void populate_core(xce_sched_core_t* core)
|
|
{
|
|
xce_resource_t* lic;
|
|
xce_resource_t* gpu;
|
|
xce_worker_t wire_worker;
|
|
xce_worker_t* workers[1];
|
|
xce_name_val_t* nvs[3];
|
|
xce_core_event_t ev;
|
|
xce_sched_worker_t* w;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, bake_resource(core->resmgr, "slots",
|
|
XCE_RESF_CONSUMABLE)->idx);
|
|
TEST_ASSERT_EQUAL_INT(1, bake_resource(core->resmgr, "mem",
|
|
XCE_RESF_CONSUMABLE)->idx);
|
|
lic = bake_resource(core->resmgr, "license",
|
|
XCE_RESF_GLOBAL | XCE_RESF_SHARED
|
|
| XCE_RESF_CONSUMABLE);
|
|
TEST_ASSERT_EQUAL_INT(2, lic->idx);
|
|
gpu = bake_resource(core->resmgr, "gpu", XCE_RESF_CONSUMABLE);
|
|
TEST_ASSERT_EQUAL_INT(3, gpu->idx);
|
|
|
|
zarray_put(&(core->sharedInst), lic->idx, make_fval(LICENSE_TOTAL));
|
|
TEST_ASSERT_EQUAL_INT(1, core->sharedInst.count);
|
|
|
|
nvs[0] = make_nv("slots", 4.0f);
|
|
nvs[1] = make_nv("mem", 16384.0f);
|
|
nvs[2] = make_nv("gpu", GPU_TOTAL);
|
|
|
|
memset(&wire_worker, 0, sizeof(wire_worker));
|
|
wire_worker.name = (char*)"w01";
|
|
wire_worker.status = 0;
|
|
wire_worker.nresvals_to_sched = 3;
|
|
wire_worker.resvals_to_sched = nvs;
|
|
workers[0] = &wire_worker;
|
|
|
|
memset(&ev, 0, sizeof(ev));
|
|
ev.type = XCE_CORE_EVENT_WORKERS_PUSH;
|
|
ev.u.workers_push.nworkers = 1;
|
|
ev.u.workers_push.workers = workers;
|
|
TEST_ASSERT_EQUAL_INT(0, xce_core_push_events(core, &ev, 1));
|
|
|
|
w = sched_get_worker(core, "w01");
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
TEST_ASSERT_NOT_NULL(zarray_get(&(w->resvals), gpu->idx));
|
|
TEST_ASSERT_NULL(zarray_get(&(w->resvals), lic->idx));
|
|
TEST_ASSERT_EQUAL_INT(3, w->resvals.count);
|
|
}
|
|
|
|
/* The license value is in `core`'s shared pool, at the license's idx. */
|
|
static void assert_license_loaded(xce_sched_core_t* core)
|
|
{
|
|
xce_resource_t* lic = lookup_resource(core, "license");
|
|
xce_val_t* v = (xce_val_t*)zarray_get(&(core->sharedInst),
|
|
lic->idx);
|
|
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(v, "license value missing from sharedInst");
|
|
TEST_ASSERT_EQUAL_FLOAT(LICENSE_TOTAL, v->u.fvalue);
|
|
}
|
|
|
|
/* Worker w01's gpu value is in its per-worker resvals. */
|
|
static void assert_worker_gpu_loaded(xce_sched_core_t* core)
|
|
{
|
|
xce_resource_t* gpu = lookup_resource(core, "gpu");
|
|
xce_sched_worker_t* w = sched_get_worker(core, "w01");
|
|
xce_val_t* v;
|
|
|
|
TEST_ASSERT_NOT_NULL(w);
|
|
v = (xce_val_t*)zarray_get(&(w->resvals), gpu->idx);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(v, "w01 gpu value missing from resvals");
|
|
TEST_ASSERT_EQUAL_FLOAT(GPU_TOTAL, v->u.fvalue);
|
|
}
|
|
|
|
static xce_resval_t* find_snapshot_resval(sched_snapshot_data_t* snap,
|
|
const char* name)
|
|
{
|
|
int i;
|
|
|
|
if (snap->resvals == NULL) {
|
|
return NULL;
|
|
}
|
|
for (i = 0; i < snap->resvals->count; i++) {
|
|
xce_resval_t* rv = snap->resvals->vals[i];
|
|
|
|
if ((rv != NULL) && (rv->name != NULL)
|
|
&& (strcmp(rv->name, name) == 0)) {
|
|
return rv;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* (a) in-memory save -> load */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static void test_save_keeps_sparse_shared_resval(void)
|
|
{
|
|
xce_sched_core_t core;
|
|
sched_snapshot_data_t snap;
|
|
xce_resval_t* rv;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core));
|
|
populate_core(&core);
|
|
|
|
memset(&snap, 0, sizeof(snap));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_save_snapshot(&core, &snap));
|
|
|
|
rv = find_snapshot_resval(&snap, "license");
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(rv, "snapshot dropped the license value "
|
|
"at sharedInst idx 2 (count 1)");
|
|
TEST_ASSERT_NULL(rv->host);
|
|
TEST_ASSERT_EQUAL_FLOAT(LICENSE_TOTAL, rv->val->u.fvalue);
|
|
}
|
|
|
|
static void test_save_keeps_sparse_worker_resval(void)
|
|
{
|
|
xce_sched_core_t core;
|
|
sched_snapshot_data_t snap;
|
|
xce_worker_t* ww = NULL;
|
|
int found = 0;
|
|
int i;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core));
|
|
populate_core(&core);
|
|
|
|
memset(&snap, 0, sizeof(snap));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_save_snapshot(&core, &snap));
|
|
|
|
TEST_ASSERT_NOT_NULL(snap.workers);
|
|
TEST_ASSERT_EQUAL_INT(1, snap.workers->count);
|
|
ww = snap.workers->workers[0];
|
|
TEST_ASSERT_NOT_NULL(ww);
|
|
TEST_ASSERT_EQUAL_INT(3, ww->nresvals_to_sched);
|
|
|
|
for (i = 0; i < ww->nresvals_to_sched; i++) {
|
|
xce_name_val_t* nv = ww->resvals_to_sched[i];
|
|
|
|
if ((nv != NULL) && (strcmp(nv->name, "gpu") == 0)) {
|
|
TEST_ASSERT_EQUAL_FLOAT(GPU_TOTAL, nv->val->u.fvalue);
|
|
found = 1;
|
|
}
|
|
}
|
|
TEST_ASSERT_TRUE_MESSAGE(found, "snapshot dropped w01's gpu value at "
|
|
"resvals idx 3 (count 3)");
|
|
}
|
|
|
|
static void test_save_load_round_trip_keeps_sparse_resvals(void)
|
|
{
|
|
xce_sched_core_t core_a;
|
|
xce_sched_core_t core_b;
|
|
sched_snapshot_data_t snap;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core_a));
|
|
populate_core(&core_a);
|
|
|
|
memset(&snap, 0, sizeof(snap));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_save_snapshot(&core_a, &snap));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core_b));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_load_snapshot(&core_b, &snap));
|
|
|
|
assert_license_loaded(&core_b);
|
|
assert_worker_gpu_loaded(&core_b);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* (b) load: an empty host (as capnp decodes NULL) is a shared-pool entry */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static void test_load_takes_shared_resval_with_empty_host(void)
|
|
{
|
|
xce_sched_core_t core_a;
|
|
xce_sched_core_t core_b;
|
|
sched_snapshot_data_t snap;
|
|
xce_resval_t* rv;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core_a));
|
|
populate_core(&core_a);
|
|
|
|
memset(&snap, 0, sizeof(snap));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_save_snapshot(&core_a, &snap));
|
|
|
|
/* Decoded form, independent of the save-side fix: one license entry
|
|
* whose host is "" (what decode_SnapshotEntry() yields for a NULL
|
|
* host). */
|
|
rv = (xce_resval_t*)calloc(1, sizeof(*rv));
|
|
TEST_ASSERT_NOT_NULL(rv);
|
|
rv->name = strdup("license");
|
|
rv->host = strdup("");
|
|
rv->val = make_fval(LICENSE_TOTAL);
|
|
snap.resvals->vals = (xce_resval_t**)calloc(1, sizeof(rv));
|
|
TEST_ASSERT_NOT_NULL(snap.resvals->vals);
|
|
snap.resvals->vals[0] = rv;
|
|
snap.resvals->count = 1;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core_b));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_load_snapshot(&core_b, &snap));
|
|
|
|
assert_license_loaded(&core_b);
|
|
}
|
|
|
|
/* A resval that names a host is per-worker data, never the shared pool:
|
|
* the empty-host rule must not widen the load to it. */
|
|
static void test_load_skips_resval_with_named_host(void)
|
|
{
|
|
xce_sched_core_t core_a;
|
|
xce_sched_core_t core_b;
|
|
sched_snapshot_data_t snap;
|
|
xce_resval_t* rv;
|
|
xce_resource_t* lic;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core_a));
|
|
populate_core(&core_a);
|
|
|
|
memset(&snap, 0, sizeof(snap));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_save_snapshot(&core_a, &snap));
|
|
|
|
rv = (xce_resval_t*)calloc(1, sizeof(*rv));
|
|
TEST_ASSERT_NOT_NULL(rv);
|
|
rv->name = strdup("license");
|
|
rv->host = strdup("w01");
|
|
rv->val = make_fval(LICENSE_TOTAL);
|
|
snap.resvals->vals = (xce_resval_t**)calloc(1, sizeof(rv));
|
|
TEST_ASSERT_NOT_NULL(snap.resvals->vals);
|
|
snap.resvals->vals[0] = rv;
|
|
snap.resvals->count = 1;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&core_b));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_load_snapshot(&core_b, &snap));
|
|
|
|
lic = lookup_resource(&core_b, "license");
|
|
TEST_ASSERT_NULL(zarray_get(&(core_b.sharedInst), lic->idx));
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* (c) real round trip: sched_record_start() -> .replay -> */
|
|
/* sched_sim_driver_start() */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static void test_replay_round_trip_keeps_sparse_resvals(void)
|
|
{
|
|
char tmpl[] = "/tmp/sched_sparseresvals_XXXXXX";
|
|
char path[PATH_MAX];
|
|
xce_scheduler_t sched;
|
|
xce_sched_plugin_t* fcfs;
|
|
sched_sim_args_t args;
|
|
xce_sched_sim_driver_t sim;
|
|
|
|
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
|
|
|
memset(&sched, 0, sizeof(sched));
|
|
mylock_init(&(sched.record_lock));
|
|
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
|
|
fcfs = builtin_fcfs_new_plugin();
|
|
TEST_ASSERT_NOT_NULL(fcfs);
|
|
TEST_ASSERT_TRUE(plugin_register_core(&(sched.core), fcfs) >= 0);
|
|
sched.core.job_pick_plugin = (char*)"builtin_fcfs";
|
|
sched.record_dir = tmpl;
|
|
|
|
populate_core(&(sched.core));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, "sparse.replay"));
|
|
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(path));
|
|
strcpy(path, sched.record_file_path);
|
|
TEST_ASSERT_EQUAL_INT(0, sched_record_stop(&sched));
|
|
|
|
memset(&args, 0, sizeof(args));
|
|
args.replay_path = path;
|
|
args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
|
|
memset(&sim, 0, sizeof(sim));
|
|
TEST_ASSERT_EQUAL_INT(0, sched_sim_driver_start(&sim, &args));
|
|
|
|
assert_license_loaded(&(sim.core));
|
|
assert_worker_gpu_loaded(&(sim.core));
|
|
|
|
if (sim.replay_file != NULL) {
|
|
fclose(sim.replay_file);
|
|
sim.replay_file = NULL;
|
|
}
|
|
unlink(path);
|
|
rmdir(tmpl);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_save_keeps_sparse_shared_resval);
|
|
RUN_TEST(test_save_keeps_sparse_worker_resval);
|
|
RUN_TEST(test_save_load_round_trip_keeps_sparse_resvals);
|
|
RUN_TEST(test_load_takes_shared_resval_with_empty_host);
|
|
RUN_TEST(test_load_skips_resval_with_named_host);
|
|
RUN_TEST(test_replay_round_trip_keeps_sparse_resvals);
|
|
return UNITY_END();
|
|
}
|