feat(scheduler-allocation): implement parjob's combinatorial plan generation
Add the real xce_alloc_plan_iter_t for parjob: for host-count k=1..maxTasks (ascending, most-preferred-first), generate one balanced partition of maxTasks into k buckets (floor/ceil split, remainder to the first rem buckets), which is descending by construction. Pure function of maxTasks alone -- never dereferences sched -- with a real pthread_testcancel() cancellation point and stable post-exhaustion NULL (Req 2.1-2.6, 4.1). my_alloc()/parjob_prepare_allocation() remain untouched stubs; wiring the iterator into real allocation is task 4.2. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYdtrEWWZfKRRGED7raEYg
This commit is contained in:
parent
9d64ba0d69
commit
6ca717fb86
4 changed files with 583 additions and 1 deletions
|
|
@ -8,6 +8,7 @@
|
|||
#include "kvmgr.h"
|
||||
#include "plugin.h"
|
||||
#include "preason.h"
|
||||
#include "sched/alloc_iter.h"
|
||||
#include "scheduler.h"
|
||||
#include "utils.h"
|
||||
#include "xce/types.h"
|
||||
|
|
@ -74,6 +75,160 @@ static int parjob_prepare_allocation(void* arg, xce_sched_ctx_t* ctx)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* parjob's combinatorial xce_alloc_plan_iter_t (task 4.1, Req 2 AC 1-6 /
|
||||
* Req 4 AC 1). Unlike seqjob's fixed single-plan iterator
|
||||
* (seqjob_allocator.c), this generates the full ascending host-count
|
||||
* sequence k = 1..maxTasks, one balanced partition of maxTasks into k
|
||||
* buckets per host-count, most-preferred (fewest hosts) first.
|
||||
*
|
||||
* Pure function of maxTasks alone (Req 4 AC 1's "1host x N, 2hosts x
|
||||
* ceil(N/2), ..., Nhosts x 1" sequence, generalized to real partitions
|
||||
* per requirements.md's "Data Structure Changes Summary"): `sched` is
|
||||
* accepted (fixed by the shared xce_alloc_plan_iter_t.init() signature)
|
||||
* but never dereferenced -- plan SHAPE and ORDER never inspect
|
||||
* sched->core.workers or any other live worker/resource state. Only
|
||||
* try_alloc_on_host()/host_iter.c (task 4.2's job) ever look at live
|
||||
* worker state, to decide whether a generated plan succeeds.
|
||||
*
|
||||
* Partition rule for a given host-count k (requirements.md, "Data
|
||||
* Structure Changes Summary", v1 default): the canonical BALANCED split
|
||||
* -- (maxTasks mod k) buckets get ceil(maxTasks/k) slots, the remaining
|
||||
* (k - maxTasks mod k) buckets get floor(maxTasks/k) slots. E.g.
|
||||
* maxTasks=7, k=3 -> [3,2,2]. Filling the first `rem` buckets with
|
||||
* (base+1) and the rest with `base` already yields descending
|
||||
* (largest-first) bucket order within the plan by construction -- no
|
||||
* separate sort step is needed, and this is the v1 default bucket order
|
||||
* requirements.md specifies (a real correctness property here, not a
|
||||
* fail-fast heuristic -- see the Composition Algorithm's worked example).
|
||||
*
|
||||
* `bucket_slots` is one heap buffer sized to the tightest true upper
|
||||
* bound (maxTasks, since a plan can never need more buckets than
|
||||
* maxTasks -- each bucket needs >=1 slot) and reused across next() calls
|
||||
* within one iterator instance, refilled from index 0 up to the current
|
||||
* plan's own nbuckets each call -- matching xce_alloc_plan_t's own
|
||||
* documented "valid until the next next()/fini() call" contract.
|
||||
*
|
||||
* plan.ctx is left NULL here, exactly like seqjob_plan_iter_init()'s own
|
||||
* convention (seqjob_allocator.c): init()'s fixed (iter, resreq, sched)
|
||||
* signature has no ctx parameter to thread it through. Setting
|
||||
* plan->ctx = ctx on every plan returned from next(), before
|
||||
* constructing a host_iter from it, is the CALLER's responsibility
|
||||
* (task 4.2's my_alloc(), not this iterator) -- see alloc_iter.h's
|
||||
* xce_alloc_plan_t.ctx doc comment and tasks.md's task 2.1 implementation
|
||||
* note.
|
||||
*/
|
||||
typedef struct {
|
||||
xce_alloc_plan_t plan;
|
||||
int* bucket_slots; /* heap buffer, size == max_tasks */
|
||||
int max_tasks; /* N -- primary_resreq->maxTasks */
|
||||
int next_k; /* next host-count to generate, 1-based */
|
||||
} parjob_plan_iter_priv_t;
|
||||
|
||||
static int parjob_plan_iter_init(xce_alloc_plan_iter_t* iter,
|
||||
xce_primary_resreq_t* resreq,
|
||||
xce_scheduler_t* sched)
|
||||
{
|
||||
parjob_plan_iter_priv_t* priv;
|
||||
int max_tasks;
|
||||
|
||||
/* Req 4 AC 1 / "pure function of the task count alone": sched is part
|
||||
* of the shared init() signature but never inspected -- plan
|
||||
* generation must not touch live worker/resource state. */
|
||||
(void)sched;
|
||||
|
||||
if ((iter == NULL) || (resreq == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
max_tasks = resreq->maxTasks;
|
||||
if (max_tasks < 1) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
priv = (parjob_plan_iter_priv_t*)calloc(1, sizeof(*priv));
|
||||
if (priv == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
priv->bucket_slots = (int*)calloc((size_t)max_tasks, sizeof(int));
|
||||
if (priv->bucket_slots == NULL) {
|
||||
free(priv);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
priv->max_tasks = max_tasks;
|
||||
priv->next_k = 1;
|
||||
priv->plan.nbuckets = 0;
|
||||
priv->plan.bucket_slots = priv->bucket_slots;
|
||||
priv->plan.ctx = NULL;
|
||||
|
||||
iter->private = priv;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static xce_alloc_plan_t* parjob_plan_iter_next(xce_alloc_plan_iter_t* iter)
|
||||
{
|
||||
parjob_plan_iter_priv_t* priv;
|
||||
int k;
|
||||
int base;
|
||||
int rem;
|
||||
int i;
|
||||
|
||||
/* Req 2 AC 6: a real cancellation point at least once per invocation
|
||||
* -- combinatorial plan generation is fast, non-blocking pure
|
||||
* computation with no cancellation point of its own. Called
|
||||
* unconditionally, including once already-exhausted, matching
|
||||
* host_iter.c's/seqjob_plan_iter_next()'s own convention. */
|
||||
pthread_testcancel();
|
||||
|
||||
if ((iter == NULL) || (iter->private == NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
priv = (parjob_plan_iter_priv_t*)iter->private;
|
||||
|
||||
/* Req 2 AC 4: stable end-of-iteration signal, once host-count would
|
||||
* exceed maxTasks, across every repeated call thereafter. */
|
||||
if (priv->next_k > priv->max_tasks) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
k = priv->next_k;
|
||||
priv->next_k++;
|
||||
|
||||
base = priv->max_tasks / k;
|
||||
rem = priv->max_tasks % k;
|
||||
|
||||
for (i = 0; i < rem; i++) {
|
||||
priv->bucket_slots[i] = base + 1;
|
||||
}
|
||||
for (i = rem; i < k; i++) {
|
||||
priv->bucket_slots[i] = base;
|
||||
}
|
||||
|
||||
priv->plan.nbuckets = k;
|
||||
|
||||
return &(priv->plan);
|
||||
}
|
||||
|
||||
static void parjob_plan_iter_fini(xce_alloc_plan_iter_t* iter)
|
||||
{
|
||||
parjob_plan_iter_priv_t* priv;
|
||||
|
||||
if ((iter == NULL) || (iter->private == NULL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
priv = (parjob_plan_iter_priv_t*)iter->private;
|
||||
|
||||
free(priv->bucket_slots);
|
||||
free(priv);
|
||||
|
||||
iter->private = NULL;
|
||||
}
|
||||
|
||||
static int my_alloc(xce_sched_plugin_t* plugin, xce_sched_ctx_t* ctx)
|
||||
{
|
||||
xce_scheduler_t* sched = (xce_scheduler_t*)plugin->sched;
|
||||
|
|
|
|||
|
|
@ -1559,6 +1559,27 @@ sched_seqjob_rewrap_equivalence_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Task 4.1 (scheduler-allocation spec, Req 2 AC 1-6, Req 4 AC 1):
|
||||
# parjob_allocator.c's REAL combinatorial xce_alloc_plan_iter_t
|
||||
# implementation -- ascending host-count plan order, one balanced
|
||||
# ceil/floor partition per host-count, descending bucket order within
|
||||
# each plan, a stable exhaustion signal, and a real pthread_testcancel()
|
||||
# cancellation point. White-box #include of
|
||||
# ../src/plugins/parjob_allocator.c itself (the file under test) --
|
||||
# simpler than sched_seqjob_allocator_test's own white-box stack since
|
||||
# parjob_allocator.c's still-stubbed my_alloc()/parjob_prepare_allocation()
|
||||
# bodies (untouched by this task -- task 4.2's job) make no calls into
|
||||
# core.c/resource.c/schedobj.c/host_iter.c at all, so none of those need
|
||||
# to be pulled in here. Only my_alloc()/prepare_allocation() themselves
|
||||
# are never called by this file's tests -- they exercise
|
||||
# parjob_plan_iter_init()/_next()/_fini() directly.
|
||||
sched_parjob_combinatorial_test = executable(
|
||||
'sched_parjob_combinatorial_test',
|
||||
['test_parjob_combinatorial.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||
|
|
@ -1756,3 +1777,6 @@ test('scheduler seqjob allocator test',
|
|||
test('scheduler seqjob rewrap equivalence test',
|
||||
sched_seqjob_rewrap_equivalence_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler parjob combinatorial test',
|
||||
sched_parjob_combinatorial_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
403
src/scheduler/testsuite/test_parjob_combinatorial.c
Normal file
403
src/scheduler/testsuite/test_parjob_combinatorial.c
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
/*
|
||||
* test_parjob_combinatorial.c - task 4.1 (scheduler-allocation spec):
|
||||
* unit tests for parjob_allocator.c's REAL combinatorial
|
||||
* xce_alloc_plan_iter_t implementation (Req 2 AC 1-6, Req 4 AC 1).
|
||||
*
|
||||
* Task 4.1's boundary is the plan-ITERATOR only -- parjob_allocator.c's
|
||||
* my_alloc()/parjob_prepare_allocation() bodies are untouched stubs,
|
||||
* still returning 0 unconditionally (task 4.2's job). So this file never
|
||||
* calls my_alloc(); it drives parjob_plan_iter_init()/_next()/_fini()
|
||||
* (static functions, hence the white-box #include below) directly,
|
||||
* exactly the way a future my_alloc() will.
|
||||
*
|
||||
* White-box #include convention: same as test_seqjob_allocator.c
|
||||
* (task 3.1), but simpler -- unlike seqjob_allocator.c's my_alloc(),
|
||||
* parjob_allocator.c's current my_alloc()/parjob_prepare_allocation()
|
||||
* stub bodies make no calls into resource.c/schedobj.c/core.c/host_iter.c
|
||||
* at all (confirmed by reading the file: both just touch struct fields
|
||||
* and return 0), so only ../src/plugins/parjob_allocator.c itself needs
|
||||
* to be pulled in, plus a link-satisfying stub for plugin_register()/
|
||||
* plugin_unregister() (referenced by register_parjob_allocator(), never
|
||||
* itself invoked by any test below).
|
||||
*
|
||||
* Covers:
|
||||
* - one-plan-per-call generation (Req 2 AC 2)
|
||||
* - most-preferred-first ordering: ascending host-count k = 1..N
|
||||
* (Req 2 AC 3, Req 4 AC 1)
|
||||
* - the canonical balanced partition rule, including an uneven split
|
||||
* (N=7 across k=3 hosts -> [3,2,2]) (Req 4 AC 1)
|
||||
* - descending bucket order within one plan, by construction (Req 2
|
||||
* AC 2's "not a uniform pair" + the Composition Algorithm's
|
||||
* descending-order correctness property)
|
||||
* - a stable end-of-iteration signal once k > N, stable across
|
||||
* repeated post-exhaustion calls (Req 2 AC 4)
|
||||
* - purity: plan shape/order is a pure function of maxTasks alone,
|
||||
* never inspecting live worker/scheduler state (Req 4 AC 1's "never
|
||||
* inspects live worker state" clause) -- proved by passing a NULL
|
||||
* xce_scheduler_t* to init() and confirming plans still generate
|
||||
* correctly
|
||||
* - fini() releases the iterator's private state (Req 2 AC 5)
|
||||
* - Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent
|
||||
* real cancellation point) -- proved behaviorally, same technique
|
||||
* test_host_iter.c already established for host_iter_next().
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "resreq.h"
|
||||
#include "sched/alloc_iter.h"
|
||||
#include "scheduler.h"
|
||||
|
||||
/* Pull in parjob_allocator.c itself (the file under test) -- the plan
|
||||
* iterator functions (parjob_plan_iter_init/next/fini) are `static`, so
|
||||
* this white-box #include is the only way to reach them directly,
|
||||
* matching test_seqjob_allocator.c's own established convention. */
|
||||
#include "../src/plugins/parjob_allocator.c"
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Link-satisfying stubs */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/* plugin_register()/plugin_unregister() are referenced by
|
||||
* register_parjob_allocator() (parjob_allocator.c), but no test below
|
||||
* calls register_parjob_allocator() -- the plan iterator is exercised
|
||||
* directly. Never actually invoked by anything this file's tests
|
||||
* exercise; provided only so the TU links. */
|
||||
int plugin_register(void* sched, void* plugin)
|
||||
{
|
||||
(void)sched;
|
||||
(void)plugin;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void plugin_unregister(void* arg) { (void)arg; }
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void bind_plan_iter(xce_alloc_plan_iter_t* iter)
|
||||
{
|
||||
memset(iter, 0, sizeof(*iter));
|
||||
iter->init = parjob_plan_iter_init;
|
||||
iter->next = parjob_plan_iter_next;
|
||||
iter->fini = parjob_plan_iter_fini;
|
||||
}
|
||||
|
||||
static void assert_plan_eq(xce_alloc_plan_t* plan, const int* expect,
|
||||
int nexpect)
|
||||
{
|
||||
int i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(plan);
|
||||
TEST_ASSERT_EQUAL_INT(nexpect, plan->nbuckets);
|
||||
for (i = 0; i < nexpect; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(expect[i], plan->bucket_slots[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Tests */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Req 2 AC 2/AC 3, Req 4 AC 1: for maxTasks = 7, successive next() calls
|
||||
* return exactly one plan per call, in ascending host-count order
|
||||
* (k = 1..7), each plan the canonical balanced ceil/floor partition with
|
||||
* buckets in descending order. k=3 is the uneven-split worked example
|
||||
* from requirements.md: [3,2,2], not [2,2,3]/[2,3,2]/[5,1,1].
|
||||
*/
|
||||
static void
|
||||
test_plans_ordered_ascending_host_count_with_balanced_partitions(void)
|
||||
{
|
||||
xce_primary_resreq_t resreq;
|
||||
xce_alloc_plan_iter_t iter_storage;
|
||||
xce_alloc_plan_iter_t* iter = &iter_storage;
|
||||
xce_alloc_plan_t* plan;
|
||||
|
||||
/* Expected balanced partition for maxTasks=7, per host-count k=1..7,
|
||||
* descending bucket order within each -- worked by hand against
|
||||
* requirements.md's "(N mod k) buckets get ceil(N/k), the rest get
|
||||
* floor(N/k)" rule. */
|
||||
static const int expect_k1[] = { 7 };
|
||||
static const int expect_k2[] = { 4, 3 };
|
||||
static const int expect_k3[] = { 3, 2, 2 }; /* the uneven-split example */
|
||||
static const int expect_k4[] = { 2, 2, 2, 1 };
|
||||
static const int expect_k5[] = { 2, 2, 1, 1, 1 };
|
||||
static const int expect_k6[] = { 2, 1, 1, 1, 1, 1 };
|
||||
static const int expect_k7[] = { 1, 1, 1, 1, 1, 1, 1 };
|
||||
|
||||
memset(&resreq, 0, sizeof(resreq));
|
||||
resreq.minTasks = 1;
|
||||
resreq.maxTasks = 7;
|
||||
|
||||
bind_plan_iter(iter);
|
||||
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k1, 1);
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k2, 2);
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k3, 3);
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k4, 4);
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k5, 5);
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k6, 6);
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k7, 7);
|
||||
|
||||
/* Req 2 AC 4: exhausted once k would exceed maxTasks (here k=8 > 7). */
|
||||
TEST_ASSERT_NULL(iter->next(iter));
|
||||
/* Stable across repeated post-exhaustion calls. */
|
||||
TEST_ASSERT_NULL(iter->next(iter));
|
||||
TEST_ASSERT_NULL(iter->next(iter));
|
||||
|
||||
iter->fini(iter);
|
||||
TEST_ASSERT_NULL(iter->private);
|
||||
}
|
||||
|
||||
/*
|
||||
* A clean divisor case (maxTasks = 6): every partition splits evenly, so
|
||||
* every bucket in every plan carries the same slot count -- descending
|
||||
* order is trivially satisfied (all equal), and this exercises rem == 0
|
||||
* for every k that evenly divides 6 (k=1,2,3,6) as well as k=4,5 where
|
||||
* rem != 0.
|
||||
*/
|
||||
static void test_plans_for_evenly_divisible_task_count(void)
|
||||
{
|
||||
xce_primary_resreq_t resreq;
|
||||
xce_alloc_plan_iter_t iter_storage;
|
||||
xce_alloc_plan_iter_t* iter = &iter_storage;
|
||||
xce_alloc_plan_t* plan;
|
||||
|
||||
static const int expect_k1[] = { 6 };
|
||||
static const int expect_k2[] = { 3, 3 };
|
||||
static const int expect_k3[] = { 2, 2, 2 };
|
||||
static const int expect_k4[] = { 2, 2, 1, 1 };
|
||||
static const int expect_k5[] = { 2, 1, 1, 1, 1 };
|
||||
static const int expect_k6[] = { 1, 1, 1, 1, 1, 1 };
|
||||
|
||||
memset(&resreq, 0, sizeof(resreq));
|
||||
resreq.minTasks = 1;
|
||||
resreq.maxTasks = 6;
|
||||
|
||||
bind_plan_iter(iter);
|
||||
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k1, 1);
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k2, 2);
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k3, 3);
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k4, 4);
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k5, 5);
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k6, 6);
|
||||
|
||||
TEST_ASSERT_NULL(iter->next(iter));
|
||||
TEST_ASSERT_NULL(iter->next(iter));
|
||||
|
||||
iter->fini(iter);
|
||||
}
|
||||
|
||||
/*
|
||||
* maxTasks = 1: exactly one plan (a single host, single slot), then
|
||||
* stable exhaustion. Degenerate but required boundary case -- parjob is
|
||||
* only ever reached for maxTasks > 1 in production (sched_make_primary_
|
||||
* alloc_()'s dispatch), but the iterator itself makes no such assumption.
|
||||
*/
|
||||
static void test_single_task_yields_single_plan(void)
|
||||
{
|
||||
xce_primary_resreq_t resreq;
|
||||
xce_alloc_plan_iter_t iter_storage;
|
||||
xce_alloc_plan_iter_t* iter = &iter_storage;
|
||||
xce_alloc_plan_t* plan;
|
||||
static const int expect_k1[] = { 1 };
|
||||
|
||||
memset(&resreq, 0, sizeof(resreq));
|
||||
resreq.minTasks = 1;
|
||||
resreq.maxTasks = 1;
|
||||
|
||||
bind_plan_iter(iter);
|
||||
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
||||
|
||||
plan = iter->next(iter);
|
||||
assert_plan_eq(plan, expect_k1, 1);
|
||||
|
||||
TEST_ASSERT_NULL(iter->next(iter));
|
||||
|
||||
iter->fini(iter);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 4 AC 1 ("never inspects live worker state" / "pure function of the
|
||||
* task count alone"): the SAME maxTasks value must yield IDENTICAL plan
|
||||
* sequences regardless of what (if anything) `sched` points to --
|
||||
* including a NULL scheduler, and a garbage-but-non-NULL one whose
|
||||
* fields are never touched. Since parjob_plan_iter_init()/_next() never
|
||||
* dereference `sched` at all, a non-NULL "poisoned" scheduler pointer
|
||||
* that would crash on any real field access is a stronger proof of
|
||||
* purity than merely passing NULL.
|
||||
*/
|
||||
static void test_plan_generation_ignores_scheduler_state(void)
|
||||
{
|
||||
xce_primary_resreq_t resreq;
|
||||
xce_alloc_plan_iter_t iter_a_storage;
|
||||
xce_alloc_plan_iter_t* iter_a = &iter_a_storage;
|
||||
xce_alloc_plan_iter_t iter_b_storage;
|
||||
xce_alloc_plan_iter_t* iter_b = &iter_b_storage;
|
||||
xce_scheduler_t* poisoned_sched = (xce_scheduler_t*)0x1;
|
||||
int k;
|
||||
|
||||
memset(&resreq, 0, sizeof(resreq));
|
||||
resreq.minTasks = 1;
|
||||
resreq.maxTasks = 7;
|
||||
|
||||
bind_plan_iter(iter_a);
|
||||
TEST_ASSERT_EQUAL_INT(0, iter_a->init(iter_a, &resreq, NULL));
|
||||
|
||||
bind_plan_iter(iter_b);
|
||||
/* A deliberately invalid, non-dereferenceable pointer -- init()/
|
||||
* next() must never read through it, or this test would crash
|
||||
* instead of merely failing an assertion. */
|
||||
TEST_ASSERT_EQUAL_INT(0, iter_b->init(iter_b, &resreq, poisoned_sched));
|
||||
|
||||
for (k = 1; k <= 7; k++) {
|
||||
xce_alloc_plan_t* plan_a = iter_a->next(iter_a);
|
||||
xce_alloc_plan_t* plan_b = iter_b->next(iter_b);
|
||||
int i;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(plan_a);
|
||||
TEST_ASSERT_NOT_NULL(plan_b);
|
||||
TEST_ASSERT_EQUAL_INT(plan_a->nbuckets, plan_b->nbuckets);
|
||||
for (i = 0; i < plan_a->nbuckets; i++) {
|
||||
TEST_ASSERT_EQUAL_INT(plan_a->bucket_slots[i],
|
||||
plan_b->bucket_slots[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_ASSERT_NULL(iter_a->next(iter_a));
|
||||
TEST_ASSERT_NULL(iter_b->next(iter_b));
|
||||
|
||||
iter_a->fini(iter_a);
|
||||
iter_b->fini(iter_b);
|
||||
}
|
||||
|
||||
/*
|
||||
* init() rejects invalid input rather than crashing: a NULL iter, a NULL
|
||||
* resreq, and a non-positive maxTasks.
|
||||
*/
|
||||
static void test_init_rejects_invalid_input(void)
|
||||
{
|
||||
xce_primary_resreq_t resreq;
|
||||
xce_alloc_plan_iter_t iter_storage;
|
||||
xce_alloc_plan_iter_t* iter = &iter_storage;
|
||||
|
||||
memset(&resreq, 0, sizeof(resreq));
|
||||
resreq.maxTasks = 3;
|
||||
|
||||
bind_plan_iter(iter);
|
||||
TEST_ASSERT_TRUE(iter->init(NULL, &resreq, NULL) != 0);
|
||||
TEST_ASSERT_TRUE(iter->init(iter, NULL, NULL) != 0);
|
||||
|
||||
resreq.maxTasks = 0;
|
||||
TEST_ASSERT_TRUE(iter->init(iter, &resreq, NULL) != 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Req 2 AC 6: next() calls pthread_testcancel() (or an equivalent real
|
||||
* cancellation point) at least once per invocation. Proved behaviorally,
|
||||
* same technique as test_host_iter.c's test_next_has_a_real_cancellation_
|
||||
* point(): a worker thread parked in a tight loop calling next()
|
||||
* repeatedly (spinning past exhaustion, which still executes the
|
||||
* cancellation point on every call) is reliably torn down by
|
||||
* pthread_cancel() from the main thread. If parjob_plan_iter_next() had
|
||||
* no real cancellation point, this tight loop would never yield to the
|
||||
* pending cancellation and pthread_join() below would hang (timing out
|
||||
* the whole test binary) rather than return.
|
||||
*/
|
||||
typedef struct {
|
||||
xce_alloc_plan_iter_t* plan_iter;
|
||||
} cancel_thread_arg_t;
|
||||
|
||||
static void* cancel_spin_thread(void* argp)
|
||||
{
|
||||
cancel_thread_arg_t* arg = (cancel_thread_arg_t*)argp;
|
||||
|
||||
for (;;) {
|
||||
(void)arg->plan_iter->next(arg->plan_iter);
|
||||
}
|
||||
|
||||
return NULL; /* unreachable */
|
||||
}
|
||||
|
||||
static void test_next_has_a_real_cancellation_point(void)
|
||||
{
|
||||
xce_primary_resreq_t resreq;
|
||||
xce_alloc_plan_iter_t iter_storage;
|
||||
xce_alloc_plan_iter_t* iter = &iter_storage;
|
||||
cancel_thread_arg_t arg;
|
||||
pthread_t tid;
|
||||
int rc;
|
||||
|
||||
memset(&resreq, 0, sizeof(resreq));
|
||||
resreq.minTasks = 1;
|
||||
resreq.maxTasks = 4;
|
||||
|
||||
bind_plan_iter(iter);
|
||||
TEST_ASSERT_EQUAL_INT(0, iter->init(iter, &resreq, NULL));
|
||||
|
||||
arg.plan_iter = iter;
|
||||
|
||||
rc = pthread_create(&tid, NULL, cancel_spin_thread, &arg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Let the spin thread actually get running before cancelling it. */
|
||||
usleep(20000);
|
||||
|
||||
rc = pthread_cancel(tid);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = pthread_join(tid, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Safe post-cancellation cleanup: pthread_testcancel() only ever
|
||||
* fires between complete logical steps (function entry), never
|
||||
* mid-mutation, so the iterator's private state is always
|
||||
* well-formed here regardless of exactly where cancellation
|
||||
* landed. */
|
||||
iter->fini(iter);
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
void tearDown(void) { return; }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_plans_ordered_ascending_host_count_with_balanced_partitions);
|
||||
RUN_TEST(test_plans_for_evenly_divisible_task_count);
|
||||
RUN_TEST(test_single_task_yields_single_plan);
|
||||
RUN_TEST(test_plan_generation_ignores_scheduler_state);
|
||||
RUN_TEST(test_init_rejects_invalid_input);
|
||||
RUN_TEST(test_next_has_a_real_cancellation_point);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue