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:
parent
1b4737360b
commit
097e33ae34
5 changed files with 1023 additions and 418 deletions
91
.kiro/specs/scheduler-snapshot-job-allocation/tasks.md
Normal file
91
.kiro/specs/scheduler-snapshot-job-allocation/tasks.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Implementation Plan
|
||||
|
||||
- [ ] 1. Core: independent snapshot-reconstruction primitives
|
||||
- [x] 1.1 (P) Build a deep-copy primitive for a job's resource allocation
|
||||
- Write failing unit tests first (TDD red): single-host/single-resource copy, multi-host copy, multi-resource-per-host copy, shared-resource-pool copy, an independence proof (mutate or free the source after copying and confirm the copy is unaffected), and a fidelity check that the copy's allocation start-time/duration match the source exactly rather than being reset to copy-time.
|
||||
- Implement the primitive by composing the existing allocation-construction helpers, explicitly carrying over the source's original start-time/duration rather than accepting a fresh default.
|
||||
- On an allocation failure partway through, return no result rather than a partial/inconsistent one, matching this area's existing convention (no forced full-rollback machinery).
|
||||
- Observable completion: the new unit test suite goes from failing to passing, and a copy produced from a multi-host, multi-resource, shared-pool source is byte-for-byte equivalent in content to the source while sharing no pointers with it.
|
||||
- _Requirements: 1.1, 1.4_
|
||||
- _Boundary: xcealloc (job allocation deep-copy)_
|
||||
|
||||
- [ ] 1.2 (P) Construct a valid scheduling representation for every job restored from a snapshot
|
||||
- Write a failing test first (TDD red) asserting that a job reconstructed from a snapshot has a non-NULL, correctly-shaped internal scheduling representation for a primary, a compound, and an alternative resource request.
|
||||
- Implement construction using only information already available by the time a restored job's resource request has been parsed (its queue, user, project, and resource request) — no new snapshot data is captured or required.
|
||||
- Skip construction (leaving today's behavior unchanged) when a restored job's resource request itself failed to parse — do not attempt to fix that separate, pre-existing failure path as part of this task.
|
||||
- Observable completion: the new test goes from failing to passing, and a restored job's scheduling representation is structurally identical to what the same job would get if it had just been freshly submitted with the same queue/user/project/resource request.
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
||||
- _Boundary: snapshot load (scheduling-representation construction)_
|
||||
|
||||
- [ ] 2. Core: wire the allocation restoration into the save and load paths
|
||||
- [ ] 2.1 Populate a job's resource allocation on the save side
|
||||
- Write a failing test first (TDD red) asserting that saving a running job with a resource allocation produces a saved representation carrying that allocation.
|
||||
- Populate the already-existing allocation slot on the saved job representation using the deep-copy primitive from 1.1 whenever the live job has an allocation; leave it absent otherwise, matching how every other optional value here is already handled.
|
||||
- Observable completion: the new test goes from failing to passing, and saving a job with a multi-host allocation produces a saved job whose allocation content matches the live job's.
|
||||
- _Requirements: 1.1_
|
||||
- _Depends: 1.1_
|
||||
- _Boundary: snapshot save (allocation population)_
|
||||
- _Note: not marked parallel with 2.2 — both tasks' tests land in the same test file (test_snapshot_save_load.c), a shared-file conflict even though the production code they touch is disjoint._
|
||||
|
||||
- [ ] 2.2 Restore a job's resource allocation and accounting on the load side
|
||||
- Write a failing test first (TDD red) asserting that loading a saved job that carries a resource allocation reconstructs both the job's allocation and its per-host/per-resource accounting.
|
||||
- Restore the allocation via the deep-copy primitive from 1.1 (never sharing the saved representation's own copy), then rebuild the job's accounting from the restored allocation using the existing accounting constructor.
|
||||
- Position this restoration immediately after the scheduling-representation construction from 1.2 within the same job-reconstruction step, before the job becomes visible to the rest of the scheduler.
|
||||
- Treat a job with no saved allocation data as the normal case (no error, no warning) — leave its allocation and accounting unset, identical to today's behavior.
|
||||
- Observable completion: the new test goes from failing to passing, and loading a saved job with a multi-host, multi-resource allocation produces a restored job whose worker and shared resource usage accounting immediately reflects that allocation, with no further activity required.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 3.5_
|
||||
- _Depends: 1.1, 1.2_
|
||||
- _Boundary: snapshot load (allocation and accounting restoration)_
|
||||
|
||||
- [ ] 3. Integration: prove the save/load round trip end to end
|
||||
- [ ] 3.1 Verify a full cross-instance save/load round trip preserves allocation, accounting, and scheduling representation together
|
||||
- Save a scheduler instance containing a mix of running jobs (spanning multiple hosts, multiple resources, and a shared-resource-pool allocation) and pending jobs, then load that saved state into a second, independent scheduler instance.
|
||||
- Assert every running job's allocation and accounting match the source instance, every job (running or pending) has a valid scheduling representation, and that ending either instance's job list afterward does not corrupt or crash the other.
|
||||
- Observable completion: the round-trip test passes, demonstrating the restored instance's resource-in-use totals exactly match what the source instance reported before saving.
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.3, 2.4_
|
||||
- _Depends: 1.2, 2.1, 2.2_
|
||||
|
||||
- [ ] 3.2 Verify a snapshot with no allocation data loads cleanly
|
||||
- Extend the round-trip coverage with a saved state containing no allocation data for its jobs (the natural case for a job that was never dispatched, or a save taken before this feature existed).
|
||||
- Assert the load succeeds with no error, and every such job's resource contribution reads as zero/unavailable, identical to today's behavior.
|
||||
- Observable completion: the backward-compatibility test passes without any special-casing visible to the caller.
|
||||
- _Requirements: 3.5_
|
||||
- _Depends: 2.2_
|
||||
|
||||
- [ ] 4. Validation: close the reported symptom, prove no regressions, and keep documentation current
|
||||
- [ ] 4.1 (P) Prove a restored pending job with a valid resource request can be evaluated for allocation without crashing
|
||||
- Load a saved state containing a pending job whose resource request parses successfully, then run that job through the scheduler's normal allocation-evaluation path.
|
||||
- Assert no crash and no undefined behavior — this is the direct regression test for the scheduling-representation gap this feature closes. This does not cover a pending job whose resource request itself fails to parse on load — that pre-existing, separate failure path is explicitly out of this feature's boundary (see design.md's Known Limitation) and is not exercised by this test.
|
||||
- Observable completion: the test passes cleanly under the project's memory-safety build configuration.
|
||||
- _Requirements: 2.2, 2.4_
|
||||
- _Depends: 1.2_
|
||||
- _Boundary: snapshot load (scheduling-representation construction), test_snapshot_save_load.c_
|
||||
|
||||
- [ ] 4.2 (P) Reproduce and close the originally-reported resource-accounting symptom end to end
|
||||
- Extend the existing simulation-mode scenario that already reproduces the reported symptom (a saved state with many running jobs, replayed forward) to assert that every running job's resource contribution is correctly reflected immediately after loading, not only the jobs whose dispatch also happens to appear later in the replay.
|
||||
- Observable completion: the extended end-to-end scenario passes, showing 100% of the saved running jobs' resource contribution accounted for immediately on load.
|
||||
- _Requirements: 1.1, 1.2, 1.3_
|
||||
- _Depends: 3.1_
|
||||
- _Boundary: test_sim_e2e_original_motivation.c_
|
||||
|
||||
- [ ] 4.3 (P) Confirm no regression to existing reconciliation and the rest of the snapshot format
|
||||
- Run the existing replay-based reconciliation test coverage and the full existing test suite unchanged; confirm they all still pass exactly as before this feature.
|
||||
- Confirm no other previously-supported snapshot field's save/load behavior changed.
|
||||
- Observable completion: a full test run shows zero new failures and zero behavior change outside the allocation/scheduling-representation fields this feature touches.
|
||||
- _Requirements: 3.1, 3.4_
|
||||
- _Depends: 2.1, 2.2_
|
||||
- _Boundary: existing test suite (read-only run, no source changes)_
|
||||
|
||||
- [ ] 4.4 (P) Keep the snapshot format's own scope-decision documentation current
|
||||
- Correct the existing source-level note that a job's resource allocation "is not captured" to reflect that it now is, and add a note documenting the job's latest resource-usage report as a known, still-uncaptured gap requiring a future format change.
|
||||
- Observable completion: the source-level documentation and the feature's actual behavior no longer contradict each other.
|
||||
- _Requirements: 3.2, 3.3_
|
||||
- _Depends: 2.2_
|
||||
- _Boundary: snapshot.c (scope-decision header comment only)_
|
||||
|
||||
- [ ] 4.5 Run the project's scheduler design-invariant audit as final validation
|
||||
- Run the scheduler component's field-completeness invariant audit dimension (or manually re-check it) against the finished implementation.
|
||||
- Confirm the audit finds no undocumented gap introduced or left behind by this feature.
|
||||
- Observable completion: the audit dimension reports clean, with this feature's fields correctly classified as fixed or documented.
|
||||
- _Requirements: 3.2, 3.3, 3.4_
|
||||
- _Depends: 4.1, 4.2, 4.3, 4.4_
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -13,5 +13,6 @@ extern xce_job_alloc_t* make_job_alloc(uint64_t, int, xce_host_alloc_t**);
|
|||
extern xce_alloc_t** make_allocs(int, ...);
|
||||
extern xce_host_alloc_t** make_host_allocs(int, ...);
|
||||
extern xce_job_account_t* make_job_account(xce_job_alloc_t*);
|
||||
extern xce_job_alloc_t* dup_job_alloc(const xce_job_alloc_t*);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,6 +167,124 @@ xce_job_alloc_t* make_job_alloc(uint64_t jobid, int nhost,
|
|||
return jalloc;
|
||||
}
|
||||
|
||||
/* dup_job_alloc() (scheduler-snapshot-job-allocation task 1.1) -- deep-copy
|
||||
* a job's resource allocation, independent of the source, for safe
|
||||
* cross-core snapshot restoration (Req 1.1, 1.4). Composed entirely from
|
||||
* make_alloc()/make_host_alloc()/make_job_alloc() below; on any allocation
|
||||
* failure partway through, returns NULL rather than a partial/inconsistent
|
||||
* result -- matching this file's existing convention (make_host_account()/
|
||||
* make_job_account() do not unwind partial state on OOM either; a leak on
|
||||
* this rare path is accepted, not a defect). */
|
||||
|
||||
static xce_alloc_t* dup_alloc(const xce_alloc_t* src)
|
||||
{
|
||||
xce_alloc_t* alloc = NULL;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* make_alloc() stamps ->from = time(NULL) internally rather than
|
||||
* accepting the caller's original value, so from/duration are
|
||||
* explicitly re-stamped from src below after construction. */
|
||||
alloc = make_alloc(src->resname, src->reserve, src->instid,
|
||||
(float)src->value, 0);
|
||||
if (alloc == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
alloc->from = src->from;
|
||||
alloc->duration = src->duration;
|
||||
|
||||
return alloc;
|
||||
}
|
||||
|
||||
static xce_host_alloc_t* dup_host_alloc(const xce_host_alloc_t* src)
|
||||
{
|
||||
xce_alloc_t** allocs = NULL;
|
||||
xce_host_alloc_t* halloc = NULL;
|
||||
int i;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (src->nallocs > 0) {
|
||||
allocs = (xce_alloc_t**)calloc((size_t)src->nallocs,
|
||||
sizeof(*allocs));
|
||||
if (allocs == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < src->nallocs; i++) {
|
||||
allocs[i] = dup_alloc(src->allocs[i]);
|
||||
if (allocs[i] == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
halloc = make_host_alloc(src->host, src->nallocs, allocs);
|
||||
|
||||
return halloc;
|
||||
}
|
||||
|
||||
xce_job_alloc_t* dup_job_alloc(const xce_job_alloc_t* src)
|
||||
{
|
||||
xce_host_alloc_t** hallocs = NULL;
|
||||
xce_alloc_t** shared = NULL;
|
||||
xce_job_alloc_t* jalloc = NULL;
|
||||
int i;
|
||||
|
||||
if (src == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (src->nhosts > 0) {
|
||||
hallocs = (xce_host_alloc_t**)calloc((size_t)src->nhosts,
|
||||
sizeof(*hallocs));
|
||||
if (hallocs == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < src->nhosts; i++) {
|
||||
hallocs[i] = dup_host_alloc(src->allocs[i]);
|
||||
if (hallocs[i] == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jalloc = make_job_alloc(src->jobid, src->nhosts, hallocs);
|
||||
if (jalloc == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* make_job_alloc() always zeroes nres_shared/allocs_shared itself, so
|
||||
* the shared-resource pool is populated here, after construction --
|
||||
* the same "patch after construct" pattern used for from/duration
|
||||
* above. */
|
||||
if (src->nres_shared > 0) {
|
||||
shared = (xce_alloc_t**)calloc((size_t)src->nres_shared,
|
||||
sizeof(*shared));
|
||||
if (shared == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < src->nres_shared; i++) {
|
||||
shared[i] = dup_alloc(src->allocs_shared[i]);
|
||||
if (shared[i] == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
jalloc->allocs_shared = shared;
|
||||
jalloc->nres_shared = src->nres_shared;
|
||||
}
|
||||
|
||||
return jalloc;
|
||||
}
|
||||
|
||||
xce_job_account_t* make_job_account(xce_job_alloc_t* jalloc)
|
||||
{
|
||||
xce_job_account_t* account = NULL;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue