feat(scheduler-snapshot-job-allocation): add dup_job_alloc deep-copy primitive

Task 1.1: new deep-copy for xce_job_alloc_t composed from existing
make_alloc/make_host_alloc/make_job_alloc, explicitly preserving each
allocation's original from/duration rather than accepting make_alloc's
own time(NULL) stamp. TDD: 6 new unit tests (single-host, multi-host,
multi-resource-per-host, shared-resource-pool, independence-after-free,
from/duration fidelity), all passing under ASAN.

Also logs bug-1723 (pre-existing, unrelated leak in two bug-1722 test
fixtures, confirmed via git-stash A/B rebuild to be byte-for-byte
identical before and after this change) -- documentation only, not fixed
here.

Requirements: 1.1, 1.4
This commit is contained in:
Rongsong Shen 2026-08-27 12:07:27 +00:00
parent 1b4737360b
commit 097e33ae34
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
5 changed files with 1023 additions and 418 deletions

View file

@ -1,12 +1,17 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "misc.h"
#include "unity.h"
#include "xce/types.h"
#include "xcealloc.h"
/* generated Cap'n Proto codec — pulls in free_JobAlloc_ptr(), the existing
* NULL-safe free path dup_job_alloc()'s output must remain compatible with. */
#include "controller.capnp.h"
/* Heap buffer overflow: make_job_account() allocates account->accounts with
* room for exactly 1 xce_host_account_t* (xcealloc.c, calloc(1, ...)) but
* its own loop writes account->accounts[i] for i up to jalloc->nhosts-1 --
@ -88,10 +93,375 @@ static void test_make_host_allocs_multi_host_no_overflow(void)
TEST_ASSERT_EQUAL_STRING("host1", hallocs[1]->host);
}
/* ── dup_job_alloc() (scheduler-snapshot-job-allocation task 1.1) ───────
*
* dup_job_alloc() must build a fresh, independent xce_job_alloc_t from a
* source one -- composed entirely from make_alloc()/make_host_alloc()/
* make_job_alloc() -- so a snapshot-restored job's resource allocation can
* be reconstructed without aliasing the snapshot-producing core's own copy
* (Req 1.1, 1.4). The output must remain compatible with the existing,
* NULL-safe free_JobAlloc_ptr() -- no new free path is introduced.
*
* Unity here is built without UNITY_INCLUDE_DOUBLE, so xce_alloc_t.value
* (a double) is compared via float cast with TEST_ASSERT_EQUAL_FLOAT,
* matching the existing convention (scheduler/testsuite/test.c,
* controller/testsuite/test_e2e_capnp.c). */
/* single-host/single-resource copy: the simplest shape. */
static void test_dup_job_alloc_single_host_single_resource(void)
{
xce_alloc_t* a;
xce_host_alloc_t* h;
xce_host_alloc_t** hallocs;
xce_job_alloc_t* src;
xce_job_alloc_t* dup;
a = make_alloc((char*)"slots", (char*)"resv-x", 42, 3.0f, 60);
TEST_ASSERT_NOT_NULL(a);
h = make_host_alloc((char*)"host0", 1, make_allocs(1, a));
TEST_ASSERT_NOT_NULL(h);
hallocs = (xce_host_alloc_t**)calloc(1, sizeof(*hallocs));
hallocs[0] = h;
src = make_job_alloc(7777, 1, hallocs);
TEST_ASSERT_NOT_NULL(src);
dup = dup_job_alloc(src);
TEST_ASSERT_NOT_NULL(dup);
TEST_ASSERT_EQUAL_UINT64(src->jobid, dup->jobid);
TEST_ASSERT_EQUAL_INT(1, dup->nhosts);
TEST_ASSERT_EQUAL_INT(0, dup->nres_shared);
TEST_ASSERT_NULL(dup->allocs_shared);
TEST_ASSERT_EQUAL_STRING("host0", dup->allocs[0]->host);
TEST_ASSERT_EQUAL_INT(1, dup->allocs[0]->nallocs);
TEST_ASSERT_EQUAL_STRING("slots", dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_EQUAL_STRING("resv-x", dup->allocs[0]->allocs[0]->reserve);
TEST_ASSERT_EQUAL_UINT64(42, dup->allocs[0]->allocs[0]->instid);
TEST_ASSERT_EQUAL_FLOAT(3.0f, (float)dup->allocs[0]->allocs[0]->value);
/* independence: no pointer shared with src anywhere in the tree */
TEST_ASSERT_NOT_EQUAL(src, dup);
TEST_ASSERT_NOT_EQUAL(src->allocs, dup->allocs);
TEST_ASSERT_NOT_EQUAL(src->allocs[0], dup->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->host, dup->allocs[0]->host);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs, dup->allocs[0]->allocs);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0], dup->allocs[0]->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0]->resname,
dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0]->reserve,
dup->allocs[0]->allocs[0]->reserve);
free_JobAlloc_ptr(&src);
free_JobAlloc_ptr(&dup);
}
/* multi-host copy: 2 hosts, 1 resource each. */
static void test_dup_job_alloc_multi_host(void)
{
xce_alloc_t* a0;
xce_alloc_t* a1;
xce_host_alloc_t* h0;
xce_host_alloc_t* h1;
xce_host_alloc_t** hallocs;
xce_job_alloc_t* src;
xce_job_alloc_t* dup;
a0 = make_alloc((char*)"slots", NULL, 10, 1.0f, 30);
a1 = make_alloc((char*)"slots", NULL, 20, 2.0f, 45);
TEST_ASSERT_NOT_NULL(a0);
TEST_ASSERT_NOT_NULL(a1);
h0 = make_host_alloc((char*)"host0", 1, make_allocs(1, a0));
h1 = make_host_alloc((char*)"host1", 1, make_allocs(1, a1));
TEST_ASSERT_NOT_NULL(h0);
TEST_ASSERT_NOT_NULL(h1);
hallocs = (xce_host_alloc_t**)calloc(2, sizeof(*hallocs));
hallocs[0] = h0;
hallocs[1] = h1;
src = make_job_alloc(8888, 2, hallocs);
TEST_ASSERT_NOT_NULL(src);
dup = dup_job_alloc(src);
TEST_ASSERT_NOT_NULL(dup);
TEST_ASSERT_EQUAL_INT(2, dup->nhosts);
TEST_ASSERT_EQUAL_STRING("host0", dup->allocs[0]->host);
TEST_ASSERT_EQUAL_STRING("host1", dup->allocs[1]->host);
TEST_ASSERT_EQUAL_UINT64(10, dup->allocs[0]->allocs[0]->instid);
TEST_ASSERT_EQUAL_UINT64(20, dup->allocs[1]->allocs[0]->instid);
TEST_ASSERT_NOT_EQUAL(src->allocs, dup->allocs);
TEST_ASSERT_NOT_EQUAL(src->allocs[0], dup->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[1], dup->allocs[1]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->host, dup->allocs[0]->host);
TEST_ASSERT_NOT_EQUAL(src->allocs[1]->host, dup->allocs[1]->host);
free_JobAlloc_ptr(&src);
free_JobAlloc_ptr(&dup);
}
/* multi-resource-per-host copy: 1 host, 2 resources. */
static void test_dup_job_alloc_multi_resource_per_host(void)
{
xce_alloc_t* a0;
xce_alloc_t* a1;
xce_host_alloc_t* h0;
xce_host_alloc_t** hallocs;
xce_job_alloc_t* src;
xce_job_alloc_t* dup;
a0 = make_alloc((char*)"cpu", NULL, 1, 4.0f, 60);
a1 = make_alloc((char*)"mem", NULL, 2, 8.0f, 60);
TEST_ASSERT_NOT_NULL(a0);
TEST_ASSERT_NOT_NULL(a1);
h0 = make_host_alloc((char*)"host0", 2, make_allocs(2, a0, a1));
TEST_ASSERT_NOT_NULL(h0);
hallocs = (xce_host_alloc_t**)calloc(1, sizeof(*hallocs));
hallocs[0] = h0;
src = make_job_alloc(9999, 1, hallocs);
TEST_ASSERT_NOT_NULL(src);
dup = dup_job_alloc(src);
TEST_ASSERT_NOT_NULL(dup);
TEST_ASSERT_EQUAL_INT(2, dup->allocs[0]->nallocs);
TEST_ASSERT_EQUAL_STRING("cpu", dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_EQUAL_STRING("mem", dup->allocs[0]->allocs[1]->resname);
TEST_ASSERT_EQUAL_FLOAT(4.0f, (float)dup->allocs[0]->allocs[0]->value);
TEST_ASSERT_EQUAL_FLOAT(8.0f, (float)dup->allocs[0]->allocs[1]->value);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs, dup->allocs[0]->allocs);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0], dup->allocs[0]->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[1], dup->allocs[0]->allocs[1]);
free_JobAlloc_ptr(&src);
free_JobAlloc_ptr(&dup);
}
/* shared-resource-pool copy, combined with multi-host + multi-resource-per-
* host, as the comprehensive "byte-for-byte equivalent, shares no pointers"
* proof called out in the task's observable completion criterion. */
static void test_dup_job_alloc_shared_resource_pool_full_equivalence(void)
{
xce_alloc_t* h0r0;
xce_alloc_t* h0r1;
xce_alloc_t* h1r0;
xce_host_alloc_t* h0;
xce_host_alloc_t* h1;
xce_host_alloc_t** hallocs;
xce_alloc_t* s0;
xce_alloc_t* s1;
xce_job_alloc_t* src;
xce_job_alloc_t* dup;
h0r0 = make_alloc((char*)"cpu", (char*)"resv-a", 100, 2.0f, 60);
h0r1 = make_alloc((char*)"mem", (char*)"resv-b", 101, 4.0f, 90);
h1r0 = make_alloc((char*)"cpu", NULL, 200, 8.0f, 120);
TEST_ASSERT_NOT_NULL(h0r0);
TEST_ASSERT_NOT_NULL(h0r1);
TEST_ASSERT_NOT_NULL(h1r0);
h0 = make_host_alloc((char*)"host0", 2, make_allocs(2, h0r0, h0r1));
h1 = make_host_alloc((char*)"host1", 1, make_allocs(1, h1r0));
TEST_ASSERT_NOT_NULL(h0);
TEST_ASSERT_NOT_NULL(h1);
hallocs = (xce_host_alloc_t**)calloc(2, sizeof(*hallocs));
hallocs[0] = h0;
hallocs[1] = h1;
src = make_job_alloc(4242, 2, hallocs);
TEST_ASSERT_NOT_NULL(src);
s0 = make_alloc((char*)"license", NULL, 300, 1.0f, 30);
s1 = make_alloc((char*)"license2", (char*)"resv-c", 301, 3.0f, 45);
TEST_ASSERT_NOT_NULL(s0);
TEST_ASSERT_NOT_NULL(s1);
src->nres_shared = 2;
src->allocs_shared = make_allocs(2, s0, s1);
dup = dup_job_alloc(src);
TEST_ASSERT_NOT_NULL(dup);
/* --- byte-for-byte content equivalence --- */
TEST_ASSERT_EQUAL_UINT64(src->jobid, dup->jobid);
TEST_ASSERT_EQUAL_INT(src->nhosts, dup->nhosts);
TEST_ASSERT_EQUAL_INT(src->nres_shared, dup->nres_shared);
TEST_ASSERT_EQUAL_STRING("host0", dup->allocs[0]->host);
TEST_ASSERT_EQUAL_INT(2, dup->allocs[0]->nallocs);
TEST_ASSERT_EQUAL_STRING("cpu", dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_EQUAL_STRING("resv-a", dup->allocs[0]->allocs[0]->reserve);
TEST_ASSERT_EQUAL_UINT64(100, dup->allocs[0]->allocs[0]->instid);
TEST_ASSERT_EQUAL_FLOAT(2.0f, (float)dup->allocs[0]->allocs[0]->value);
TEST_ASSERT_EQUAL_STRING("mem", dup->allocs[0]->allocs[1]->resname);
TEST_ASSERT_EQUAL_STRING("resv-b", dup->allocs[0]->allocs[1]->reserve);
TEST_ASSERT_EQUAL_UINT64(101, dup->allocs[0]->allocs[1]->instid);
TEST_ASSERT_EQUAL_FLOAT(4.0f, (float)dup->allocs[0]->allocs[1]->value);
TEST_ASSERT_EQUAL_STRING("host1", dup->allocs[1]->host);
TEST_ASSERT_EQUAL_INT(1, dup->allocs[1]->nallocs);
TEST_ASSERT_EQUAL_STRING("cpu", dup->allocs[1]->allocs[0]->resname);
TEST_ASSERT_EQUAL_STRING("", dup->allocs[1]->allocs[0]->reserve);
TEST_ASSERT_EQUAL_UINT64(200, dup->allocs[1]->allocs[0]->instid);
TEST_ASSERT_EQUAL_FLOAT(8.0f, (float)dup->allocs[1]->allocs[0]->value);
TEST_ASSERT_NOT_NULL(dup->allocs_shared);
TEST_ASSERT_EQUAL_STRING("license", dup->allocs_shared[0]->resname);
TEST_ASSERT_EQUAL_UINT64(300, dup->allocs_shared[0]->instid);
TEST_ASSERT_EQUAL_FLOAT(1.0f, (float)dup->allocs_shared[0]->value);
TEST_ASSERT_EQUAL_STRING("license2", dup->allocs_shared[1]->resname);
TEST_ASSERT_EQUAL_STRING("resv-c", dup->allocs_shared[1]->reserve);
TEST_ASSERT_EQUAL_UINT64(301, dup->allocs_shared[1]->instid);
TEST_ASSERT_EQUAL_FLOAT(3.0f, (float)dup->allocs_shared[1]->value);
/* --- no pointer anywhere in the copy is shared with the source --- */
TEST_ASSERT_NOT_EQUAL(src, dup);
TEST_ASSERT_NOT_EQUAL(src->allocs, dup->allocs);
TEST_ASSERT_NOT_EQUAL(src->allocs_shared, dup->allocs_shared);
TEST_ASSERT_NOT_EQUAL(src->allocs[0], dup->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[1], dup->allocs[1]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->host, dup->allocs[0]->host);
TEST_ASSERT_NOT_EQUAL(src->allocs[1]->host, dup->allocs[1]->host);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs, dup->allocs[0]->allocs);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0], dup->allocs[0]->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[1], dup->allocs[0]->allocs[1]);
TEST_ASSERT_NOT_EQUAL(src->allocs[1]->allocs[0], dup->allocs[1]->allocs[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0]->resname,
dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_NOT_EQUAL(src->allocs[0]->allocs[0]->reserve,
dup->allocs[0]->allocs[0]->reserve);
TEST_ASSERT_NOT_EQUAL(src->allocs_shared[0], dup->allocs_shared[0]);
TEST_ASSERT_NOT_EQUAL(src->allocs_shared[1], dup->allocs_shared[1]);
TEST_ASSERT_NOT_EQUAL(src->allocs_shared[0]->resname,
dup->allocs_shared[0]->resname);
TEST_ASSERT_NOT_EQUAL(src->allocs_shared[1]->reserve,
dup->allocs_shared[1]->reserve);
free_JobAlloc_ptr(&src);
free_JobAlloc_ptr(&dup);
}
/* independence proof: mutate the source's strings/values in place, then
* free the source entirely, and confirm the copy is unaffected either way
* -- the direct test of the Req 1.4 "independent of the snapshot-producing
* instance" guarantee. */
static void test_dup_job_alloc_independence_after_mutate_and_free(void)
{
xce_alloc_t* a;
xce_host_alloc_t* h;
xce_host_alloc_t** hallocs;
xce_job_alloc_t* src;
xce_job_alloc_t* dup;
a = make_alloc((char*)"slots", (char*)"resv-x", 55, 5.0f, 70);
TEST_ASSERT_NOT_NULL(a);
h = make_host_alloc((char*)"host0", 1, make_allocs(1, a));
TEST_ASSERT_NOT_NULL(h);
hallocs = (xce_host_alloc_t**)calloc(1, sizeof(*hallocs));
hallocs[0] = h;
src = make_job_alloc(1234, 1, hallocs);
TEST_ASSERT_NOT_NULL(src);
dup = dup_job_alloc(src);
TEST_ASSERT_NOT_NULL(dup);
/* mutate source strings in place (same length -- strdup'd buffers are
* sized exactly, no room to grow) and mutate source numeric fields. */
strcpy(src->allocs[0]->host, "hostX");
strcpy(src->allocs[0]->allocs[0]->resname, "slotX");
src->allocs[0]->allocs[0]->value = 999.0;
src->allocs[0]->allocs[0]->instid = 999;
TEST_ASSERT_EQUAL_STRING("host0", dup->allocs[0]->host);
TEST_ASSERT_EQUAL_STRING("slots", dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_EQUAL_FLOAT(5.0f, (float)dup->allocs[0]->allocs[0]->value);
TEST_ASSERT_EQUAL_UINT64(55, dup->allocs[0]->allocs[0]->instid);
/* now free the source entirely; the copy must remain fully valid */
free_JobAlloc_ptr(&src);
TEST_ASSERT_NULL(src);
TEST_ASSERT_EQUAL_STRING("host0", dup->allocs[0]->host);
TEST_ASSERT_EQUAL_STRING("slots", dup->allocs[0]->allocs[0]->resname);
TEST_ASSERT_EQUAL_STRING("resv-x", dup->allocs[0]->allocs[0]->reserve);
TEST_ASSERT_EQUAL_FLOAT(5.0f, (float)dup->allocs[0]->allocs[0]->value);
TEST_ASSERT_EQUAL_UINT64(55, dup->allocs[0]->allocs[0]->instid);
free_JobAlloc_ptr(&dup);
}
/* fidelity check: make_alloc() itself stamps ->from = time(NULL) internally
* (xcealloc.c) -- a naive copy that only called make_alloc() and stopped
* would silently reset every restored allocation's start-time to copy-time.
* dup_job_alloc() must explicitly carry over the source's original from/
* duration instead. */
static void test_dup_job_alloc_from_duration_fidelity(void)
{
xce_alloc_t* a;
xce_host_alloc_t* h;
xce_host_alloc_t** hallocs;
xce_job_alloc_t* src;
xce_job_alloc_t* dup;
uint64_t old_from;
uint64_t now;
a = make_alloc((char*)"slots", NULL, 1, 1.0f, 0);
TEST_ASSERT_NOT_NULL(a);
/* overwrite make_alloc()'s own time(NULL) stamp with a fixed value far
* in the past, and a distinctive (negative -- "indefinite", per the
* scheduler.c convention of duration=-1) duration. */
old_from = (uint64_t)1000000;
a->from = old_from;
a->duration = -1;
h = make_host_alloc((char*)"host0", 1, make_allocs(1, a));
TEST_ASSERT_NOT_NULL(h);
hallocs = (xce_host_alloc_t**)calloc(1, sizeof(*hallocs));
hallocs[0] = h;
src = make_job_alloc(1, 1, hallocs);
TEST_ASSERT_NOT_NULL(src);
now = (uint64_t)time(NULL);
dup = dup_job_alloc(src);
TEST_ASSERT_NOT_NULL(dup);
TEST_ASSERT_EQUAL_UINT64(old_from, dup->allocs[0]->allocs[0]->from);
TEST_ASSERT_EQUAL_INT64(-1, dup->allocs[0]->allocs[0]->duration);
/* explicitly confirm the copy was NOT re-stamped to copy-time */
TEST_ASSERT_NOT_EQUAL(now, dup->allocs[0]->allocs[0]->from);
free_JobAlloc_ptr(&src);
free_JobAlloc_ptr(&dup);
}
void test_xcealloc(void)
{
printf("\n>> xcealloc test\n\n");
RUN_TEST(test_make_job_account_multi_host_no_overflow);
RUN_TEST(test_make_host_allocs_multi_host_no_overflow);
RUN_TEST(test_dup_job_alloc_single_host_single_resource);
RUN_TEST(test_dup_job_alloc_multi_host);
RUN_TEST(test_dup_job_alloc_multi_resource_per_host);
RUN_TEST(test_dup_job_alloc_shared_resource_pool_full_equivalence);
RUN_TEST(test_dup_job_alloc_independence_after_mutate_and_free);
RUN_TEST(test_dup_job_alloc_from_duration_fidelity);
}