fix(scheduler): cache hits keep the full pend reason; sim uses the cache (bug-1932)
The session-level unsatisfiable-resreq cache stored only the exhausted attempt's main reason (an int). The allocators record shortfalls as per-host/per-resource entries and leave main_reason at 0, so every cache hit in the live scheduler pended its job with an empty reason. The simulator registered no cache at all (its table lived on xce_scheduler_t), so it searched every identical pending job again and its traces differed from production. - The cache table moves to xce_sched_core_t; cache_lookup/insert/clear and the hook adapters take the core, with no cast to a driver struct. sched_init() builds it, cache_clear() rebuilds it per session, xce_sched_core_fini() destroys it. - A cache entry is a deep copy of the attempt's full xce_job_preason_t plus the job that proved the request unsatisfiable (core->alloc_jobid, set by sched_allocator()). A hit moves a copy into ctx->reason; a miss leaves ctx->reason untouched. - main.c passes the same three adapters to the simulator through new sched_sim_args_t fields; sched_sim_driver_start() sets them on the sim core (plain field writes: white-box tests include sim.c alone). - Decision trace: a hit renders "no plan generated: skipped, same request as <job> proved unsatisfiable earlier this session" (new XCE_TRACE_NOPLAN_KNOWN_UNSAT via sched_trace_known_unsat()); the outcome shows the restored reasons. - sim_verify keeps its main-only PEND compare (old recordings carry main-only hits); comments and the scheduler-sim-live-allocation design note updated. Tests: full-reason assertions in the cache dispatch tests (RED before: "a cache hit must restore the original attempt's FULL pend reason"), test_alloc_cache.c ported to the new API (deep copy, clear frees, -EEXIST frees), a sim-registered clear test, a renderer test, and a sim e2e scenario with two identical unsatisfiable jobs (204, 205) where 205 is skipped with 204's full reasons (RED before: no skip line). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cff3304f5d
commit
0317c7af98
27 changed files with 982 additions and 737 deletions
|
|
@ -35,6 +35,8 @@
|
|||
- bug-1827 (the `core_clock.c` isolation-test coverage gap) — unrelated, tracked separately.
|
||||
- Any change to `.replay` file format, recording/capture (`recording.c`/`recording_events.c`), or snapshot format (`snapshot_wire.c`/`snapshot_load.c`) — confirmed field-complete and unaffected by the audit (dimension 4).
|
||||
- Giving simulation mode a working `unsatisfiable_cache` implementation of its own.
|
||||
|
||||
> **Superseded (2026-10-09, bug-1932):** the two cache non-goals above no longer hold. The cache table moved from `xce_scheduler_t` onto `xce_sched_core_t`, the simulator registers the same three hooks as normal mode (passed in through `sched_sim_args_t`), and a hit now restores the original attempt's full pend reason instead of only its main reason. The seam itself is unchanged in role: no hook casts the core to a driver struct.
|
||||
- Simulation-mode thread lifecycle/teardown (`sched_fini()`-equivalent for sim mode does not exist today and this spec does not add one).
|
||||
|
||||
### Allowed Dependencies
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ extern void erase_preason_index(xce_preason_index_t*);
|
|||
*
|
||||
* The scheduler simulator (--sim-verify, sim_verify.c) deliberately does NOT
|
||||
* use this function for its PEND outcome check: it compares main_reason only
|
||||
* (bug-1845), because a recorded alloc-cache-hit PEND carries only the main
|
||||
* reason while the simulator (which has no alloc cache) always builds the
|
||||
* full detail lists. That loosening is a replay property, not object
|
||||
* (bug-1845), because a recorded PEND and its replay can legitimately carry
|
||||
* different detail (an alloc-cache hit recorded before bug-1932 carries only
|
||||
* the main reason). That loosening is a replay property, not object
|
||||
* identity, so it stays simulator-local.
|
||||
*
|
||||
* NULL handling: identical pointers (including two NULLs) are equal; exactly
|
||||
|
|
|
|||
|
|
@ -73,97 +73,53 @@ typedef int (*xce_core_decision_cb_t)(void*, xce_decision_t*);
|
|||
/*
|
||||
* xce_core_cache_clear_cb_t - driver-registered per-session cache-clear
|
||||
* hook (scheduler-allocation spec, task 5.2 round 2, Req 7.2):
|
||||
* `void (*)(xce_sched_core_t*)`. Mirrors xce_core_decision_cb_t's
|
||||
* driver-registered-hook idiom just above, but generic on purpose -- no
|
||||
* `void* driver_ctx` companion field, because the sole real
|
||||
* implementation (alloc_cache.c's xce_alloc_cache_clear_cb() -- see that
|
||||
* function's own doc comment for why it lives in alloc_cache.c rather
|
||||
* than driver.c) recovers its owning xce_scheduler_t* via the
|
||||
* pointer-to-first-member idiom this codebase already uses for this exact
|
||||
* struct (scheduler.h: `xce_sched_core_t core` is xce_scheduler_t's first
|
||||
* member), not via an opaque context pointer.
|
||||
* `void (*)(xce_sched_core_t*)`, called by core_begin_session_internal()
|
||||
* (core_session.c) when non-NULL. Both drivers register alloc_cache.c's
|
||||
* xce_alloc_cache_clear_cb() (main.c; sched_sim_driver_start() from the
|
||||
* caller's sched_sim_args_t), which empties THIS core's own
|
||||
* unsatisfiable_cache (bug-1932).
|
||||
*
|
||||
* WHY a hook instead of a direct call (task 5.2 round 1's CRITICAL defect):
|
||||
* core_begin_session_internal() (core_session.c) is reached from BOTH
|
||||
* driver.c's xce_sched_driver_run_normal_mode_round() (a real
|
||||
* xce_scheduler_t*) and sim_loop.c's sched_sim_driver_run() (a
|
||||
* structurally different xce_sched_sim_driver_t*, sim.h -- Req 5 AC 17
|
||||
* "Simulation isolation" -- which has NO unsatisfiable_cache field at
|
||||
* all). Round 1 called alloc_cache.c's real, strong
|
||||
* cache_clear(xce_scheduler_t*) unconditionally for both callers (routed
|
||||
* through a same-named weak fallback that the strong definition always
|
||||
* overrides at link time in the production binary) -- since link-time
|
||||
* weak/strong resolution picks ONE winner process-wide, not per call
|
||||
* site, every simulation-mode session reinterpreted whatever field sits
|
||||
* at unsatisfiable_cache's byte offset in xce_sched_sim_driver_t
|
||||
* (alt_events_file, a live FILE*) as a kvtab_t* and clobbered it.
|
||||
*
|
||||
* A per-core-instance function pointer fixes this at the data-ownership
|
||||
* level: each xce_sched_core_t only ever carries the hook ITS OWN driver
|
||||
* installed via xce_core_set_cache_clear_cb() below. main.c installs
|
||||
* alloc_cache.c's xce_alloc_cache_clear_cb() on the real xce_scheduler_t's
|
||||
* core; sim_loop.c never calls the setter at all, so a simulation-mode core's
|
||||
* hook stays NULL (xce_sched_core_init()'s memset() default) and
|
||||
* core_begin_session_internal() treats NULL as "this driver has no cache
|
||||
* to clear" and skips the call entirely -- a true no-op, not an
|
||||
* accidental type-confused one.
|
||||
* WHY a hook instead of a direct call: task 5.2 round 1 called a
|
||||
* cache_clear(xce_scheduler_t*) unconditionally for both drivers, so every
|
||||
* simulation-mode session reinterpreted xce_sched_sim_driver_t's
|
||||
* alt_events_file FILE* as the cache and clobbered it. The table now lives
|
||||
* on the core and no hook casts the core to a driver struct, but the hook
|
||||
* stays: white-box tests that link core_session.c without alloc_cache.c
|
||||
* leave it NULL, and NULL means "no cache to clear".
|
||||
*/
|
||||
typedef void (*xce_core_cache_clear_cb_t)(xce_sched_core_t*);
|
||||
|
||||
/*
|
||||
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t -
|
||||
* driver-registered, OPTIONAL per-session unsatisfiable-resreq cache
|
||||
* consult/populate seam (scheduler-sim-live-allocation spec, task 1.1,
|
||||
* Req 5.2). Mirrors xce_core_cache_clear_cb_t's own shape and rationale
|
||||
* immediately above exactly: a bare function pointer taking
|
||||
* xce_sched_core_t* directly, no wrapper struct, no opaque "impl"/
|
||||
* user-data pointer -- single registrant (main.c's normal-mode branch),
|
||||
* no foreseeable second implementation.
|
||||
* driver-registered, OPTIONAL unsatisfiable-resreq cache consult/populate
|
||||
* seam (scheduler-sim-live-allocation spec, task 1.1, Req 5.2) around the
|
||||
* allocation dispatch chokepoint, sched_make_primary_alloc_()
|
||||
* (scheduler.c). `obj` is the object cache_lookup()/cache_insert()
|
||||
* (sched/alloc_cache.h) key on: its resreq fingerprint plus user/queue/
|
||||
* project.
|
||||
*
|
||||
* Parameter shape mirrors the REAL cache's own existing entry points
|
||||
* (alloc_cache.c's cache_lookup()/cache_insert(), design.md's
|
||||
* "Unsatisfiable-Cache Callback Seam" component) rather than inventing a
|
||||
* new bare-integer "fingerprint" type: xce_sched_obj_t* is the exact
|
||||
* object cache_lookup()/cache_insert() already key on (its embedded
|
||||
* xce_resreq_fingerprint_t plus user/queue/project identity, sched/
|
||||
* alloc_cache.h) -- this seam adds no new type, only a NULL-safe
|
||||
* indirection over the existing one. xce_sched_obj_t is already visible
|
||||
* here as a forward-declared pointer type (sched/types.h, included
|
||||
* above); only a pointer, never the full struct, is needed in this
|
||||
* header.
|
||||
* bug-1932: a hit must report the original attempt's FULL pend reason,
|
||||
* not only its main reason (in production the main reason is usually 0
|
||||
* and the real cause is per-host, so a main-only hit pended later jobs
|
||||
* with an empty reason). Lookup returns 1 on a hit and replaces
|
||||
* *out_reasons with a copy of the stored reason, *out_jobid with the job
|
||||
* whose search proved the request unsatisfiable; 0 on a miss (outputs
|
||||
* untouched). Populate stores a copy of `reasons` (NULL: empty) for
|
||||
* `jobid`.
|
||||
*
|
||||
* WHY a hook instead of a direct call (mirrors xce_core_cache_clear_cb_t's
|
||||
* own documented incident above): the allocation dispatch chokepoint this
|
||||
* seam wraps (sched_make_primary_alloc_(), scheduler.c) is reached
|
||||
* identically by both drivers once a later task's Core-Safe Allocation
|
||||
* Chain migration lands -- but the real cache
|
||||
* (xce_scheduler_t.unsatisfiable_cache) exists ONLY on a normal-mode
|
||||
* xce_scheduler_t, never on a simulation-mode xce_sched_sim_driver_t. A
|
||||
* NULL-checked, driver-registered callback keeps that difference behind
|
||||
* an explicit seam instead of a struct-prefix cast into mode-specific
|
||||
* state from code both drivers share -- the same anti-pattern
|
||||
* xce_core_cache_clear_cb_t's own doc comment documents a real, prior
|
||||
* memory-corruption incident from.
|
||||
*
|
||||
* NULL (the xce_sched_core_init() memset() default, unless a driver
|
||||
* registers one) means "always compute fresh" for BOTH callbacks -- never
|
||||
* an error, never a degraded-correctness path, only a degraded-
|
||||
* performance one. Wiring the actual consult/populate call sites into
|
||||
* sched_make_primary_alloc_() is a LATER task's job (tasks.md 2.2,
|
||||
* boundary "Core-Safe Allocation Chain, Unsatisfiable-Cache Callback
|
||||
* Seam") -- this task adds only the seam (these two fields, their
|
||||
* adapters in alloc_cache.c, and main.c's normal-mode-only registration),
|
||||
* not that call site. Every simulation-mode core leaves both fields NULL
|
||||
* forever (sim_loop.c never registers them, Req 5 AC 17 "Simulation
|
||||
* isolation"), so a simulation-mode session never touches the real cache
|
||||
* implementation at all.
|
||||
* Both drivers register alloc_cache.c's adapters (bug-1932: the
|
||||
* simulator used to register none, so it searched every identical pending
|
||||
* job again and its traces differed from production). NULL (the
|
||||
* xce_sched_core_init() memset() default) means "always compute fresh" --
|
||||
* a performance difference only, never a correctness one.
|
||||
*/
|
||||
typedef int (*xce_unsatisfiable_lookup_cb_t)(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int* out_reason_category);
|
||||
typedef void (*xce_unsatisfiable_populate_cb_t)(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int reason_category);
|
||||
typedef int (*xce_unsatisfiable_lookup_cb_t)(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
xce_job_preason_t* out_reasons,
|
||||
uint64_t* out_jobid);
|
||||
typedef void (*xce_unsatisfiable_populate_cb_t)(
|
||||
xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
const xce_job_preason_t* reasons, uint64_t jobid);
|
||||
|
||||
/* Reserved jbuf sentinel for the Recorded-Decision Reconciliation
|
||||
* allocator-thread relay (bug-1837) -- see xce_sched_core_s's reconcile_req/
|
||||
|
|
@ -238,25 +194,25 @@ struct xce_sched_core_s {
|
|||
xce_core_decision_cb_t on_decision;
|
||||
/* Req 7.2 (scheduler-allocation spec, task 5.2 round 2): driver-
|
||||
* registered per-session cache-clear hook -- see
|
||||
* xce_core_cache_clear_cb_t's own doc comment above for the full
|
||||
* rationale. NULL until a driver registers one (xce_core_set_
|
||||
* cache_clear_cb()); core_begin_session_internal() (core_session.c)
|
||||
* treats NULL as "this driver has no cache to clear" and skips the
|
||||
* call -- true for every simulation-mode xce_sched_core_t, which never
|
||||
* registers this hook. */
|
||||
* xce_core_cache_clear_cb_t above. NULL until a driver registers one
|
||||
* (xce_core_set_cache_clear_cb()). */
|
||||
xce_core_cache_clear_cb_t cache_clear_cb;
|
||||
/* Req 5.2 (scheduler-sim-live-allocation spec, task 1.1): driver-
|
||||
* registered, OPTIONAL unsatisfiable-resreq cache consult/populate
|
||||
* seam -- see xce_unsatisfiable_lookup_cb_t/xce_unsatisfiable_
|
||||
* populate_cb_t's own doc comment above for the full rationale. NULL
|
||||
* (xce_sched_core_init()'s memset() default) until main.c's
|
||||
* normal-mode branch registers both, alongside cache_clear_cb just
|
||||
* above. A simulation-mode core never registers either, so
|
||||
* consulting/populating this seam is always a true no-op for sim
|
||||
* mode ("always compute fresh"), never a fallback onto real cache
|
||||
* state. */
|
||||
/* Req 5.2 (scheduler-sim-live-allocation spec, task 1.1): the
|
||||
* unsatisfiable-resreq cache consult/populate seam -- see
|
||||
* xce_unsatisfiable_lookup_cb_t above. NULL until a driver registers
|
||||
* both (both drivers do, bug-1932). */
|
||||
xce_unsatisfiable_lookup_cb_t unsatisfiable_lookup_cb;
|
||||
xce_unsatisfiable_populate_cb_t unsatisfiable_populate_cb;
|
||||
/* bug-1932: the session-level unsatisfiable-resreq cache itself
|
||||
* (sched/alloc_cache.h), owned by the core so both drivers have one.
|
||||
* Built and emptied by cache_clear() at every session start, destroyed
|
||||
* by xce_sched_core_fini(); NULL until the first clear. Allocator
|
||||
* thread only, plus the clear between sessions. */
|
||||
var unsatisfiable_cache;
|
||||
/* bug-1932: the job sched_make_allocation() is working on, set by
|
||||
* sched_allocator() (sched_election.c) so a cache populate can record
|
||||
* which job proved a request unsatisfiable. 0 when unknown. */
|
||||
uint64_t alloc_jobid;
|
||||
_Atomic int session_active; /* 1 during scheduling session, 0 between */
|
||||
_Atomic int eval_in_progress; /* 1 while eval_select loop runs */
|
||||
_Atomic int staging_merge_complete; /* 1 after staging merge completes */
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@
|
|||
*
|
||||
* Key shape: xce_resreq_fingerprint_t (sched/types.h, already populated on
|
||||
* xce_primary_schedobj_t.resreq_fp at job-ingest time -- see task 1.2)
|
||||
* composed with job identity (user/queue/project). Value: the ORIGINAL
|
||||
* exhausted attempt's pend-reason category (an int code), not a bare
|
||||
* present/absent marker -- Req 7.5 requires a cache hit to report the same
|
||||
* reason category the original miss determined, so operators are not
|
||||
* misled about why a later job was skipped.
|
||||
* composed with job identity (user/queue/project). Value: a deep copy of
|
||||
* the ORIGINAL exhausted attempt's full pend reason (main, per-host and
|
||||
* per-resource entries) plus the job whose search proved the request
|
||||
* unsatisfiable -- Req 7.5 requires a cache hit to report the same reason
|
||||
* the original miss determined, and bug-1932 found that keeping only the
|
||||
* main-reason int pended later jobs with an empty reason whenever the real
|
||||
* cause was per-host.
|
||||
*
|
||||
* Backed by this codebase's existing keyed-table infrastructure (kv.h's
|
||||
* kvtab_type/key_find()/key_add()) via a THIRD kv_key_ops_t instance
|
||||
|
|
@ -28,23 +30,16 @@
|
|||
* (cache_key_assign_(), below) and returns >=0 on success, -EEXIST if the
|
||||
* exact key is already present.
|
||||
*
|
||||
* This header intentionally exposes only what design.md's Service
|
||||
* Interface specifies (cache_lookup/cache_insert/cache_clear) plus the
|
||||
* key type and key ops -- no separate cache-construct/-destroy wrapper.
|
||||
* xce_scheduler_t.unsatisfiable_cache (scheduler.h) is constructed and
|
||||
* destroyed directly by sched_init()/sched_fini() (scheduler.c) via
|
||||
* makeInstance(&kvtab_type, ¶ms)/destroy(), using the exported
|
||||
* cache_key_ops below and the XCE_UNSAT_CACHE_NSLOTS slot count -- the
|
||||
* exact same convention xce_select.c's sel_node_table_init() already
|
||||
* establishes for its own composite-struct-keyed kvtab_t
|
||||
* (sel_node_key_ops). This cache's own unit test (test_alloc_cache.c)
|
||||
* constructs/destroys its fixture's table identically, so the two never
|
||||
* drift.
|
||||
* The table is xce_sched_core_t.unsatisfiable_cache (core.h, bug-1932:
|
||||
* moved off xce_scheduler_t so the simulation driver has one too).
|
||||
* cache_clear() builds and empties it at every session start, through the
|
||||
* per-session clear hook both drivers register; xce_sched_core_fini()
|
||||
* destroys it. Its datakiller frees each entry.
|
||||
*/
|
||||
|
||||
#include <kv.h>
|
||||
|
||||
#include "scheduler.h"
|
||||
#include "core.h"
|
||||
|
||||
/*
|
||||
* xce_cache_key_t - full cache key: the pre-computed resreq fingerprint
|
||||
|
|
@ -63,100 +58,87 @@ typedef struct {
|
|||
|
||||
/*
|
||||
* cache_key_ops - the kv_key_ops_t (kv.h) binding hash/assign/cmp/destroy
|
||||
* for xce_cache_key_t. Exported so scheduler.c's sched_init()/sched_fini()
|
||||
* (the real construct/destroy call sites for xce_scheduler_t.
|
||||
* unsatisfiable_cache) and this cache's own test can build an identical
|
||||
* kvtab_t independently.
|
||||
* for xce_cache_key_t; cache_clear() builds the table with it.
|
||||
*/
|
||||
extern kv_key_ops_t cache_key_ops;
|
||||
|
||||
/*
|
||||
* XCE_UNSAT_CACHE_NSLOTS - slot count for the kvtab_t backing
|
||||
* xce_scheduler_t.unsatisfiable_cache. Shared by every construction call
|
||||
* site (sched_init(), cache_clear()'s destroy+recreate, and this cache's
|
||||
* own test) so they never drift apart. kv.c's kvtab_init() requires
|
||||
* xce_sched_core_t.unsatisfiable_cache. kv.c's kvtab_init() requires
|
||||
* nslots > 1.
|
||||
*/
|
||||
#define XCE_UNSAT_CACHE_NSLOTS 257
|
||||
|
||||
/*
|
||||
* cache_lookup - Req 7.4: before starting full allocation for a job, look
|
||||
* up its resreq fingerprint + identity in the cache.
|
||||
* up its resreq fingerprint + identity in core's cache.
|
||||
*
|
||||
* Preconditions: obj->tag == XCE_RESREQ_PRIMARY; obj->u.primary->resreq_fp
|
||||
* already populated (job-ingest time, core_build_primary_schedobj(),
|
||||
* core_events.c -- task 1.2).
|
||||
*
|
||||
* Returns:
|
||||
* 1 - hit; *out_reason_category is set to the reason category the
|
||||
* ORIGINAL exhausted attempt stored at cache_insert() time (Req
|
||||
* 7.5). Caller pends the job using this category, without
|
||||
* invoking the plan/host iterators or preemption for this attempt.
|
||||
* 0 - miss (including: cache not yet constructed).
|
||||
* 1 - hit; *out is erased and refilled with a copy of the reason the
|
||||
* ORIGINAL exhausted attempt stored (Req 7.5, bug-1932) and
|
||||
* *out_jobid set to that attempt's job. Caller pends the job with
|
||||
* it, without invoking the plan/host iterators for this attempt.
|
||||
* A copy that runs out of memory part way keeps the entries copied
|
||||
* so far.
|
||||
* 0 - miss (including: cache not yet built); *out untouched.
|
||||
* <0 - error (NULL argument, or obj is not a populated primary resreq
|
||||
* scheduling object).
|
||||
*/
|
||||
extern int cache_lookup(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
||||
int* out_reason_category);
|
||||
extern int cache_lookup(xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
xce_job_preason_t* out, uint64_t* out_jobid);
|
||||
|
||||
/*
|
||||
* cache_insert - Req 7.3: when a job's primary resreq's plan iterator is
|
||||
* exhausted without a successful allocation, populate the cache with that
|
||||
* resreq's fingerprint + identity and the exhausted attempt's own final
|
||||
* pend-reason category (the exhausted plan iterator's own final pend
|
||||
* reason's main_reason), so a later cache hit for the same key reports the
|
||||
* same reason (Req 7.5).
|
||||
* resreq's fingerprint + identity, a deep copy of the attempt's pend
|
||||
* reason (`reasons`; NULL stores an empty one) and `jobid`, so a later hit
|
||||
* for the same key reports the same reason (Req 7.5).
|
||||
*
|
||||
* Returns 0 on success, <0 on error (NULL argument; obj is not a populated
|
||||
* primary resreq scheduling object; -EEXIST if this exact key is already
|
||||
* cached; the cache is not yet constructed).
|
||||
* Returns 0 on success, <0 on error (NULL core/obj; obj is not a
|
||||
* populated primary resreq scheduling object; -EEXIST if this exact key
|
||||
* is already cached; -ENOMEM; the cache is not yet built). Nothing is
|
||||
* stored on error.
|
||||
*/
|
||||
extern int cache_insert(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
||||
int reason_category);
|
||||
extern int cache_insert(xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
const xce_job_preason_t* reasons, uint64_t jobid);
|
||||
|
||||
/*
|
||||
* cache_clear - Req 7.2 (wiring into core_begin_session_internal() is a
|
||||
* later task -- tasks.md 5.2, not this one): empty every entry from the
|
||||
* cache. Called once at the start of every scheduling session.
|
||||
* cache_clear - Req 7.2: empty every entry from core's cache, building the
|
||||
* table on first use. Called once at the start of every scheduling
|
||||
* session through xce_alloc_cache_clear_cb().
|
||||
*
|
||||
* kv.h's intf_kv_t exposes only add_key/del_key/find_key -- no batch
|
||||
* clear-all primitive exists on kvtab_t. This implementation destroys the
|
||||
* existing backing table and constructs a fresh, empty one with the same
|
||||
* cache_key_ops/XCE_UNSAT_CACHE_NSLOTS in its place, which is functionally
|
||||
* equivalent to clearing every entry. If reconstruction fails (e.g.
|
||||
* existing backing table (its datakiller frees every entry) and constructs
|
||||
* a fresh, empty one in its place. If reconstruction fails (e.g.
|
||||
* makeInstance() returns NULL on -ENOMEM), the cache is left NULL:
|
||||
* cache_lookup() treats a NULL cache as an unconditional miss and
|
||||
* cache_insert() returns an error, so a failed clear degrades to "cache
|
||||
* always misses" rather than crashing or operating on a stale table.
|
||||
*/
|
||||
extern void cache_clear(xce_scheduler_t* sched);
|
||||
extern void cache_clear(xce_sched_core_t* core);
|
||||
|
||||
/*
|
||||
* xce_alloc_cache_clear_cb - the xce_core_cache_clear_cb_t (core.h)
|
||||
* adapter for cache_clear() above (scheduler-allocation spec, task 5.2
|
||||
* round 2, Req 7.2). Registered on the real xce_scheduler_t's core via
|
||||
* xce_core_set_cache_clear_cb() (main.c). See alloc_cache.c's own doc
|
||||
* comment on this function for the full rationale, including why it lives
|
||||
* xce_alloc_cache_clear_cb / xce_alloc_cache_lookup_cb /
|
||||
* xce_alloc_cache_populate_cb - the xce_core_cache_clear_cb_t /
|
||||
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t (core.h)
|
||||
* adapters for cache_clear()/cache_lookup()/cache_insert() on the given
|
||||
* core. Both drivers register all three (main.c directly; the simulation
|
||||
* driver from its sched_sim_args_t). See alloc_cache.c for why they live
|
||||
* here rather than in driver.c.
|
||||
*/
|
||||
extern void xce_alloc_cache_clear_cb(xce_sched_core_t* core);
|
||||
|
||||
/*
|
||||
* xce_alloc_cache_lookup_cb / xce_alloc_cache_populate_cb - the
|
||||
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t (core.h)
|
||||
* adapters for cache_lookup()/cache_insert() above (scheduler-sim-live-
|
||||
* allocation spec, task 1.1, Req 5.2). Registered on the real
|
||||
* xce_scheduler_t's core via direct field assignment in main.c's
|
||||
* normal-mode-only startup branch, alongside xce_alloc_cache_clear_cb()
|
||||
* just above. See alloc_cache.c's own doc comment on these functions for
|
||||
* the full rationale, including why they live here rather than in
|
||||
* driver.c.
|
||||
*/
|
||||
extern int xce_alloc_cache_lookup_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int* out_reason_category);
|
||||
extern void xce_alloc_cache_populate_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int reason_category);
|
||||
extern int xce_alloc_cache_lookup_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
xce_job_preason_t* out_reasons,
|
||||
uint64_t* out_jobid);
|
||||
extern void xce_alloc_cache_populate_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
const xce_job_preason_t* reasons,
|
||||
uint64_t jobid);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -132,7 +132,8 @@ typedef struct {
|
|||
typedef enum {
|
||||
XCE_TRACE_NOPLAN_DECLINED, /* allocator accepts() declined */
|
||||
XCE_TRACE_NOPLAN_GENERATOR_FAILED, /* plan_init failed */
|
||||
XCE_TRACE_NOPLAN_NONE_GENERATED /* generator produced no plan */
|
||||
XCE_TRACE_NOPLAN_NONE_GENERATED, /* generator produced no plan */
|
||||
XCE_TRACE_NOPLAN_KNOWN_UNSAT /* bug-1932: unsatisfiable-cache hit */
|
||||
} xce_sched_trace_noplan_t;
|
||||
|
||||
typedef enum {
|
||||
|
|
@ -314,6 +315,7 @@ typedef struct sched_trace_entry_s {
|
|||
} check_end;
|
||||
struct { /* NO_PLAN */
|
||||
xce_sched_trace_noplan_t why;
|
||||
uint64_t src_jobid; /* KNOWN_UNSAT: the job that proved it */
|
||||
} no_plan;
|
||||
struct { /* PLAN_BEGIN, PLAN_DISCARDED, PLAN_NOT_TRIED */
|
||||
int plan_no; /* 1-based, in generation order per requirement */
|
||||
|
|
@ -630,6 +632,10 @@ extern void sched_trace_check_end(xce_sched_trace_rec_t* rec, int rc,
|
|||
* later in the same requirement makes no statement and counts nothing. */
|
||||
extern void sched_trace_no_plan(xce_sched_trace_rec_t* rec,
|
||||
xce_sched_trace_noplan_t why);
|
||||
/* bug-1932: the same, for a request src_jobid (0: unknown) proved
|
||||
* unsatisfiable earlier this session -- the cache skipped the search. */
|
||||
extern void sched_trace_known_unsat(xce_sched_trace_rec_t* rec,
|
||||
uint64_t src_jobid);
|
||||
|
||||
/* Plans, numbered in generation order within the current requirement. */
|
||||
extern void sched_trace_plan_begin(xce_sched_trace_rec_t* rec,
|
||||
|
|
|
|||
|
|
@ -175,27 +175,6 @@ typedef struct {
|
|||
typedef struct {
|
||||
xce_sched_core_t core;
|
||||
xcecli_ctx_t apictx;
|
||||
/* Req 7.1 (scheduler-allocation spec, task 2.2): the per-session
|
||||
* unsatisfiable-resreq fingerprint cache -- a kvtab_t (kv.h, RTTI `var`)
|
||||
* keyed by xce_cache_key_t (sched/alloc_cache.h), value = a pend-reason
|
||||
* category boxed as (void*)(intptr_t)category (Req 7.5). Constructed in
|
||||
* sched_init(), destroyed in sched_fini() (scheduler.c) -- both minimal
|
||||
* construct/destroy calls only, using the exported cache_key_ops
|
||||
* (sched/alloc_cache.h), mirroring xce_select.c's sel_node_table_init()
|
||||
* convention for a composite-struct-keyed kvtab_t. Lookup/insert
|
||||
* (cache_lookup()/cache_insert(), alloc_cache.c) are confined to the
|
||||
* allocator thread; NULL until sched_init() constructs it.
|
||||
*
|
||||
* NOTE (task 2.2 CONCERN, for a later task's attention): Req 7.2
|
||||
* requires cache_clear() to run inside core_begin_session_internal()
|
||||
* (core_session.c), which only receives xce_sched_core_t* core, not
|
||||
* xce_scheduler_t* -- this field lives on the outer driver struct per
|
||||
* this task's own scope, not on `core` above. Wiring AC 2's clear call
|
||||
* is explicitly a later task (tasks.md 5.2), not this one; whoever
|
||||
* implements it will need to either reach the owning xce_scheduler_t
|
||||
* from `core` (core is this struct's first member, so the standard
|
||||
* pointer-to-first-member idiom applies) or relocate this field. */
|
||||
var unsatisfiable_cache;
|
||||
xce_cluster_config_t
|
||||
cluster_cfg; /* HA candidate list. Empty (candidates_count==0) only
|
||||
* transiently before sched_init() runs, when no
|
||||
|
|
|
|||
|
|
@ -164,6 +164,15 @@ typedef struct {
|
|||
bool interactive; /* --interactive / -i (task 12) */
|
||||
const char* log_path; /* --logfile FILE (optional; NULL means
|
||||
* SCHED_DEFAULT_LOG_PATH) */
|
||||
/* bug-1932: the unsatisfiable-resreq cache hooks (core.h), registered
|
||||
* on the simulation core exactly as main.c registers them on the
|
||||
* normal-mode one, so the simulator skips a request already proved
|
||||
* unsatisfiable this session the way production does. main.c passes
|
||||
* alloc_cache.c's adapters; NULL (white-box tests that do not link
|
||||
* alloc_cache.c) leaves the core computing every attempt fresh. */
|
||||
xce_core_cache_clear_cb_t cache_clear_cb;
|
||||
xce_unsatisfiable_lookup_cb_t unsat_lookup_cb;
|
||||
xce_unsatisfiable_populate_cb_t unsat_populate_cb;
|
||||
} sched_sim_args_t;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
* string_key_ops, since a composite struct key needs its own.
|
||||
*
|
||||
* - cache_lookup()/cache_insert()/cache_clear(), the three functions
|
||||
* design.md's Service Interface specifies.
|
||||
* design.md's Service Interface specifies, on the core's own table
|
||||
* (bug-1932), and the entry type they store: a deep copy of the
|
||||
* attempt's full pend reason plus the job that made it.
|
||||
*
|
||||
* cache_key_cmp_() is a THREE-WAY comparator (0 == equal), matching kv.c's
|
||||
* real int_key_ops.cmp (cmp_int_key) contract -- kvtab_get()'s only actual
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "preason.h"
|
||||
#include "sched/alloc_cache.h"
|
||||
|
||||
/*
|
||||
|
|
@ -122,6 +125,70 @@ kv_key_ops_t cache_key_ops = {
|
|||
.destroy = cache_key_destroy_,
|
||||
};
|
||||
|
||||
/*
|
||||
* cache_entry_t - one cached verdict (bug-1932): the exhausted attempt's
|
||||
* full pend reason, owned, and the job whose search produced it.
|
||||
*/
|
||||
typedef struct {
|
||||
xce_job_preason_t reasons;
|
||||
uint64_t jobid;
|
||||
} cache_entry_t;
|
||||
|
||||
/* The table's datakiller: frees an entry and every reason it owns. */
|
||||
static void cache_entry_free_(void* p)
|
||||
{
|
||||
cache_entry_t* e = (cache_entry_t*)p;
|
||||
|
||||
if (e == NULL) {
|
||||
return;
|
||||
}
|
||||
erase_job_preason(&(e->reasons));
|
||||
free(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* copy_reasons_ - replaces *dst with a deep copy of *src (NULL: empty).
|
||||
* The main reason goes first: add_job_preason()'s main-reason branch
|
||||
* erases the lists. Each entry's strings are strdup()ed by
|
||||
* add_job_preason(); on -ENOMEM the entries copied so far stay in *dst.
|
||||
*/
|
||||
static int copy_reasons_(xce_job_preason_t* dst, const xce_job_preason_t* src)
|
||||
{
|
||||
int rc;
|
||||
int i;
|
||||
|
||||
rc = add_job_preason(dst, (src != NULL) ? src->main_reason : 0, NULL, NULL);
|
||||
if ((rc < 0) || (src == NULL)) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
for (i = 0; i < src->nhost_reasons; i++) {
|
||||
const xce_host_reason_t* h = src->host_based_reasons[i];
|
||||
|
||||
if (h == NULL) {
|
||||
continue;
|
||||
}
|
||||
rc = add_job_preason(dst, h->reason, h->host, NULL);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < src->nres_reasons; i++) {
|
||||
const xce_res_reason_t* r = src->res_based_reasons[i];
|
||||
|
||||
if (r == NULL) {
|
||||
continue;
|
||||
}
|
||||
rc = add_job_preason(dst, r->reason, r->host, r->resource);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* build_key_ - construct the full cache key on the stack from a primary
|
||||
* scheduling object's pre-computed resreq fingerprint (Req 7.1, populated
|
||||
|
|
@ -144,14 +211,16 @@ static int build_key_(xce_cache_key_t* key, xce_sched_obj_t* obj)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int cache_lookup(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
||||
int* out_reason_category)
|
||||
int cache_lookup(xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
xce_job_preason_t* out, uint64_t* out_jobid)
|
||||
{
|
||||
xce_cache_key_t key;
|
||||
void* val = NULL;
|
||||
int rc;
|
||||
void* val = NULL;
|
||||
cache_entry_t* e;
|
||||
int rc;
|
||||
|
||||
if ((sched == NULL) || (obj == NULL) || (out_reason_category == NULL)) {
|
||||
if ((core == NULL) || (obj == NULL) || (out == NULL)
|
||||
|| (out_jobid == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
|
|
@ -160,29 +229,34 @@ int cache_lookup(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
|||
return rc;
|
||||
}
|
||||
|
||||
if (sched->unsatisfiable_cache == NULL) {
|
||||
/* Cache not (yet) constructed -- treat as an unconditional miss
|
||||
* rather than an error. */
|
||||
if (core->unsatisfiable_cache == NULL) {
|
||||
/* Cache not (yet) built -- treat as an unconditional miss rather
|
||||
* than an error. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
rc = key_find(sched->unsatisfiable_cache, &key, &val);
|
||||
if (rc <= 0) {
|
||||
rc = key_find(core->unsatisfiable_cache, &key, &val);
|
||||
if ((rc <= 0) || (val == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out_reason_category = (int)(intptr_t)val;
|
||||
e = (cache_entry_t*)val;
|
||||
*out_jobid = e->jobid;
|
||||
/* still a hit when the copy runs short of memory: the job is pended
|
||||
* either way, with whatever reasons were copied */
|
||||
(void)copy_reasons_(out, &(e->reasons));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int cache_insert(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
||||
int reason_category)
|
||||
int cache_insert(xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
const xce_job_preason_t* reasons, uint64_t jobid)
|
||||
{
|
||||
xce_cache_key_t key;
|
||||
int rc;
|
||||
cache_entry_t* e;
|
||||
int rc;
|
||||
|
||||
if ((sched == NULL) || (obj == NULL)) {
|
||||
if ((core == NULL) || (obj == NULL)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
|
|
@ -191,126 +265,76 @@ int cache_insert(xce_scheduler_t* sched, xce_sched_obj_t* obj,
|
|||
return rc;
|
||||
}
|
||||
|
||||
if (sched->unsatisfiable_cache == NULL) {
|
||||
if (core->unsatisfiable_cache == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
rc = key_add(sched->unsatisfiable_cache, &key,
|
||||
(void*)(intptr_t)reason_category);
|
||||
e = (cache_entry_t*)calloc(1, sizeof(*e));
|
||||
if (e == NULL) {
|
||||
return -ENOMEM;
|
||||
}
|
||||
e->jobid = jobid;
|
||||
|
||||
rc = copy_reasons_(&(e->reasons), reasons);
|
||||
if (rc >= 0) {
|
||||
rc = key_add(core->unsatisfiable_cache, &key, e);
|
||||
}
|
||||
if (rc < 0) {
|
||||
/* kvtab_add() never runs the datakiller on a failed add */
|
||||
cache_entry_free_(e);
|
||||
return rc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cache_clear(xce_scheduler_t* sched)
|
||||
void cache_clear(xce_sched_core_t* core)
|
||||
{
|
||||
kv_params_t params = { .nslots = XCE_UNSAT_CACHE_NSLOTS,
|
||||
.keyops = &cache_key_ops,
|
||||
.datakiller = NULL };
|
||||
.datakiller = cache_entry_free_ };
|
||||
|
||||
if (sched == NULL) {
|
||||
if (core == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sched->unsatisfiable_cache != NULL) {
|
||||
destroy(sched->unsatisfiable_cache);
|
||||
if (core->unsatisfiable_cache != NULL) {
|
||||
destroy(core->unsatisfiable_cache);
|
||||
}
|
||||
|
||||
sched->unsatisfiable_cache = makeInstance(&kvtab_type, ¶ms);
|
||||
core->unsatisfiable_cache = makeInstance(&kvtab_type, ¶ms);
|
||||
}
|
||||
|
||||
/*
|
||||
* xce_alloc_cache_clear_cb (scheduler-allocation spec, task 5.2 round 2,
|
||||
* Req 7.2) - the xce_core_cache_clear_cb_t (core.h) adapter for
|
||||
* cache_clear() above, registered on the real xce_scheduler_t's core via
|
||||
* xce_core_set_cache_clear_cb() (main.c, alongside the existing
|
||||
* xce_core_set_decision_cb() registration). `core` is xce_scheduler_t's
|
||||
* first member (scheduler.h: `typedef struct { xce_sched_core_t core;
|
||||
* ... } xce_scheduler_t;`), so per C11 6.7.2.1p15 (a pointer to a
|
||||
* structure's first member is validly convertible to a pointer to the
|
||||
* enclosing structure, and vice versa) this cast recovers the correct,
|
||||
* real xce_scheduler_t whenever this function is called through the
|
||||
* hook -- valid ONLY because the sole registration call site (main.c)
|
||||
* installs this exact function on a core instance genuinely embedded in
|
||||
* a real xce_scheduler_t.
|
||||
* xce_alloc_cache_clear_cb / xce_alloc_cache_lookup_cb /
|
||||
* xce_alloc_cache_populate_cb - the core.h hook adapters both drivers
|
||||
* register (main.c; sched_sim_driver_start() from its sched_sim_args_t).
|
||||
* Since bug-1932 the table is the core's own, so they pass `core` straight
|
||||
* through -- no cast to a driver struct (the round-1 cast of a simulation
|
||||
* core to xce_scheduler_t* is what clobbered alt_events_file, see core.h).
|
||||
*
|
||||
* Deliberately defined HERE (alloc_cache.c), not in driver.c, even though
|
||||
* driver.c is the file that legitimately owns a real xce_scheduler_t*
|
||||
* elsewhere: alloc_cache.c already owns cache_clear()'s xce_scheduler_t*
|
||||
* cast, and several white-box test TUs in this suite #include
|
||||
* ../src/driver.c directly WITHOUT linking alloc_cache.c (test_driver_
|
||||
* translate.c, test_driver_decision_cb.c, test_resval_ownership.c, every
|
||||
* test_sim_*.c/test_admin_request.c/test_record_lifecycle.c that pulls
|
||||
* driver.c in) -- had this adapter lived in driver.c and called
|
||||
* cache_clear() directly, every one of those TUs would fail to LINK (a
|
||||
* reference to an unresolved external symbol is a link error regardless
|
||||
* of whether the referencing function is ever actually invoked). Keeping
|
||||
* this adapter in alloc_cache.c means those TUs are entirely unaffected:
|
||||
* none of them link alloc_cache.c, none of them reference this function,
|
||||
* and none of them ever call xce_core_set_cache_clear_cb() either -- their
|
||||
* cache_clear_cb stays NULL, matching production simulation-mode
|
||||
* behavior exactly (see xce_core_cache_clear_cb_t's own doc comment,
|
||||
* core.h, for the full "why NULL is correct for sim mode" rationale).
|
||||
*
|
||||
* Round 1's CRITICAL defect is NOT reintroduced by this design: round 1's
|
||||
* bug was calling cache_clear() UNCONDITIONALLY from inside
|
||||
* core_begin_session_internal() (core_session.c) for every caller,
|
||||
* indiscriminately. This function is never called unconditionally by
|
||||
* anything -- it is only ever invoked as core->cache_clear_cb(core), and
|
||||
* only for a core instance that main.c's own registration explicitly
|
||||
* installed it on.
|
||||
* Deliberately defined HERE, not in driver.c or sim.c: several white-box
|
||||
* test TUs #include ../src/driver.c or ../src/sim.c directly WITHOUT
|
||||
* linking alloc_cache.c (test_driver_translate.c,
|
||||
* test_driver_decision_cb.c, test_resval_ownership.c, every test_sim_*.c
|
||||
* that pulls those in). A reference to these functions from there would
|
||||
* fail to link even if never called; those TUs leave the hooks NULL and
|
||||
* compute every attempt fresh. For the same reason the simulation driver
|
||||
* receives them as function pointers in sched_sim_args_t (sim.h) rather
|
||||
* than naming them.
|
||||
*/
|
||||
void xce_alloc_cache_clear_cb(xce_sched_core_t* core)
|
||||
{
|
||||
cache_clear((xce_scheduler_t*)core);
|
||||
}
|
||||
void xce_alloc_cache_clear_cb(xce_sched_core_t* core) { cache_clear(core); }
|
||||
|
||||
/*
|
||||
* xce_alloc_cache_lookup_cb / xce_alloc_cache_populate_cb -
|
||||
* scheduler-sim-live-allocation spec, task 1.1 (Req 5.2): the
|
||||
* xce_unsatisfiable_lookup_cb_t / xce_unsatisfiable_populate_cb_t (core.h)
|
||||
* adapters for cache_lookup()/cache_insert() above, mirroring
|
||||
* xce_alloc_cache_clear_cb()'s own shape and rationale exactly.
|
||||
*
|
||||
* Deliberately defined HERE (alloc_cache.c), not in driver.c or
|
||||
* scheduler.c, for the IDENTICAL white-box-test-linkage reason
|
||||
* xce_alloc_cache_clear_cb()'s own doc comment above documents: several
|
||||
* white-box test TUs #include ../src/driver.c directly WITHOUT linking
|
||||
* alloc_cache.c. Had either adapter lived anywhere those TUs don't
|
||||
* already link, every one of them would fail to LINK the moment a future
|
||||
* task wires a call site (main.c/a shared allocation-chain function)
|
||||
* against it. Keeping both adapters here means those TUs stay entirely
|
||||
* unaffected: none of them link alloc_cache.c, none of them reference
|
||||
* either adapter, and none of them ever assign core->unsatisfiable_
|
||||
* lookup_cb/unsatisfiable_populate_cb either -- both fields stay NULL,
|
||||
* matching production simulation-mode behavior exactly (see
|
||||
* xce_unsatisfiable_lookup_cb_t/xce_unsatisfiable_populate_cb_t's own doc
|
||||
* comment, core.h, for the full "why NULL is correct for sim mode"
|
||||
* rationale).
|
||||
*
|
||||
* The (xce_scheduler_t*)core cast below is safe for the identical reason
|
||||
* xce_alloc_cache_clear_cb()'s own cast is safe just above: these two
|
||||
* adapters are normal-mode-exclusive -- only ever assigned onto
|
||||
* core->unsatisfiable_lookup_cb/unsatisfiable_populate_cb by main.c's
|
||||
* normal-mode branch, only ever invoked with a `core` pointer genuinely
|
||||
* embedded in an xce_scheduler_t (xce_sched_core_t core is
|
||||
* xce_scheduler_t's first member, scheduler.h) -- a simulation-mode core
|
||||
* never registers either callback, so this cast is never reached with a
|
||||
* xce_sched_sim_driver_t*-embedded core. This is exactly the "small
|
||||
* adapter function... itself normal-mode-exclusive" allowance design.md's
|
||||
* "Unsatisfiable-Cache Callback Seam" component documents -- the cast
|
||||
* must never appear in code shared by both drivers, but is safe here.
|
||||
*/
|
||||
int xce_alloc_cache_lookup_cb(xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
int* out_reason_category)
|
||||
xce_job_preason_t* out_reasons,
|
||||
uint64_t* out_jobid)
|
||||
{
|
||||
return cache_lookup((xce_scheduler_t*)core, obj, out_reason_category);
|
||||
return cache_lookup(core, obj, out_reasons, out_jobid);
|
||||
}
|
||||
|
||||
void xce_alloc_cache_populate_cb(xce_sched_core_t* core, xce_sched_obj_t* obj,
|
||||
int reason_category)
|
||||
const xce_job_preason_t* reasons,
|
||||
uint64_t jobid)
|
||||
{
|
||||
cache_insert((xce_scheduler_t*)core, obj, reason_category);
|
||||
(void)cache_insert(core, obj, reasons, jobid);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,12 @@ void xce_sched_core_fini(xce_sched_core_t* core)
|
|||
destroy(core->plugins);
|
||||
core->plugins = NULL;
|
||||
}
|
||||
/* bug-1932: the unsatisfiable-resreq cache (cache_clear() built it with
|
||||
* a datakiller, so this frees every entry too) */
|
||||
if (core->unsatisfiable_cache != NULL) {
|
||||
destroy(core->unsatisfiable_cache);
|
||||
core->unsatisfiable_cache = NULL;
|
||||
}
|
||||
core_destroy_and_free_(&(core->jobs), (void (*)(void*))core_job_free_);
|
||||
core_destroy_and_free_(&(core->workers),
|
||||
(void (*)(void*))core_worker_free_);
|
||||
|
|
|
|||
|
|
@ -191,10 +191,8 @@ static void core_begin_session_internal(xce_sched_core_t* core)
|
|||
* per-session state this function already resets above, via the
|
||||
* driver-registered hook (core.h's xce_core_cache_clear_cb_t) rather
|
||||
* than an unconditional call -- see that typedef's own doc comment for
|
||||
* why round 1's unconditional weak-symbol cache_clear() call was a
|
||||
* CRITICAL type-confusion bug for simulation-mode sessions, and why a
|
||||
* NULL hook (sim_loop.c never registers one) must be a true no-op
|
||||
* here, not a fallthrough to any cache-clearing behavior. */
|
||||
* round 1's type-confusion bug. A NULL hook (a core whose driver
|
||||
* registered no cache) is a true no-op. */
|
||||
if (core->cache_clear_cb != NULL) {
|
||||
core->cache_clear_cb(core);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -388,6 +388,11 @@ int main(int argc, char* argv[])
|
|||
sim_args.output_path = arguments.sim_output;
|
||||
sim_args.interactive = (arguments.interactive != 0);
|
||||
sim_args.log_path = logfile;
|
||||
/* bug-1932: the simulator uses the same unsatisfiable-resreq cache
|
||||
* as normal mode (registered below for the live core) */
|
||||
sim_args.cache_clear_cb = xce_alloc_cache_clear_cb;
|
||||
sim_args.unsat_lookup_cb = xce_alloc_cache_lookup_cb;
|
||||
sim_args.unsat_populate_cb = xce_alloc_cache_populate_cb;
|
||||
|
||||
rc = sched_sim_driver_run(&sim, &sim_args);
|
||||
if (rc == -ENOSYS) {
|
||||
|
|
@ -492,38 +497,13 @@ int main(int argc, char* argv[])
|
|||
xce_core_set_decision_cb(&(sched.core), xce_sched_driver_on_decision,
|
||||
&sched);
|
||||
|
||||
/* Req 7.2 (scheduler-allocation spec, task 5.2 round 2): register the
|
||||
* per-session cache-clear hook -- see xce_core_cache_clear_cb_t's doc
|
||||
* comment (core.h) for why this replaced round 1's unconditional
|
||||
* weak-symbol cache_clear() call, and alloc_cache.c's own doc comment
|
||||
* on xce_alloc_cache_clear_cb() for why the adapter lives there
|
||||
* rather than in driver.c. Registered once, alongside the decision-cb
|
||||
* registration just above (the only other driver-registered core
|
||||
* hook), before the first scheduling round. The simulation driver
|
||||
* (sim_loop.c) deliberately never makes this call -- a
|
||||
* simulation-mode core has no cache to clear (Req 5 AC 17
|
||||
* "Simulation isolation"). */
|
||||
/* Req 7.2 / Req 5.2 (scheduler-allocation task 5.2, scheduler-sim-
|
||||
* live-allocation task 1.1): register the unsatisfiable-resreq cache's
|
||||
* per-session clear hook and its consult/populate seam, once, before
|
||||
* the first scheduling round -- see core.h's xce_core_cache_clear_cb_t
|
||||
* and xce_unsatisfiable_lookup_cb_t. The simulation branch above passes
|
||||
* the same three adapters through sched_sim_args_t (bug-1932). */
|
||||
xce_core_set_cache_clear_cb(&(sched.core), xce_alloc_cache_clear_cb);
|
||||
|
||||
/* Req 5.2 (scheduler-sim-live-allocation spec, task 1.1): register the
|
||||
* optional unsatisfiable-resreq cache consult/populate seam -- see
|
||||
* xce_unsatisfiable_lookup_cb_t/xce_unsatisfiable_populate_cb_t's own
|
||||
* doc comment (core.h) for the full rationale. Direct field
|
||||
* assignment, not a setter call (unlike cache_clear_cb just above):
|
||||
* xce_sched_core_t is not opaque, and these two fields carry no
|
||||
* locking/session-active discipline beyond the plain NULL-check the
|
||||
* seam's own contract already gives them -- main.c already writes
|
||||
* xce_sched_core_t fields directly elsewhere (e.g. sig_handler()'s
|
||||
* `sched->core.stop = 1;`, above), so this is a precedented idiom, not
|
||||
* a new one. Registered once, alongside every other driver-registered
|
||||
* core hook, before the first scheduling round. The simulation driver
|
||||
* (sim_loop.c) deliberately never sets either field -- a
|
||||
* simulation-mode core has no cache to consult or populate (Req 5 AC
|
||||
* 17 "Simulation isolation", same rationale as cache_clear_cb just
|
||||
* above). Wiring the actual consult/populate call site into
|
||||
* sched_make_primary_alloc_() (scheduler.c) is a LATER task's job
|
||||
* (tasks.md 2.2) -- until then, both fields being non-NULL here has
|
||||
* no observable effect, since nothing yet reads them. */
|
||||
sched.core.unsatisfiable_lookup_cb = xce_alloc_cache_lookup_cb;
|
||||
sched.core.unsatisfiable_populate_cb = xce_alloc_cache_populate_cb;
|
||||
|
||||
|
|
|
|||
|
|
@ -307,16 +307,26 @@ static void put_check_end(FILE* f, int rc)
|
|||
}
|
||||
}
|
||||
|
||||
static void put_no_plan(FILE* f, xce_sched_trace_noplan_t why)
|
||||
static void put_no_plan(FILE* f, const sched_trace_entry_t* e)
|
||||
{
|
||||
char job[64];
|
||||
|
||||
fputs("no plan generated: ", f);
|
||||
switch (why) {
|
||||
switch (e->u.no_plan.why) {
|
||||
case XCE_TRACE_NOPLAN_DECLINED:
|
||||
fputs("allocator declined the requirement", f);
|
||||
break;
|
||||
case XCE_TRACE_NOPLAN_GENERATOR_FAILED:
|
||||
fputs("plan generator failed to start", f);
|
||||
break;
|
||||
case XCE_TRACE_NOPLAN_KNOWN_UNSAT: /* bug-1932 */
|
||||
fputs("skipped, same request ", f);
|
||||
if ((e->u.no_plan.src_jobid != 0)
|
||||
&& (fmt_jobid(job, sizeof(job), e->u.no_plan.src_jobid) >= 0)) {
|
||||
fprintf(f, "as %s ", job);
|
||||
}
|
||||
fputs("proved unsatisfiable earlier this session", f);
|
||||
break;
|
||||
case XCE_TRACE_NOPLAN_NONE_GENERATED:
|
||||
default:
|
||||
fputs("generator produced no plan", f);
|
||||
|
|
@ -534,7 +544,7 @@ static void put_entry(FILE* f, const sched_trace_entry_t* e,
|
|||
put_check_end(f, e->u.check_end.rc);
|
||||
break;
|
||||
case SCHED_TRACE_E_NO_PLAN:
|
||||
put_no_plan(f, e->u.no_plan.why);
|
||||
put_no_plan(f, e);
|
||||
break;
|
||||
case SCHED_TRACE_E_PLAN_BEGIN:
|
||||
put_plan(f, e);
|
||||
|
|
|
|||
|
|
@ -317,7 +317,11 @@ void* sched_allocator(void* arg)
|
|||
/* calculate decision for jobid */
|
||||
/* scheduler-sim-live-allocation spec, task 2.2: sched_make_
|
||||
* allocation() is core-typed -- pass core directly. */
|
||||
alloc_rc = sched_make_allocation(core, job->schedobj);
|
||||
/* bug-1932: a cache populate records the job that proved its
|
||||
* request unsatisfiable, for later hits' trace lines */
|
||||
core->alloc_jobid = jobid;
|
||||
alloc_rc = sched_make_allocation(core, job->schedobj);
|
||||
core->alloc_jobid = 0;
|
||||
if (alloc_rc < 0) {
|
||||
/* something wrong */
|
||||
SCHED_TRACE(core,
|
||||
|
|
|
|||
|
|
@ -574,8 +574,10 @@ void sched_trace_check_end(xce_sched_trace_rec_t* rec, int rc,
|
|||
e->u.check_end.plans_follow = plans_follow;
|
||||
}
|
||||
|
||||
void sched_trace_no_plan(xce_sched_trace_rec_t* rec,
|
||||
xce_sched_trace_noplan_t why)
|
||||
/* The NO_PLAN entry, or NULL when it is not stored (over the line bound,
|
||||
* or the record is incomplete). */
|
||||
static sched_trace_entry_t* no_plan_add(xce_sched_trace_rec_t* rec,
|
||||
xce_sched_trace_noplan_t why)
|
||||
{
|
||||
sched_trace_entry_t* e;
|
||||
|
||||
|
|
@ -588,10 +590,25 @@ void sched_trace_no_plan(xce_sched_trace_rec_t* rec,
|
|||
}
|
||||
|
||||
e = sti_entry_add(rec, SCHED_TRACE_E_NO_PLAN);
|
||||
if (e == NULL) {
|
||||
return;
|
||||
if (e != NULL) {
|
||||
e->u.no_plan.why = why;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
void sched_trace_no_plan(xce_sched_trace_rec_t* rec,
|
||||
xce_sched_trace_noplan_t why)
|
||||
{
|
||||
(void)no_plan_add(rec, why);
|
||||
}
|
||||
|
||||
void sched_trace_known_unsat(xce_sched_trace_rec_t* rec, uint64_t src_jobid)
|
||||
{
|
||||
sched_trace_entry_t* e = no_plan_add(rec, XCE_TRACE_NOPLAN_KNOWN_UNSAT);
|
||||
|
||||
if (e != NULL) {
|
||||
e->u.no_plan.src_jobid = src_jobid;
|
||||
}
|
||||
e->u.no_plan.why = why;
|
||||
}
|
||||
|
||||
/* Fills a PLAN_BEGIN / PLAN_DISCARDED / PLAN_NOT_TRIED payload. */
|
||||
|
|
|
|||
|
|
@ -158,29 +158,51 @@ sched_primary_cache_obj_(xce_sched_ctx_t* ctx,
|
|||
}
|
||||
|
||||
/*
|
||||
* sched_pend_from_cached_reason_ - Req 7.4/7.5: on a cache hit, attach the
|
||||
* STORED reason category to ctx->reason using the identical mechanism
|
||||
* every allocator-exhaustion path already uses to create/populate a pend
|
||||
* reason (make_pendreason() + add_job_preason(), never a direct calloc()/
|
||||
* field write -- see preason.h/preason.c and e.g. resource.c's
|
||||
* sched_try_alloc_on_host_() or alloc_runner.c's sched_allocator_run(),
|
||||
* all of which follow this exact "if (ctx->reason == NULL) {
|
||||
* ctx->reason = make_pendreason(0); }" pattern). add_job_preason(reasons,
|
||||
* code, NULL, NULL) is that same function's own "set main reason" branch
|
||||
* (preason.c) -- the identical call shape design.md's cache_insert()
|
||||
* precondition ties reason_category to (xce_job_preason_t.main_reason).
|
||||
* sched_pend_from_cache_ - Req 7.4/7.5, bug-1932: consult the
|
||||
* driver-registered unsatisfiable-cache lookup hook. On a hit, ctx->reason
|
||||
* (created the way every allocator-exhaustion path creates it) is replaced
|
||||
* by a copy of the FULL reason the original exhausted attempt recorded --
|
||||
* main, per-host and per-resource -- and the decision trace names the job
|
||||
* that proved the request unsatisfiable. Keeping only the main reason
|
||||
* (the pre-bug-1932 behavior) pended later jobs with an empty reason
|
||||
* whenever the real cause was per-host.
|
||||
*
|
||||
* 1 on a hit (the caller pends without running either allocator), 0 on a
|
||||
* miss, a NULL hook, or no memory for the reason (all compute fresh).
|
||||
*/
|
||||
static void sched_pend_from_cached_reason_(xce_sched_ctx_t* ctx, int category)
|
||||
static int sched_pend_from_cache_(xce_sched_core_t* core, xce_sched_ctx_t* ctx,
|
||||
xce_sched_obj_t* cache_obj)
|
||||
{
|
||||
xce_job_preason_t found;
|
||||
uint64_t src_jobid = 0;
|
||||
|
||||
if (core->unsatisfiable_lookup_cb == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* looked up into a local, so a miss leaves ctx->reason as it was */
|
||||
memset(&found, 0, sizeof(found));
|
||||
if (core->unsatisfiable_lookup_cb(core, cache_obj, &found, &src_jobid)
|
||||
!= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ctx->reason == NULL) {
|
||||
ctx->reason = make_pendreason(0);
|
||||
}
|
||||
|
||||
if (ctx->reason == NULL) {
|
||||
return;
|
||||
if ((ctx->reason == NULL) || (ctx->reason->reasons == NULL)) {
|
||||
/* no storage for the reason: compute fresh instead */
|
||||
erase_job_preason(&found);
|
||||
return 0;
|
||||
}
|
||||
/* the copy's lists move into ctx's reason */
|
||||
erase_job_preason(ctx->reason->reasons);
|
||||
*(ctx->reason->reasons) = found;
|
||||
|
||||
add_job_preason(ctx->reason->reasons, category, NULL, NULL);
|
||||
/* Decision trace: no check and no plan for this requirement */
|
||||
SCHED_TRACE(core, sched_trace_known_unsat(core->trace_rec, src_jobid));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -240,7 +262,6 @@ static int sched_make_primary_alloc_(xce_sched_core_t* core,
|
|||
xce_primary_schedobj_t scratch_primary;
|
||||
xce_sched_obj_t scratch_obj;
|
||||
xce_sched_obj_t* cache_obj;
|
||||
int reason_category = 0;
|
||||
int rc;
|
||||
|
||||
cache_obj = sched_primary_cache_obj_(ctx, &scratch_primary, &scratch_obj);
|
||||
|
|
@ -248,14 +269,10 @@ static int sched_make_primary_alloc_(xce_sched_core_t* core,
|
|||
/* Req 7.4 / scheduler-sim-live-allocation spec, task 2.2
|
||||
* (Unsatisfiable-Cache Callback Seam): consult the driver-registered
|
||||
* lookup hook instead of calling cache_lookup() directly -- a NULL
|
||||
* hook (simulation mode, which never registers one) means "always
|
||||
* compute fresh", never an error, so this is the one primary-resreq
|
||||
* dispatch chokepoint the seam exists for. A cache hit pends the job
|
||||
* immediately, without invoking either allocator for this attempt. */
|
||||
if ((core->unsatisfiable_lookup_cb != NULL)
|
||||
&& (core->unsatisfiable_lookup_cb(core, cache_obj, &reason_category)
|
||||
== 1)) {
|
||||
sched_pend_from_cached_reason_(ctx, reason_category);
|
||||
* hook means "always compute fresh", never an error. A cache hit pends
|
||||
* the job immediately, with the original attempt's full reason
|
||||
* (bug-1932), without invoking either allocator for this attempt. */
|
||||
if (sched_pend_from_cache_(core, ctx, cache_obj)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -274,19 +291,17 @@ static int sched_make_primary_alloc_(xce_sched_core_t* core,
|
|||
|
||||
/* Req 7.3: once the chosen allocator has exhausted every candidate
|
||||
* plan (the not-satisfied signal, rc == 0), populate the cache with
|
||||
* this resreq's fingerprint + identity and the exhausted attempt's own
|
||||
* final pend-reason category, before the caller pends the job. A
|
||||
* negative rc is an infrastructure error, not an unsatisfiable
|
||||
* determination, so it is never cached; a positive rc is success.
|
||||
* Routed through the same NULL-safe seam as the lookup above -- a NULL
|
||||
* populate hook is simply a no-op, not an error. */
|
||||
if (rc == 0) {
|
||||
int category
|
||||
= (ctx->reason != NULL) ? ctx->reason->reasons->main_reason : 0;
|
||||
|
||||
if (core->unsatisfiable_populate_cb != NULL) {
|
||||
core->unsatisfiable_populate_cb(core, cache_obj, category);
|
||||
}
|
||||
* this resreq's fingerprint + identity, a copy of the exhausted
|
||||
* attempt's full pend reason and the job it was for (bug-1932), before
|
||||
* the caller pends the job. A negative rc is an infrastructure error,
|
||||
* not an unsatisfiable determination, so it is never cached; a
|
||||
* positive rc is success. Routed through the same NULL-safe seam as
|
||||
* the lookup above -- a NULL populate hook is simply a no-op. */
|
||||
if ((rc == 0) && (core->unsatisfiable_populate_cb != NULL)) {
|
||||
core->unsatisfiable_populate_cb(
|
||||
core, cache_obj,
|
||||
(ctx->reason != NULL) ? ctx->reason->reasons : NULL,
|
||||
core->alloc_jobid);
|
||||
}
|
||||
|
||||
return rc;
|
||||
|
|
@ -791,23 +806,11 @@ int sched_init(xce_scheduler_t* sched, xce_client_config_t* config,
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* Req 7.1 (scheduler-allocation spec, task 2.2): construct the
|
||||
* per-session unsatisfiable-resreq cache's backing kvtab_t, using the
|
||||
* exported cache_key_ops (sched/alloc_cache.h) -- same
|
||||
* makeInstance(&kvtab_type, ¶ms) convention xce_select.c's
|
||||
* sel_node_table_init() already establishes for a composite-struct-
|
||||
* keyed kvtab_t. Left NULL on -ENOMEM: cache_lookup()/cache_insert()
|
||||
* (alloc_cache.c) both treat a NULL cache defensively (unconditional
|
||||
* miss / error respectively) rather than crashing. */
|
||||
{
|
||||
kv_params_t unsat_cache_params
|
||||
= { .nslots = XCE_UNSAT_CACHE_NSLOTS,
|
||||
.keyops = &cache_key_ops,
|
||||
.datakiller = NULL };
|
||||
|
||||
sched->unsatisfiable_cache
|
||||
= makeInstance(&kvtab_type, &unsat_cache_params);
|
||||
}
|
||||
/* Req 7.1: build the unsatisfiable-resreq cache now, so it is usable
|
||||
* before the first session start empties it again (bug-1932: it lives
|
||||
* on the core). Left NULL on -ENOMEM: cache_lookup()/cache_insert()
|
||||
* treat a NULL cache as a miss / an error, never a crash. */
|
||||
cache_clear(&(sched->core));
|
||||
|
||||
/* bug-1584: xce_sched_core_init() alone leaves the clock atoms at the
|
||||
* neutral (0,0,1,1) default, under which xce_core_now() returns raw
|
||||
|
|
@ -969,20 +972,11 @@ void sched_fini(xce_scheduler_t* sched)
|
|||
xcecli_stop(sched->apictx);
|
||||
xcecli_ctx_fini(sched->apictx);
|
||||
|
||||
/* Req 7.1 (scheduler-allocation spec, task 2.2): destroy the
|
||||
* unsatisfiable-resreq cache's backing kvtab_t. Safe here -- both the
|
||||
* jobpicker and allocator threads (the only threads that ever touch
|
||||
* this cache, confined to the allocator side) have already been
|
||||
* cancelled and joined above. */
|
||||
if (sched->unsatisfiable_cache != NULL) {
|
||||
destroy(sched->unsatisfiable_cache);
|
||||
sched->unsatisfiable_cache = NULL;
|
||||
}
|
||||
|
||||
/* Req 5.1/5.2 (scheduler-core spec, task 4.1): the remaining teardown
|
||||
* (cbufs, pend_updates dataqs, resmgr/group managers, the CSE intern
|
||||
* table, the core lock) is xce_sched_core_fini()'s job (core.c) --
|
||||
* same order as before this task, just relocated. */
|
||||
* table, the core lock -- and, since bug-1932, the unsatisfiable-resreq
|
||||
* cache, now that the threads that touch it are joined above) is
|
||||
* xce_sched_core_fini()'s job (core.c). */
|
||||
xce_sched_core_fini(&(sched->core));
|
||||
|
||||
/* scheduler-core spec, task 9.4 fix round: pairs with sched_init()'s
|
||||
|
|
|
|||
|
|
@ -550,6 +550,15 @@ int sched_sim_driver_start(xce_sched_sim_driver_t* sim,
|
|||
return rc;
|
||||
}
|
||||
|
||||
/* bug-1932: the same unsatisfiable-resreq cache hooks normal mode
|
||||
* registers (main.c), passed in by the caller -- see sched_sim_args_t.
|
||||
* The cache is the core's own, so no hook reaches driver state. Plain
|
||||
* field writes, not xce_core_set_cache_clear_cb(): white-box tests
|
||||
* #include this file without core_session.c. */
|
||||
sim->core.cache_clear_cb = args->cache_clear_cb;
|
||||
sim->core.unsatisfiable_lookup_cb = args->unsat_lookup_cb;
|
||||
sim->core.unsatisfiable_populate_cb = args->unsat_populate_cb;
|
||||
|
||||
/* xce_sched_core_init() deliberately leaves core->pend_updates[0]/[1]
|
||||
* NULL (core.c's own file header comment: their dataq_init() killer
|
||||
* callback stays driver-owned) -- but xce_sched_core_fini()
|
||||
|
|
|
|||
|
|
@ -129,26 +129,17 @@ static void sim_verify_why(char** why, const char* fmt, ...)
|
|||
* pending-reason objects identical") than this one ("is this the same
|
||||
* replay outcome").
|
||||
*
|
||||
* bug-1845: core.h documents, as a deliberate "simulation isolation"
|
||||
* design decision, that "a simulation-mode session never touches the real
|
||||
* cache implementation at all" -- core->unsatisfiable_lookup_cb/
|
||||
* unsatisfiable_populate_cb are registered only by main.c's normal-mode
|
||||
* branch (alloc_cache.c), never by sim_loop.c. sched_make_primary_alloc_()
|
||||
* (scheduler.c) consults that cache FIRST; on a hit it records only
|
||||
* main_reason via sched_pend_from_cached_reason_() (no host/resource
|
||||
* detail at all -- that is the entire point of the cache, skipping the
|
||||
* expensive per-host iteration that would normally produce that detail).
|
||||
* Replay, having no cache, always falls through to the full per-host
|
||||
* computation and therefore always produces full detail. Two independent,
|
||||
* architecturally-intentional detail levels for the exact same semantic
|
||||
* outcome (this job is PEND, for this main reason) -- not a decision
|
||||
* correctness signal, and requiring them to match produced systematic
|
||||
* false MISMATCH reports on every cache-hit PEND decision in the original
|
||||
* recording. Live-verified against a real captured .replay file: expected
|
||||
* (recorded, cache-hit) reason showed
|
||||
* {main_reason=0, nhost_reasons=0, nres_reasons=0}; actual (replay, full
|
||||
* computation) showed {main_reason=0, nhost_reasons=0, nres_reasons=2} --
|
||||
* main_reason matched, detail did not, and never architecturally can.
|
||||
* bug-1845: a recorded PEND and its replay can carry the same outcome at
|
||||
* different detail levels. Until bug-1932, a live unsatisfiable-cache hit
|
||||
* recorded only main_reason ({main 0, nhost 0, nres 0} in a real captured
|
||||
* .replay) while the simulator, which then had no cache, computed the full
|
||||
* per-host detail ({main 0, nres 2}); requiring the detail to match
|
||||
* reported a false MISMATCH on every cache-hit PEND. Since bug-1932 both
|
||||
* modes use the cache and a hit copies the full reason, but main-only
|
||||
* stays: recordings made before the fix still carry main-only hits, and a
|
||||
* hit legitimately carries the reasons of the job that proved the request
|
||||
* unsatisfiable, which a recording made without a hit for this job need
|
||||
* not share.
|
||||
*/
|
||||
static int sim_verify_pend_main_reason_eq(const xce_job_preason_t* a,
|
||||
const xce_job_preason_t* b,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* test_alloc_cache.c - unit test for the session-level unsatisfiable-
|
||||
* resreq cache (scheduler-allocation spec, task 2.2, Req 7.1, 7.3, 7.4,
|
||||
* 7.5).
|
||||
* 7.5; bug-1932).
|
||||
*
|
||||
* White-box test TU: #includes ../src/alloc_cache.c directly (same
|
||||
* convention test_host_iter.c/test_job_update.c/test_handlers.c already
|
||||
|
|
@ -9,14 +9,12 @@
|
|||
* cache_key_hash_/cache_key_cmp_/cache_key_assign_/cache_key_destroy_ and
|
||||
* cache_lookup()/cache_insert()/cache_clear() implementations, not a stub.
|
||||
*
|
||||
* alloc_cache.c only depends on kv.h (libxce, already linked) and
|
||||
* scheduler.h/sched/types.h (header-only struct definitions) -- it never
|
||||
* touches xce_scheduler_t.core, so this fixture never calls
|
||||
* xce_sched_core_init(); a bare memset()'d xce_scheduler_t with its own
|
||||
* unsatisfiable_cache field constructed directly (mirroring sched_init()'s
|
||||
* own construction call, scheduler.c) is sufficient.
|
||||
* bug-1932: the cache lives on xce_sched_core_t (both drivers own one), so
|
||||
* the fixture is a bare memset()'d core whose table cache_clear() builds --
|
||||
* the same call both drivers' per-session clear hook makes. core.c is not
|
||||
* linked here, so the fixture destroys the table itself.
|
||||
*
|
||||
* Covers, per this task's Observable line:
|
||||
* Covers:
|
||||
* - same-key-twice HIT (not just insert-then-miss-on-a-different-key,
|
||||
* which the cache_key_cmp_() inverted-polarity bug already found and
|
||||
* fixed during design review -- requirements.md's "New types (Req 7)"
|
||||
|
|
@ -25,10 +23,10 @@
|
|||
* - a lookup differing in any single field (each of the four resreq
|
||||
* fingerprint fields, and each of the three identity fields) is a
|
||||
* miss.
|
||||
* - reason-category round-trip: insert with category X, hit later
|
||||
* returns X -- tested with two distinct category values to rule out a
|
||||
* trivial always-returns-0 implementation.
|
||||
* - clear() empties all entries.
|
||||
* - bug-1932: a hit returns the FULL stored pend reason (main, per-host
|
||||
* and per-resource entries) and the job that proved it, as a deep copy
|
||||
* independent of both the inserted original and later lookups.
|
||||
* - clear() empties all entries (and frees them: ASAN).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
|
|
@ -37,6 +35,7 @@
|
|||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "preason.h"
|
||||
#include "sched/alloc_cache.h"
|
||||
#include "scheduler.h"
|
||||
|
||||
|
|
@ -51,28 +50,20 @@ void tearDown(void) { return; }
|
|||
/* Fixture helpers */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* init_sched - a bare xce_scheduler_t with only unsatisfiable_cache
|
||||
* constructed (this cache never touches sched->core). Mirrors sched_init()'s
|
||||
* own construction call (scheduler.c) exactly, so the fixture and
|
||||
* production code can never silently drift.
|
||||
*/
|
||||
static void init_sched(xce_scheduler_t* sched)
|
||||
/* init_core - a bare core with only its cache table built, by the same
|
||||
* cache_clear() call the drivers' per-session hook makes. */
|
||||
static void init_core(xce_sched_core_t* core)
|
||||
{
|
||||
kv_params_t params = { .nslots = XCE_UNSAT_CACHE_NSLOTS,
|
||||
.keyops = &cache_key_ops,
|
||||
.datakiller = NULL };
|
||||
|
||||
memset(sched, 0, sizeof(*sched));
|
||||
sched->unsatisfiable_cache = makeInstance(&kvtab_type, ¶ms);
|
||||
TEST_ASSERT_NOT_NULL(sched->unsatisfiable_cache);
|
||||
memset(core, 0, sizeof(*core));
|
||||
cache_clear(core);
|
||||
TEST_ASSERT_NOT_NULL(core->unsatisfiable_cache);
|
||||
}
|
||||
|
||||
static void fini_sched(xce_scheduler_t* sched)
|
||||
static void fini_core(xce_sched_core_t* core)
|
||||
{
|
||||
if (sched->unsatisfiable_cache != NULL) {
|
||||
destroy(sched->unsatisfiable_cache);
|
||||
sched->unsatisfiable_cache = NULL;
|
||||
if (core->unsatisfiable_cache != NULL) {
|
||||
destroy(core->unsatisfiable_cache);
|
||||
core->unsatisfiable_cache = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +96,22 @@ static void init_primary_obj(xce_sched_obj_t* obj, xce_primary_schedobj_t* prima
|
|||
obj->project = (char*)project;
|
||||
}
|
||||
|
||||
/* A pend reason with every kind of entry: main `main_reason`, one host
|
||||
* reason and two resource reasons (one host-scoped, one cluster-wide). */
|
||||
static void make_reasons(xce_job_preason_t* r, int main_reason,
|
||||
const char* host)
|
||||
{
|
||||
memset(r, 0, sizeof(*r));
|
||||
TEST_ASSERT_EQUAL_INT(0, add_job_preason(r, main_reason, NULL, NULL));
|
||||
TEST_ASSERT_TRUE(add_job_preason(r, PREASON_MATCH_FAIL, (char*)host, NULL)
|
||||
> 0);
|
||||
TEST_ASSERT_TRUE(
|
||||
add_job_preason(r, PREASON_NO_RESOURCE, (char*)host, (char*)"slots")
|
||||
> 0);
|
||||
TEST_ASSERT_TRUE(
|
||||
add_job_preason(r, PREASON_NO_RESOURCE, NULL, (char*)"license") > 0);
|
||||
}
|
||||
|
||||
/* Baseline fixture fingerprint + identity, reused (with one field varied
|
||||
* at a time) across the miss tests below. Distinct string literals (not
|
||||
* merely distinct content) are used for the identity fields so pointer
|
||||
|
|
@ -115,6 +122,7 @@ static void init_primary_obj(xce_sched_obj_t* obj, xce_primary_schedobj_t* prima
|
|||
#define BASE_MAX_TASKS 4
|
||||
#define BASE_SELECT_NODE_ID 100u
|
||||
#define BASE_RUSAGE_HASH ((uint64_t)0xdeadbeefULL)
|
||||
#define BASE_JOBID ((uint64_t)4242ULL)
|
||||
static const char BASE_USER[] = "alice";
|
||||
static const char BASE_QUEUE[] = "normal";
|
||||
static const char BASE_PROJECT[] = "proj1";
|
||||
|
|
@ -128,21 +136,24 @@ static const char BASE_PROJECT[] = "proj1";
|
|||
* from day one would not fail any test that doesn't specifically assert a
|
||||
* hit"): insert once, then look up an INDEPENDENTLY constructed key with
|
||||
* every field identical (including pointer-identical identity fields) --
|
||||
* must be a hit, returning the exact category given at insert time. A
|
||||
* cache_key_cmp_() with the earlier, inverted boolean polarity would fail
|
||||
* must be a hit, returning the exact reasons and job given at insert time.
|
||||
* A cache_key_cmp_() with the earlier, inverted boolean polarity would fail
|
||||
* this test (every lookup a false miss) while still passing a weaker
|
||||
* insert-then-miss-on-a-different-key round-trip.
|
||||
*/
|
||||
static void test_same_key_twice_is_a_hit(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj1, obj2;
|
||||
xce_primary_schedobj_t primary1, primary2;
|
||||
int category = 42;
|
||||
int out = -1;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj1, obj2;
|
||||
xce_primary_schedobj_t primary1, primary2;
|
||||
xce_job_preason_t in;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 0;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
make_reasons(&in, 0, "host1");
|
||||
memset(&out, 0, sizeof(out));
|
||||
|
||||
init_primary_obj(&obj1, &primary1, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
|
|
@ -151,56 +162,73 @@ static void test_same_key_twice_is_a_hit(void)
|
|||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
rc = cache_insert(&sched, &obj1, category);
|
||||
rc = cache_insert(&core, &obj1, &in, BASE_JOBID);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = cache_lookup(&sched, &obj2, &out);
|
||||
rc = cache_lookup(&core, &obj2, &out, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(category, out);
|
||||
TEST_ASSERT_EQUAL_UINT64(BASE_JOBID, jobid);
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
job_preason_eq(&in, &out, NULL),
|
||||
"bug-1932: a hit must restore the full stored pend reason -- main, "
|
||||
"per-host and per-resource entries -- not just the main reason");
|
||||
TEST_ASSERT_EQUAL_INT(1, out.nhost_reasons);
|
||||
TEST_ASSERT_EQUAL_INT(2, out.nres_reasons);
|
||||
|
||||
fini_sched(&sched);
|
||||
erase_job_preason(&in);
|
||||
erase_job_preason(&out);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* A lookup before any insert is a plain miss.
|
||||
* A lookup before any insert is a plain miss, and leaves `out` alone.
|
||||
*/
|
||||
static void test_lookup_before_any_insert_is_a_miss(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
int out = -1;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 7;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
memset(&out, 0, sizeof(out));
|
||||
out.main_reason = 9;
|
||||
|
||||
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
rc = cache_lookup(&sched, &obj, &out);
|
||||
rc = cache_lookup(&core, &obj, &out, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(9, out.main_reason);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, jobid);
|
||||
|
||||
fini_sched(&sched);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* differing_field_misses - shared body for "a lookup differing in exactly
|
||||
* one field is a miss": insert the base key with `category`, then look up
|
||||
* a second key built from the given (possibly-varied) fields -- must miss.
|
||||
* assert_variant_misses - shared body for "a lookup differing in exactly
|
||||
* one field is a miss": insert the base key, then look up a second key
|
||||
* built from the given (possibly-varied) fields -- must miss.
|
||||
*/
|
||||
static void assert_variant_misses(int min_tasks, int max_tasks,
|
||||
uint32_t select_node_id,
|
||||
uint64_t rusage_hash, const char* user,
|
||||
const char* queue, const char* project)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t base_obj, variant_obj;
|
||||
xce_primary_schedobj_t base_primary, variant_primary;
|
||||
int out = -1;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t base_obj, variant_obj;
|
||||
xce_primary_schedobj_t base_primary, variant_primary;
|
||||
xce_job_preason_t in;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 0;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
make_reasons(&in, 0, "host1");
|
||||
memset(&out, 0, sizeof(out));
|
||||
|
||||
init_primary_obj(&base_obj, &base_primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
|
|
@ -208,13 +236,14 @@ static void assert_variant_misses(int min_tasks, int max_tasks,
|
|||
init_primary_obj(&variant_obj, &variant_primary, min_tasks, max_tasks,
|
||||
select_node_id, rusage_hash, user, queue, project);
|
||||
|
||||
rc = cache_insert(&sched, &base_obj, 7);
|
||||
rc = cache_insert(&core, &base_obj, &in, BASE_JOBID);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = cache_lookup(&sched, &variant_obj, &out);
|
||||
rc = cache_lookup(&core, &variant_obj, &out, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
fini_sched(&sched);
|
||||
erase_job_preason(&in);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
static void test_miss_on_differing_min_tasks(void)
|
||||
|
|
@ -273,63 +302,128 @@ static void test_miss_on_differing_project(void)
|
|||
}
|
||||
|
||||
/*
|
||||
* Req 7.5: a cache hit reports the SAME reason category the original
|
||||
* miss's exhausted attempt stored -- tested with two distinct category
|
||||
* values (on two distinct keys, since re-inserting the identical key
|
||||
* would hit kvtab_add()'s own -EEXIST contract) to rule out a trivial
|
||||
* always-returns-0 (or always-returns-the-first-inserted-value)
|
||||
* implementation.
|
||||
* Req 7.5 / bug-1932: two distinct keys keep their own reasons and jobs --
|
||||
* rules out an implementation returning the first-inserted (or a fixed)
|
||||
* value for every hit.
|
||||
*/
|
||||
static void test_reason_category_round_trip_two_distinct_categories(void)
|
||||
static void test_two_entries_keep_their_own_reasons_and_jobs(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj_a, obj_b;
|
||||
xce_primary_schedobj_t primary_a, primary_b;
|
||||
static const char user_b[] = "carol";
|
||||
int out_a = -1, out_b = -1;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj_a, obj_b;
|
||||
xce_primary_schedobj_t primary_a, primary_b;
|
||||
static const char user_b[] = "carol";
|
||||
xce_job_preason_t in_a, in_b, out;
|
||||
uint64_t jobid = 0;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
make_reasons(&in_a, 0, "host-a");
|
||||
make_reasons(&in_b, PREASON_BAD_RESREQ, "host-b");
|
||||
memset(&out, 0, sizeof(out));
|
||||
|
||||
init_primary_obj(&obj_a, &primary_a, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
/* Distinct key (differs only in identity) so this doesn't collide with
|
||||
* obj_a's own insert. */
|
||||
init_primary_obj(&obj_b, &primary_b, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, user_b,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
rc = cache_insert(&sched, &obj_a, 11);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
rc = cache_insert(&sched, &obj_b, 99);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj_a, &in_a, 11));
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj_b, &in_b, 99));
|
||||
|
||||
rc = cache_lookup(&sched, &obj_a, &out_a);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(11, out_a);
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj_a, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_UINT64(11, jobid);
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&in_a, &out, NULL));
|
||||
|
||||
rc = cache_lookup(&sched, &obj_b, &out_b);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(99, out_b);
|
||||
/* a hit replaces whatever `out` held: no merge with a's entries */
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj_b, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_UINT64(99, jobid);
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&in_b, &out, NULL));
|
||||
|
||||
fini_sched(&sched);
|
||||
erase_job_preason(&in_a);
|
||||
erase_job_preason(&in_b);
|
||||
erase_job_preason(&out);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* cache_clear() empties every entry: insert two distinct keys, clear, then
|
||||
* confirm BOTH now miss (not just the one inserted last).
|
||||
* bug-1932: the cache owns a deep copy. Erasing (freeing) the inserted
|
||||
* original, and erasing a previous lookup's output, must leave the stored
|
||||
* entry intact -- the original attempt's reason is freed when its decision
|
||||
* is published, long before a later job's lookup.
|
||||
*/
|
||||
static void test_entry_is_a_deep_copy(void)
|
||||
{
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_job_preason_t in, expect, out;
|
||||
uint64_t jobid = 0;
|
||||
|
||||
init_core(&core);
|
||||
make_reasons(&in, 0, "host1");
|
||||
make_reasons(&expect, 0, "host1");
|
||||
memset(&out, 0, sizeof(out));
|
||||
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj, &in, BASE_JOBID));
|
||||
erase_job_preason(&in);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj, &out, &jobid));
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&expect, &out, NULL));
|
||||
erase_job_preason(&out);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj, &out, &jobid));
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&expect, &out, NULL));
|
||||
|
||||
erase_job_preason(&expect);
|
||||
erase_job_preason(&out);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* A NULL reason (an attempt that pended without recording any) is stored
|
||||
* as an empty reason; a hit then reports it as empty, not as garbage.
|
||||
*/
|
||||
static void test_null_reasons_stored_as_empty(void)
|
||||
{
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_job_preason_t empty, out;
|
||||
uint64_t jobid = 0;
|
||||
|
||||
init_core(&core);
|
||||
memset(&empty, 0, sizeof(empty));
|
||||
make_reasons(&out, 0, "stale");
|
||||
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj, NULL, BASE_JOBID));
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj, &out, &jobid));
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&empty, &out, NULL));
|
||||
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* cache_clear() empties all entries (freeing them -- ASAN reports a leak
|
||||
* otherwise) and leaves a usable table.
|
||||
*/
|
||||
static void test_clear_empties_all_entries(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj_a, obj_b;
|
||||
xce_primary_schedobj_t primary_a, primary_b;
|
||||
static const char user_b[] = "dave";
|
||||
int out = -1;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj_a, obj_b;
|
||||
xce_primary_schedobj_t primary_a, primary_b;
|
||||
static const char user_b[] = "dave";
|
||||
xce_job_preason_t in, out;
|
||||
uint64_t jobid = 0;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
make_reasons(&in, 0, "host1");
|
||||
memset(&out, 0, sizeof(out));
|
||||
|
||||
init_primary_obj(&obj_a, &primary_a, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
|
|
@ -338,37 +432,42 @@ static void test_clear_empties_all_entries(void)
|
|||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, user_b,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&sched, &obj_a, 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&sched, &obj_b, 2));
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj_a, &in, 1));
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj_b, &in, 2));
|
||||
|
||||
/* Sanity: both are hits before clear. */
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&sched, &obj_a, &out));
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&sched, &obj_b, &out));
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj_a, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj_b, &out, &jobid));
|
||||
|
||||
cache_clear(&sched);
|
||||
TEST_ASSERT_NOT_NULL(sched.unsatisfiable_cache);
|
||||
cache_clear(&core);
|
||||
TEST_ASSERT_NOT_NULL(core.unsatisfiable_cache);
|
||||
|
||||
rc = cache_lookup(&sched, &obj_a, &out);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
rc = cache_lookup(&sched, &obj_b, &out);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_lookup(&core, &obj_a, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_lookup(&core, &obj_b, &out, &jobid));
|
||||
|
||||
fini_sched(&sched);
|
||||
erase_job_preason(&in);
|
||||
erase_job_preason(&out);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* Re-inserting the identical key without an intervening clear propagates
|
||||
* kvtab_add()'s own -EEXIST contract -- exercising cache_insert()'s error
|
||||
* path.
|
||||
* path, which must free the rejected copy (ASAN) and keep the first entry.
|
||||
*/
|
||||
static void test_insert_duplicate_key_returns_eexist(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj1, obj2;
|
||||
xce_primary_schedobj_t primary1, primary2;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj1, obj2;
|
||||
xce_primary_schedobj_t primary1, primary2;
|
||||
xce_job_preason_t in1, in2, out;
|
||||
uint64_t jobid = 0;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
make_reasons(&in1, 0, "host1");
|
||||
make_reasons(&in2, 0, "host2");
|
||||
memset(&out, 0, sizeof(out));
|
||||
|
||||
init_primary_obj(&obj1, &primary1, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
|
|
@ -377,26 +476,39 @@ static void test_insert_duplicate_key_returns_eexist(void)
|
|||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&sched, &obj1, 5));
|
||||
rc = cache_insert(&sched, &obj2, 6);
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_insert(&core, &obj1, &in1, 5));
|
||||
rc = cache_insert(&core, &obj2, &in2, 6);
|
||||
TEST_ASSERT_EQUAL_INT(-EEXIST, rc);
|
||||
|
||||
fini_sched(&sched);
|
||||
TEST_ASSERT_EQUAL_INT(1, cache_lookup(&core, &obj1, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_UINT64(5, jobid);
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&in1, &out, NULL));
|
||||
|
||||
erase_job_preason(&in1);
|
||||
erase_job_preason(&in2);
|
||||
erase_job_preason(&out);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/*
|
||||
* Defensive argument checking: NULL sched/obj/out_reason_category, and a
|
||||
* non-primary tag object, are all rejected rather than crashing.
|
||||
* Defensive argument checking: NULL core/obj/out/out_jobid, and a
|
||||
* non-primary tag object, are all rejected rather than crashing. A core
|
||||
* whose table was never built is a plain miss on lookup and an error on
|
||||
* insert.
|
||||
*/
|
||||
static void test_argument_validation(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_sched_obj_t non_primary_obj;
|
||||
int out = -1;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_core_t bare;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_sched_obj_t non_primary_obj;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 0;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
memset(&bare, 0, sizeof(bare));
|
||||
memset(&out, 0, sizeof(out));
|
||||
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
|
@ -404,35 +516,37 @@ static void test_argument_validation(void)
|
|||
memset(&non_primary_obj, 0, sizeof(non_primary_obj));
|
||||
non_primary_obj.tag = XCE_RESREQ_PRIMARY + 1; /* anything != PRIMARY */
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(NULL, &obj, &out));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&sched, NULL, &out));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&sched, &obj, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(NULL, &obj, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&core, NULL, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&core, &obj, NULL, &jobid));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_lookup(&core, &obj, &out, NULL));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
||||
cache_lookup(&sched, &non_primary_obj, &out));
|
||||
cache_lookup(&core, &non_primary_obj, &out, &jobid));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(NULL, &obj, 1));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(&sched, NULL, 1));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(&sched, &non_primary_obj, 1));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(NULL, &obj, NULL, 1));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(&core, NULL, NULL, 1));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL,
|
||||
cache_insert(&core, &non_primary_obj, NULL, 1));
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, cache_lookup(&bare, &obj, &out, &jobid));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, cache_insert(&bare, &obj, NULL, 1));
|
||||
|
||||
/* cache_clear(NULL) must not crash. */
|
||||
cache_clear(NULL);
|
||||
|
||||
fini_sched(&sched);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Task 1.1 (scheduler-sim-live-allocation spec, Req 5.2): the */
|
||||
/* unsatisfiable-cache callback seam. */
|
||||
/* The unsatisfiable-cache callback seam (scheduler-sim-live-allocation */
|
||||
/* Req 5.2; bug-1932: both drivers register it). */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* A freshly-zeroed xce_sched_core_t (the same memset() default
|
||||
* xce_sched_core_init() applies -- core.c is not linked into this
|
||||
* white-box TU, so this test zeroes a bare instance directly rather than
|
||||
* calling that function) leaves BOTH new callback fields NULL. This is
|
||||
* the exact state every simulation-mode xce_sched_core_t is in forever --
|
||||
* sim_loop.c never registers either callback (Req 5 AC 17 "Simulation
|
||||
* isolation").
|
||||
* white-box TU) leaves BOTH callback fields NULL: a core whose driver
|
||||
* registered no cache computes every attempt fresh.
|
||||
*/
|
||||
static void test_seam_fields_default_null_on_zeroed_core(void)
|
||||
{
|
||||
|
|
@ -442,116 +556,93 @@ static void test_seam_fields_default_null_on_zeroed_core(void)
|
|||
|
||||
TEST_ASSERT_NULL(core.unsatisfiable_lookup_cb);
|
||||
TEST_ASSERT_NULL(core.unsatisfiable_populate_cb);
|
||||
TEST_ASSERT_NULL(core.unsatisfiable_cache);
|
||||
}
|
||||
|
||||
/*
|
||||
* THE Observable this task's own TDD note names: "with both callbacks
|
||||
* left NULL, the allocation dispatch chokepoint always computes fresh
|
||||
* with no crash and no incorrect skip." The real chokepoint
|
||||
* (sched_make_primary_alloc_(), scheduler.c) is migrated to actually
|
||||
* consult this seam by a LATER task (tasks.md 2.2, boundary "Core-Safe
|
||||
* Allocation Chain, Unsatisfiable-Cache Callback Seam") -- this task adds
|
||||
* only the seam itself, not that call site. This test proves the seam's
|
||||
* own NULL-safety contract in isolation, using the identical NULL-check
|
||||
* shape design.md's own invariant specifies for that future call site
|
||||
* ("the allocation chain ... only ever NULL-checks and calls these"):
|
||||
* with both fields NULL, a NULL-checked consult never dereferences either
|
||||
* function pointer (no crash), is never mistaken for a cache HIT (no
|
||||
* incorrect skip -- `hit` stays 0, meaning "compute fresh"), and a
|
||||
* NULL-checked populate attempt is silently skipped, not a fatal error.
|
||||
* With both callbacks left NULL, a NULL-checked consult never dereferences
|
||||
* either function pointer (no crash), is never mistaken for a cache HIT
|
||||
* (no incorrect skip), and a NULL-checked populate attempt is silently
|
||||
* skipped -- the same NULL-check shape sched_make_primary_alloc_()
|
||||
* (scheduler.c) uses.
|
||||
*/
|
||||
static void test_null_callbacks_mean_always_compute_fresh(void)
|
||||
{
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
int category = -1;
|
||||
int hit = -1; /* poison: neither 0 nor 1 yet */
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 0;
|
||||
int hit = -1; /* poison: neither 0 nor 1 yet */
|
||||
|
||||
memset(&core, 0, sizeof(core));
|
||||
memset(&out, 0, sizeof(out));
|
||||
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
/* Prove the guard's PRECONDITION explicitly, immediately before relying
|
||||
* on it below -- not just that the end result happens to be "compute
|
||||
* fresh", but that the NULL-ness this test's two guarded calls depend
|
||||
* on genuinely holds at the exact point each guard is evaluated. A
|
||||
* design that accidentally called through a non-NULL stale/garbage
|
||||
* pointer here (e.g. an uninitialized field instead of a true NULL)
|
||||
* could still coincidentally return 0/not crash on some platforms;
|
||||
* asserting NULL directly, right where each guard fires, rules that
|
||||
* out. */
|
||||
TEST_ASSERT_NULL(core.unsatisfiable_lookup_cb);
|
||||
if (core.unsatisfiable_lookup_cb != NULL) {
|
||||
hit = core.unsatisfiable_lookup_cb(&core, &obj, &category);
|
||||
hit = core.unsatisfiable_lookup_cb(&core, &obj, &out, &jobid);
|
||||
} else {
|
||||
hit = 0; /* no cache registered => always a miss => compute fresh */
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(0, hit);
|
||||
|
||||
/* A NULL-checked populate attempt must be a true no-op, not a crash --
|
||||
* same "assert the guard's own precondition, right at the guard"
|
||||
* discipline as the lookup check just above. */
|
||||
TEST_ASSERT_NULL(core.unsatisfiable_populate_cb);
|
||||
if (core.unsatisfiable_populate_cb != NULL) {
|
||||
core.unsatisfiable_populate_cb(&core, &obj, 7);
|
||||
core.unsatisfiable_populate_cb(&core, &obj, NULL, 7);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Normal-mode observable (this task's own note): "a normal-mode test
|
||||
* confirming the cache is still consulted/populated exactly as before
|
||||
* this change." xce_alloc_cache_lookup_cb()/xce_alloc_cache_populate_cb()
|
||||
* (alloc_cache.c) are the two adapters main.c registers on the real
|
||||
* xce_scheduler_t's core (xce_sched_core_t core is xce_scheduler_t's
|
||||
* first member, scheduler.h -- the same pointer-to-first-member idiom
|
||||
* xce_alloc_cache_clear_cb() already uses). This test calls them through
|
||||
* core.unsatisfiable_lookup_cb/unsatisfiable_populate_cb exactly as a
|
||||
* normal-mode driver would, and confirms the result is identical to
|
||||
* calling cache_lookup()/cache_insert() directly -- proving the adapters
|
||||
* are a transparent passthrough, not a behavior change.
|
||||
* xce_alloc_cache_lookup_cb()/xce_alloc_cache_populate_cb() (alloc_cache.c)
|
||||
* are the adapters both drivers register. Called through the core's seam
|
||||
* exactly as sched_make_primary_alloc_() does, they must be a transparent
|
||||
* passthrough to cache_lookup()/cache_insert() on that same core -- no
|
||||
* cast to a driver struct, so they work on any core (bug-1932).
|
||||
*/
|
||||
static void test_lookup_and_populate_cb_adapters_match_direct_cache_calls(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
int out_direct = -1;
|
||||
int out_via_cb = -1;
|
||||
int rc;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
xce_job_preason_t in, out_direct, out_via_cb;
|
||||
uint64_t jobid = 0;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
sched.core.unsatisfiable_lookup_cb = xce_alloc_cache_lookup_cb;
|
||||
sched.core.unsatisfiable_populate_cb = xce_alloc_cache_populate_cb;
|
||||
init_core(&core);
|
||||
core.unsatisfiable_lookup_cb = xce_alloc_cache_lookup_cb;
|
||||
core.unsatisfiable_populate_cb = xce_alloc_cache_populate_cb;
|
||||
make_reasons(&in, 0, "host1");
|
||||
memset(&out_direct, 0, sizeof(out_direct));
|
||||
memset(&out_via_cb, 0, sizeof(out_via_cb));
|
||||
|
||||
init_primary_obj(&obj, &primary, BASE_MIN_TASKS, BASE_MAX_TASKS,
|
||||
BASE_SELECT_NODE_ID, BASE_RUSAGE_HASH, BASE_USER,
|
||||
BASE_QUEUE, BASE_PROJECT);
|
||||
|
||||
/* Miss before any insert, via the callback -- same as a direct
|
||||
* cache_lookup() call (test_lookup_before_any_insert_is_a_miss
|
||||
* above). */
|
||||
TEST_ASSERT_NOT_NULL(sched.core.unsatisfiable_lookup_cb);
|
||||
rc = sched.core.unsatisfiable_lookup_cb(&(sched.core), &obj, &out_via_cb);
|
||||
rc = core.unsatisfiable_lookup_cb(&core, &obj, &out_via_cb, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Populate via the callback... */
|
||||
TEST_ASSERT_NOT_NULL(sched.core.unsatisfiable_populate_cb);
|
||||
sched.core.unsatisfiable_populate_cb(&(sched.core), &obj, 55);
|
||||
core.unsatisfiable_populate_cb(&core, &obj, &in, 55);
|
||||
|
||||
/* ...then confirm a DIRECT cache_lookup() call sees the SAME entry --
|
||||
* the adapter genuinely populated the real cache, not a shadow copy. */
|
||||
rc = cache_lookup(&sched, &obj, &out_direct);
|
||||
/* the adapter populated the real cache, not a shadow copy */
|
||||
rc = cache_lookup(&core, &obj, &out_direct, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(55, out_direct);
|
||||
TEST_ASSERT_EQUAL_UINT64(55, jobid);
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&in, &out_direct, NULL));
|
||||
|
||||
/* And the callback-based lookup now also reports the same hit. */
|
||||
rc = sched.core.unsatisfiable_lookup_cb(&(sched.core), &obj, &out_via_cb);
|
||||
jobid = 0;
|
||||
rc = core.unsatisfiable_lookup_cb(&core, &obj, &out_via_cb, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(55, out_via_cb);
|
||||
TEST_ASSERT_EQUAL_UINT64(55, jobid);
|
||||
TEST_ASSERT_TRUE(job_preason_eq(&in, &out_via_cb, NULL));
|
||||
|
||||
fini_sched(&sched);
|
||||
erase_job_preason(&in);
|
||||
erase_job_preason(&out_direct);
|
||||
erase_job_preason(&out_via_cb);
|
||||
fini_core(&core);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
|
|
@ -566,7 +657,9 @@ int main(void)
|
|||
RUN_TEST(test_miss_on_differing_user);
|
||||
RUN_TEST(test_miss_on_differing_queue);
|
||||
RUN_TEST(test_miss_on_differing_project);
|
||||
RUN_TEST(test_reason_category_round_trip_two_distinct_categories);
|
||||
RUN_TEST(test_two_entries_keep_their_own_reasons_and_jobs);
|
||||
RUN_TEST(test_entry_is_a_deep_copy);
|
||||
RUN_TEST(test_null_reasons_stored_as_empty);
|
||||
RUN_TEST(test_clear_empties_all_entries);
|
||||
RUN_TEST(test_insert_duplicate_key_returns_eexist);
|
||||
RUN_TEST(test_argument_validation);
|
||||
|
|
|
|||
|
|
@ -18,19 +18,19 @@
|
|||
* if non-NULL, and this fixture explicitly registers the REAL production
|
||||
* adapter (alloc_cache.c's xce_alloc_cache_clear_cb() -- see that
|
||||
* function's own doc comment for why it lives in alloc_cache.c rather
|
||||
* than driver.c) on init_sched()'s xce_scheduler_t, the same call main.c
|
||||
* makes in production.
|
||||
* than driver.c) on init_core()'s core, the same call both drivers make
|
||||
* (bug-1932).
|
||||
*
|
||||
* This file still does NOT use the white-box #include convention most of
|
||||
* this test suite uses for core_session.c; its meson.build entry still
|
||||
* lists core.c/core_clock.c/core_session.c/decision.c/alloc_cache.c as
|
||||
* genuinely separate compiled sources so this fixture links against the
|
||||
* REAL cache_clear(xce_scheduler_t*) (alloc_cache.c) that its registered
|
||||
* REAL cache_clear() (alloc_cache.c) that its registered
|
||||
* hook calls into -- not a stub.
|
||||
*
|
||||
* ── What this proves ─────────────────────────────────────────────────────
|
||||
* A job's fingerprint+identity is inserted into a REAL, live
|
||||
* xce_scheduler_t.unsatisfiable_cache (cache_insert(), simulating what
|
||||
* core->unsatisfiable_cache (cache_insert(), simulating what
|
||||
* task 5.1's allocator-exhaustion path would have done); a lookup for the
|
||||
* identical key is confirmed a HIT; the REAL xce_core_begin_session()
|
||||
* (core_session.c's public entry point, exercising the real
|
||||
|
|
@ -87,36 +87,28 @@ void tearDown(void) { return; }
|
|||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* init_sched - a real xce_scheduler_t with BOTH core (xce_sched_core_init(),
|
||||
* core.c) and unsatisfiable_cache (mirroring sched_init()'s own
|
||||
* construction call, scheduler.c, and test_alloc_cache.c's own
|
||||
* init_sched()) constructed -- everything core_begin_session_internal()
|
||||
* touches, without sched_init()'s heavier networking/threading setup
|
||||
* (this test never calls sched_init() at all). Also registers the REAL
|
||||
* production cache_clear_cb (round 2, xce_alloc_cache_clear_cb() --
|
||||
* alloc_cache.c, linked into this exact test binary, see this file's own
|
||||
* header comment) -- the same one-time-at-startup registration main.c
|
||||
* performs in production (xce_core_set_cache_clear_cb(), right alongside
|
||||
* xce_core_set_decision_cb()), so this fixture's own registration is not
|
||||
* a stand-in wrapper but the identical call production makes.
|
||||
* init_core - a real core (xce_sched_core_init(), core.c) with the REAL
|
||||
* production cache_clear_cb registered (xce_alloc_cache_clear_cb(),
|
||||
* alloc_cache.c, linked into this exact test binary) -- the same
|
||||
* one-time-at-startup registration both drivers make (main.c for normal
|
||||
* mode, sched_sim_driver_start() for simulation, bug-1932). The cache
|
||||
* table lives on the core (bug-1932); cache_clear() builds it here, the
|
||||
* same call the hook makes at every session start.
|
||||
*/
|
||||
static void init_sched(xce_scheduler_t* sched)
|
||||
static void init_core(xce_sched_core_t* core)
|
||||
{
|
||||
kv_params_t params = { .nslots = XCE_UNSAT_CACHE_NSLOTS,
|
||||
.keyops = &cache_key_ops,
|
||||
.datakiller = NULL };
|
||||
int rc;
|
||||
int rc;
|
||||
|
||||
memset(sched, 0, sizeof(*sched));
|
||||
memset(core, 0, sizeof(*core));
|
||||
|
||||
rc = xce_sched_core_init(&(sched->core));
|
||||
rc = xce_sched_core_init(core);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_core_set_cache_clear_cb(&(sched->core), xce_alloc_cache_clear_cb);
|
||||
rc = xce_core_set_cache_clear_cb(core, xce_alloc_cache_clear_cb);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
sched->unsatisfiable_cache = makeInstance(&kvtab_type, ¶ms);
|
||||
TEST_ASSERT_NOT_NULL(sched->unsatisfiable_cache);
|
||||
cache_clear(core);
|
||||
TEST_ASSERT_NOT_NULL(core->unsatisfiable_cache);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -157,40 +149,40 @@ static void init_primary_obj(xce_sched_obj_t* obj,
|
|||
*/
|
||||
static void test_begin_session_clears_previously_cached_entry(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
int category = 7;
|
||||
int out = -1;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 0;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
memset(&out, 0, sizeof(out));
|
||||
init_primary_obj(&obj, &primary, 1, 4, 100u, 0xdeadbeefULL, "alice",
|
||||
"normal", "proj1");
|
||||
|
||||
rc = cache_insert(&sched, &obj, category);
|
||||
rc = cache_insert(&core, &obj, NULL, 7);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Baseline: proves the fixture/insert are sound before relying on them
|
||||
* below -- same "prove the harness works first" discipline
|
||||
* test_core_session.c's own worker-reset tests already follow. */
|
||||
rc = cache_lookup(&sched, &obj, &out);
|
||||
rc = cache_lookup(&core, &obj, &out, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(category, out);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, jobid);
|
||||
|
||||
rc = xce_core_begin_session(&(sched.core));
|
||||
rc = xce_core_begin_session(&core);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
out = -1;
|
||||
rc = cache_lookup(&sched, &obj, &out);
|
||||
rc = cache_lookup(&core, &obj, &out, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc,
|
||||
"Req 7.2: a job cached as unsatisfiable in one session must be "
|
||||
"re-evaluated fresh (a cache MISS), not immediately pended from "
|
||||
"the cache, in the next session -- core_begin_session_internal() "
|
||||
"must clear xce_scheduler_t.unsatisfiable_cache");
|
||||
"must clear the core's unsatisfiable_cache");
|
||||
|
||||
rc = xce_core_end_session(&(sched.core));
|
||||
rc = xce_core_end_session(&core);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
}
|
||||
|
||||
|
|
@ -203,28 +195,30 @@ static void test_begin_session_clears_previously_cached_entry(void)
|
|||
*/
|
||||
static void test_cache_still_usable_after_begin_session_clear(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_sched_core_t core;
|
||||
xce_sched_obj_t obj;
|
||||
xce_primary_schedobj_t primary;
|
||||
int out = -1;
|
||||
xce_job_preason_t out;
|
||||
uint64_t jobid = 0;
|
||||
int rc;
|
||||
|
||||
init_sched(&sched);
|
||||
init_core(&core);
|
||||
memset(&out, 0, sizeof(out));
|
||||
|
||||
rc = xce_core_begin_session(&(sched.core));
|
||||
rc = xce_core_begin_session(&core);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
init_primary_obj(&obj, &primary, 2, 8, 200u, 0xfeedfaceULL, "bob", "batch",
|
||||
"proj2");
|
||||
|
||||
rc = cache_insert(&sched, &obj, 3);
|
||||
rc = cache_insert(&core, &obj, NULL, 3);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = cache_lookup(&sched, &obj, &out);
|
||||
rc = cache_lookup(&core, &obj, &out, &jobid);
|
||||
TEST_ASSERT_EQUAL_INT(1, rc);
|
||||
TEST_ASSERT_EQUAL_INT(3, out);
|
||||
TEST_ASSERT_EQUAL_UINT64(3, jobid);
|
||||
|
||||
rc = xce_core_end_session(&(sched.core));
|
||||
rc = xce_core_end_session(&core);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1065,26 +1065,28 @@ test_resource_request_update_never_changes_dispatched_task_count(void)
|
|||
static void
|
||||
test_differing_task_counts_do_not_collide_in_unsatisfiable_cache(void)
|
||||
{
|
||||
xce_scheduler_t sched;
|
||||
xce_queue_t* queue;
|
||||
xce_core_event_t queue_event;
|
||||
xce_job_t* job_a;
|
||||
xce_job_t* job_b;
|
||||
xce_job_t* job_ptrs[2];
|
||||
xce_core_event_t job_event;
|
||||
xce_sched_job_t* sjob_a;
|
||||
xce_sched_job_t* sjob_b;
|
||||
int out_reason = -1;
|
||||
int rc;
|
||||
xce_scheduler_t sched;
|
||||
xce_queue_t* queue;
|
||||
xce_core_event_t queue_event;
|
||||
xce_job_t* job_a;
|
||||
xce_job_t* job_b;
|
||||
xce_job_t* job_ptrs[2];
|
||||
xce_core_event_t job_event;
|
||||
xce_sched_job_t* sjob_a;
|
||||
xce_sched_job_t* sjob_b;
|
||||
xce_job_preason_t out_reason;
|
||||
uint64_t out_jobid = 0;
|
||||
int rc;
|
||||
|
||||
memset(&out_reason, 0, sizeof(out_reason));
|
||||
memset(&sched, 0, sizeof(sched));
|
||||
rc = xce_sched_core_init(&(sched.core));
|
||||
TEST_ASSERT_TRUE_MESSAGE(rc >= 0, "fixture: xce_sched_core_init() must "
|
||||
"succeed");
|
||||
|
||||
cache_clear(&sched);
|
||||
cache_clear(&(sched.core));
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
sched.unsatisfiable_cache,
|
||||
sched.core.unsatisfiable_cache,
|
||||
"fixture: cache_clear() must construct a real unsatisfiable_cache "
|
||||
"table (mirrors the real per-session construction, main.c's "
|
||||
"xce_core_set_cache_clear_cb() registration)");
|
||||
|
|
@ -1147,24 +1149,26 @@ test_differing_task_counts_do_not_collide_in_unsatisfiable_cache(void)
|
|||
|
||||
/* ── The Req 5.4 proof: an unsatisfiable outcome cached for job A
|
||||
* must NOT produce a cache hit for job B. ──────────────────────────── */
|
||||
rc = cache_insert(&sched, sjob_a->schedobj, 7 /* arbitrary category */);
|
||||
rc = cache_insert(&(sched.core), sjob_a->schedobj, NULL,
|
||||
7 /* arbitrary job */);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc, "fixture: cache_insert() for job A must succeed");
|
||||
|
||||
rc = cache_lookup(&sched, sjob_b->schedobj, &out_reason);
|
||||
rc = cache_lookup(&(sched.core), sjob_b->schedobj, &out_reason, &out_jobid);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
0, rc,
|
||||
"Req 5.4: job B (a different requested task count, otherwise "
|
||||
"identical) must NOT hit job A's cached unsatisfiable outcome");
|
||||
|
||||
/* Sanity: the cache mechanism itself genuinely works -- job A's OWN
|
||||
* key IS a hit, with the SAME reason category given at insert time.
|
||||
* key IS a hit, with the SAME job given at insert time.
|
||||
* Without this, the miss above could just as easily mean the cache is
|
||||
* dead altogether, not that it correctly distinguishes task counts. */
|
||||
rc = cache_lookup(&sched, sjob_a->schedobj, &out_reason);
|
||||
rc = cache_lookup(&(sched.core), sjob_a->schedobj, &out_reason, &out_jobid);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
1, rc, "sanity: job A's own key must hit its own cached entry");
|
||||
TEST_ASSERT_EQUAL_INT(7, out_reason);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, out_jobid);
|
||||
erase_job_preason(&out_reason);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@
|
|||
* - extra.replay, generated per run, for what the baseline lacks: a
|
||||
* satisfied compound requirement (201), a compound requirement that
|
||||
* fails at its first member (202), a job array with two elements
|
||||
* (0-0-0-32768[1] and [2]) and a host-selection mismatch (203 selects
|
||||
* mem>20000, which only w02 has).
|
||||
* (0-0-0-32768[1] and [2]), a host-selection mismatch (203 selects
|
||||
* mem>20000, which only w02 has) and two identical unsatisfiable jobs
|
||||
* (204, 205; bug-1932).
|
||||
*
|
||||
* `break DECISION jobid=105` (a raw packed decimal id) names 0-0-0-105;
|
||||
* since bug-1926 the dashed form 0-0-0-105 names it too.
|
||||
|
|
@ -42,6 +43,10 @@
|
|||
* selection mismatch, host-limit rejection, shortfall next to a limit,
|
||||
* `info trace` for an array, an element and an unknown job, and the
|
||||
* file equal to the inline console traces.
|
||||
* 4. bug-1932 (extra replay): 205's request equals 204's, which a full
|
||||
* search proved unsatisfiable earlier in the same session; 205 pends
|
||||
* from the unsatisfiable-request cache with no search, its trace names
|
||||
* 204, and it pends with 204's full reasons (per-host included).
|
||||
*/
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -665,11 +670,68 @@ static void test_compound_array_selection_and_info_trace(void)
|
|||
run_done(&r);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 4. A request already proved unsatisfiable this session (bug-1932) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/* The outcome section of a trace block: from its " outcome: " line on. */
|
||||
static const char* outcome_of(const char* block)
|
||||
{
|
||||
const char* o = strstr(block, "\n outcome: ");
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(o, block);
|
||||
return o;
|
||||
}
|
||||
|
||||
static void test_known_unsatisfiable_request_skipped_with_full_reasons(void)
|
||||
{
|
||||
run_t r;
|
||||
char* b204;
|
||||
char* b205;
|
||||
|
||||
run_repl(&r, EXTRA_REPLAY,
|
||||
"trace on job 0-0-0-204 0-0-0-205 level full\n"
|
||||
"step\n"
|
||||
"step\n"
|
||||
"step\n"
|
||||
"quit\n");
|
||||
|
||||
/* Session 3: nothing else runs, so 204 passes the cluster-wide check
|
||||
* and every host rejects it for mem: the reason is per-host only. */
|
||||
b204 = block_of(r.out, "[trace] job 0-0-0-204 session 3");
|
||||
IN_ORDER(b204, "\n cluster-wide check passed\n",
|
||||
"\n host w01 rejected (per-host check): resource "
|
||||
"shortfall\n",
|
||||
"\n host w02 rejected (per-host check): resource "
|
||||
"shortfall\n",
|
||||
"\n outcome: PEND\n", "\n host w01: ");
|
||||
|
||||
/* 205: no check and no plan, the trace names the job that proved the
|
||||
* request unsatisfiable, and the pend reasons are 204's, in full. */
|
||||
b205 = block_of(r.out, "[trace] job 0-0-0-205 session 3");
|
||||
IN_ORDER(b205,
|
||||
"\n no plan generated: skipped, same request as 0-0-0-204 "
|
||||
"proved unsatisfiable earlier this session\n",
|
||||
"\n outcome: PEND\n");
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(b205, "cluster-wide check"), b205);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(b205, "\n cluster-wide:"), b205);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(b205, " plan 1 "), b205);
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE(
|
||||
outcome_of(b204), outcome_of(b205),
|
||||
"bug-1932: a cache hit must pend with the full reasons of the job "
|
||||
"that proved the request unsatisfiable");
|
||||
|
||||
free(b204);
|
||||
free(b205);
|
||||
run_done(&r);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_step_mode_full_all_jobs_and_file_copy);
|
||||
RUN_TEST(test_continue_breakpoint_summary_and_info_trace);
|
||||
RUN_TEST(test_compound_array_selection_and_info_trace);
|
||||
RUN_TEST(test_known_unsatisfiable_request_skipped_with_full_reasons);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,6 +386,9 @@ static uint64_t array_elem(uint32_t k)
|
|||
* 203 selects mem>20000 (w01 mismatches), pended by the
|
||||
* cluster-wide limit (6 of 6 in use)
|
||||
* 3000 after 201 and both elements finish: 202 and 203 are tried again
|
||||
* 4000 after 202 and 203 finish: 204 and 205 (identical requests, more
|
||||
* mem than any host has; bug-1932) pend in every session -- 204
|
||||
* by a full search, 205 by the unsatisfiable-request cache
|
||||
*/
|
||||
void write_extra_replay(const char* dest)
|
||||
{
|
||||
|
|
@ -393,7 +396,7 @@ void write_extra_replay(const char* dest)
|
|||
char path[PATH_MAX];
|
||||
FILE* f;
|
||||
xce_worker_t* workers[2];
|
||||
xce_job_t* jobs[5];
|
||||
xce_job_t* jobs[7];
|
||||
|
||||
snprintf(record_dir, sizeof(record_dir), "/tmp/sched_tre2e_gen_XXXXXX");
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(record_dir));
|
||||
|
|
@ -420,7 +423,9 @@ void write_extra_replay(const char* dest)
|
|||
1, 1);
|
||||
jobs[4] = make_push_job(203, 1104, "select[mem>20000] rusage[mem=512]",
|
||||
"alice", 1, 1);
|
||||
append_jobs(f, 1003ULL, jobs, 5);
|
||||
jobs[5] = make_push_job(204, 1105, "rusage[mem=999999]", "carol", 1, 1);
|
||||
jobs[6] = make_push_job(205, 1106, "rusage[mem=999999]", "carol", 1, 1);
|
||||
append_jobs(f, 1003ULL, jobs, 7);
|
||||
|
||||
append_session(f, 2000ULL);
|
||||
append_state_change(f, 2500ULL, 201, JOB_STAT_RUN, JOB_STAT_DONE);
|
||||
|
|
@ -429,6 +434,9 @@ void write_extra_replay(const char* dest)
|
|||
append_state_change(f, 2700ULL, array_elem(2), JOB_STAT_RUN,
|
||||
JOB_STAT_DONE);
|
||||
append_session(f, 3000ULL);
|
||||
append_state_change(f, 3500ULL, 202, JOB_STAT_RUN, JOB_STAT_DONE);
|
||||
append_state_change(f, 3600ULL, 203, JOB_STAT_RUN, JOB_STAT_DONE);
|
||||
append_session(f, 4000ULL);
|
||||
|
||||
append_end_of_stream(f);
|
||||
TEST_ASSERT_EQUAL_INT(0, fclose(f));
|
||||
|
|
|
|||
|
|
@ -173,22 +173,24 @@ int __wrap_sched_meet_host_selection(xce_sched_core_t* core,
|
|||
static int g_lookup_cb_calls = 0;
|
||||
static int g_populate_cb_calls = 0;
|
||||
|
||||
static int spy_unsatisfiable_lookup_cb(xce_sched_core_t* core,
|
||||
static int spy_unsatisfiable_lookup_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int* out_reason_category)
|
||||
xce_job_preason_t* out_reasons,
|
||||
uint64_t* out_jobid)
|
||||
{
|
||||
g_lookup_cb_calls++;
|
||||
|
||||
return xce_alloc_cache_lookup_cb(core, obj, out_reason_category);
|
||||
return xce_alloc_cache_lookup_cb(core, obj, out_reasons, out_jobid);
|
||||
}
|
||||
|
||||
static void spy_unsatisfiable_populate_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int reason_category)
|
||||
static void spy_unsatisfiable_populate_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
const xce_job_preason_t* reasons,
|
||||
uint64_t jobid)
|
||||
{
|
||||
g_populate_cb_calls++;
|
||||
|
||||
xce_alloc_cache_populate_cb(core, obj, reason_category);
|
||||
xce_alloc_cache_populate_cb(core, obj, reasons, jobid);
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
|
@ -451,6 +453,21 @@ static void test_standalone_primary_cache_hit_skips_allocator(void)
|
|||
decision2->u.reason->reasons->main_reason,
|
||||
"a cache hit must attribute the SAME pend reason category as the "
|
||||
"original unsatisfiable determination (Req 7.5)");
|
||||
/* bug-1932: job 1's real cause is per-host (the worker's slots
|
||||
* shortfall), with main reason 0; a hit that keeps only the main
|
||||
* reason pends job 2 with an EMPTY reason. */
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
(decision1->u.reason->reasons->nhost_reasons
|
||||
+ decision1->u.reason->reasons->nres_reasons)
|
||||
> 0,
|
||||
"fixture: job 1's exhausted attempt must record a per-host or "
|
||||
"per-resource reason (its one worker has no free slots)");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
job_preason_eq(decision1->u.reason->reasons,
|
||||
decision2->u.reason->reasons, NULL),
|
||||
"bug-1932: a cache hit must restore the original attempt's FULL "
|
||||
"pend reason (main, per-host and per-resource), not only its main "
|
||||
"reason");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
2, g_lookup_cb_calls,
|
||||
"job 2's cache hit must also be consulted through core->"
|
||||
|
|
|
|||
|
|
@ -132,22 +132,24 @@ int __wrap_sched_meet_host_selection(xce_sched_core_t* core,
|
|||
static int g_lookup_cb_calls = 0;
|
||||
static int g_populate_cb_calls = 0;
|
||||
|
||||
static int spy_unsatisfiable_lookup_cb(xce_sched_core_t* core,
|
||||
static int spy_unsatisfiable_lookup_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int* out_reason_category)
|
||||
xce_job_preason_t* out_reasons,
|
||||
uint64_t* out_jobid)
|
||||
{
|
||||
g_lookup_cb_calls++;
|
||||
|
||||
return xce_alloc_cache_lookup_cb(core, obj, out_reason_category);
|
||||
return xce_alloc_cache_lookup_cb(core, obj, out_reasons, out_jobid);
|
||||
}
|
||||
|
||||
static void spy_unsatisfiable_populate_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
int reason_category)
|
||||
static void spy_unsatisfiable_populate_cb(xce_sched_core_t* core,
|
||||
xce_sched_obj_t* obj,
|
||||
const xce_job_preason_t* reasons,
|
||||
uint64_t jobid)
|
||||
{
|
||||
g_populate_cb_calls++;
|
||||
|
||||
xce_alloc_cache_populate_cb(core, obj, reason_category);
|
||||
xce_alloc_cache_populate_cb(core, obj, reasons, jobid);
|
||||
}
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
|
@ -412,6 +414,11 @@ static void test_compound_nested_primary_cache_hit_skips_allocator(void)
|
|||
"a cache hit must attribute the SAME pend reason category as the "
|
||||
"original unsatisfiable determination (Req 7.5), even across a "
|
||||
"compound-nested primary");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
job_preason_eq(decisionA->u.reason->reasons,
|
||||
decisionB->u.reason->reasons, NULL),
|
||||
"bug-1932: a cache hit must restore the original attempt's FULL "
|
||||
"pend reason, even across a compound-nested primary");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
2, g_lookup_cb_calls,
|
||||
"job B's cache hit must also be consulted through core->"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@
|
|||
* REAL xce_core_begin_session() (core_session.c's public entry point),
|
||||
* and asserts alt_events_file is bit-for-bit unchanged across the call.
|
||||
*
|
||||
* bug-1932 update: the table now lives on xce_sched_core_t and the
|
||||
* simulator registers the same hook as normal mode (from its
|
||||
* sched_sim_args_t). The NULL-hook test below still guards the no-hook
|
||||
* path; test_sim_registered_cache_clear_uses_the_core_table guards the
|
||||
* registered one: clearing builds the core's table and leaves
|
||||
* alt_events_file alone.
|
||||
*
|
||||
* ── Link technique ───────────────────────────────────────────────────────
|
||||
* Same technique as the sibling test_core_session_cache_clear.c (round 1):
|
||||
* this fixture's meson.build entry lists core.c/core_clock.c/
|
||||
|
|
@ -107,10 +114,9 @@ static void test_sim_begin_session_never_corrupts_alt_events_file(void)
|
|||
TEST_ASSERT_NOT_NULL(f);
|
||||
sim.alt_events_file = f;
|
||||
|
||||
/* Deliberately NOT calling xce_core_set_cache_clear_cb() here --
|
||||
* mirrors sim_loop.c exactly, which never registers this hook (Req 5
|
||||
* AC 17 "Simulation isolation": sim mode has no cache to clear). This
|
||||
* is the crux of the regression: before round 2's fix,
|
||||
/* Deliberately NOT calling xce_core_set_cache_clear_cb() here -- the
|
||||
* no-hook path (a core whose driver registered no cache). This is the
|
||||
* crux of the regression: before round 2's fix,
|
||||
* core_begin_session_internal() called cache_clear() unconditionally,
|
||||
* so linking alloc_cache.c's real, strong cache_clear(xce_scheduler_t*)
|
||||
* into this binary was enough to corrupt alt_events_file below even
|
||||
|
|
@ -132,9 +138,53 @@ static void test_sim_begin_session_never_corrupts_alt_events_file(void)
|
|||
fclose(f);
|
||||
}
|
||||
|
||||
/*
|
||||
* bug-1932: the simulator now registers the SAME cache hooks as normal mode
|
||||
* (sched_sim_driver_start(), from the driver's sched_sim_args_t). That is
|
||||
* safe only because the cache table lives on xce_sched_core_t and no hook
|
||||
* casts the core to a driver struct: clearing at session start must build
|
||||
* the core's own table and still leave every sim-only field alone.
|
||||
*/
|
||||
static void test_sim_registered_cache_clear_uses_the_core_table(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
FILE* f;
|
||||
int rc;
|
||||
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
rc = xce_sched_core_init(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
f = tmpfile();
|
||||
TEST_ASSERT_NOT_NULL(f);
|
||||
sim.alt_events_file = f;
|
||||
|
||||
rc = xce_core_set_cache_clear_cb(&(sim.core), xce_alloc_cache_clear_cb);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_core_begin_session(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_PTR_MESSAGE(
|
||||
f, sim.alt_events_file,
|
||||
"the registered cache_clear_cb must clear the core's own table, "
|
||||
"never reinterpret a simulation driver field");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
sim.core.unsatisfiable_cache,
|
||||
"bug-1932: a session start must build the core's cache table");
|
||||
|
||||
rc = xce_core_end_session(&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
fclose(f);
|
||||
/* core fini needs the driver-owned pend_updates queues this fixture
|
||||
* never builds; free the one thing this test made */
|
||||
destroy(sim.core.unsatisfiable_cache);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_sim_begin_session_never_corrupts_alt_events_file);
|
||||
RUN_TEST(test_sim_registered_cache_clear_uses_the_core_table);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -733,11 +733,8 @@ static void test_pend_decisions_differing_main_reason_mismatch(void)
|
|||
/*
|
||||
* test_pend_decisions_differing_reason_detail_still_match - bug-1845
|
||||
* regression: the recorded (expected) side represents a live cache-hit
|
||||
* PEND (core.h's documented "simulation isolation" -- sim/replay never
|
||||
* registers core->unsatisfiable_lookup_cb, so it can never itself produce
|
||||
* this shape, only ever receive it from a recorded normal-mode run) --
|
||||
* main_reason set, zero host/resource reason detail
|
||||
* (sched_pend_from_cached_reason_(), scheduler.c, never adds any). The
|
||||
* PEND recorded before bug-1932 -- main_reason set, zero host/resource
|
||||
* reason detail (the pre-fix cache stored only the main reason). The
|
||||
* live (actual) side represents replay's own full per-host computation for
|
||||
* the SAME job/round -- same main_reason, but with real host- and
|
||||
* resource-based reason detail attached (sched_try_alloc_on_host_(),
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,51 @@ static void test_no_decision_pend_without_reason_keeps_hierarchy(void)
|
|||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
/* bug-1932: a request the unsatisfiable-request cache already holds is
|
||||
* skipped; the line names the job whose search proved it, or says so
|
||||
* without a name when that job is unknown (0). */
|
||||
static void test_known_unsatisfiable_names_source_job(void)
|
||||
{
|
||||
static const char* const rn[] = { "slots" };
|
||||
static const float rv[] = { 1.0f };
|
||||
xce_sched_trace_rec_t* rec;
|
||||
fx_primary_t p;
|
||||
char* out;
|
||||
|
||||
rec = new_rec(XCE_TRACE_LEVEL_FULL, XCE_TRACE_LINE_BOUND_DEFAULT,
|
||||
XCE_TRACE_REC_DECISION);
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
set_selection(rec);
|
||||
set_glimits(rec, 0, NULL, NULL);
|
||||
make_primary(&p, 1, 1, 1, rn, rv, NULL);
|
||||
|
||||
sched_trace_path_push(rec, XCE_TRACE_PATH_SINGLE, 1, 1);
|
||||
sched_trace_resreq_begin(rec, &p.p, 0);
|
||||
/* the hit returns before the plan search: no search end follows */
|
||||
sched_trace_known_unsat(rec, (uint64_t)204);
|
||||
sched_trace_known_unsat(rec, 0);
|
||||
sched_trace_resreq_end(rec, 0, 0);
|
||||
sched_trace_path_pop(rec);
|
||||
sched_trace_rec_finish(rec, XCE_TRACE_REC_PEND_NO_REASON, 0);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, rec->counters.reqs_none_generated);
|
||||
|
||||
out = render(rec, XCE_REPL_UNITS_RAW);
|
||||
assert_golden(
|
||||
"[trace] job 1-2-1000-42 session 7 position 3 level full\n"
|
||||
SEL_LINE NO_LIMITS_LINE
|
||||
" requirement single tasks 1..1 requests slots=1\n"
|
||||
" no plan generated: skipped, same request as 0-0-0-204 proved "
|
||||
"unsatisfiable earlier this session\n"
|
||||
" no plan generated: skipped, same request proved unsatisfiable "
|
||||
"earlier this session\n"
|
||||
" single requirement not satisfied\n"
|
||||
" no decision: job pended without any pend reason\n",
|
||||
out);
|
||||
free(out);
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_reconstructed_record(void)
|
||||
{
|
||||
xce_sched_trace_rec_t* rec;
|
||||
|
|
@ -1318,6 +1363,7 @@ int main(void)
|
|||
RUN_TEST(test_no_decision_no_schedobj);
|
||||
RUN_TEST(test_no_decision_internal_error_keeps_hierarchy);
|
||||
RUN_TEST(test_no_decision_pend_without_reason_keeps_hierarchy);
|
||||
RUN_TEST(test_known_unsatisfiable_names_source_job);
|
||||
RUN_TEST(test_reconstructed_record);
|
||||
RUN_TEST(test_selection_and_limits_unavailable);
|
||||
RUN_TEST(test_entry_kinds_and_edge_cases);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue