fix(scheduler): route sim reconciliation's on_dispatch() through the allocator thread (bug-1837)
sim_reconcile_unaccounted_jobs() (sim_loop.c) was calling picker->picker_iface->on_dispatch() directly from the sim driver thread, violating plugin.h's documented "allocator thread only" contract for Post-allocation hooks. Dormant in practice only because builtin_fcfs.c happens to lock internally around every hook -- a defensive measure the contract text doesn't actually require of picker plugins. Since scheduler-sim-live-allocation, sim mode spawns the same real sched_job_picker()/sched_allocator() thread pair normal mode uses, so a real allocator thread now exists to route through. Added a reconcile_req/reconcile_ack cbuf pair on xce_sched_core_t and a reserved XCE_SCHED_RECONCILE_WAKE sentinel pushed through jbuf to wake the allocator thread without touching jbuf's pinned capacity-1/item-type invariant (Req 5 AC 7). sched_allocator() services the request and calls on_dispatch() itself; sim_reconcile_invoke_on_dispatch() blocks on the ack. Falls back to a direct call when no allocator thread is live for a given core (standalone unit tests), which is safe by construction. TDD: new test spawns a stand-in allocator-relay thread and asserts on_dispatch() fires there, not on the caller's thread -- confirmed RED against the old direct-call code, GREEN after. Four other test files with hand-rolled sched_allocator() stubs needed the same relay branch (found by an actual hang at meson's timeout). Full scheduler suite: 85/85, zero regressions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
94115b0024
commit
4f94638a1d
15 changed files with 541 additions and 67 deletions
|
|
@ -165,6 +165,16 @@ typedef void (*xce_unsatisfiable_populate_cb_t)(xce_sched_core_t* core,
|
|||
xce_sched_obj_t* obj,
|
||||
int reason_category);
|
||||
|
||||
/* Reserved jbuf sentinel for the Recorded-Decision Reconciliation
|
||||
* allocator-thread relay (bug-1837) -- see xce_sched_core_s's reconcile_req/
|
||||
* reconcile_ack fields' own doc comment below for the full protocol. Never a
|
||||
* real jobid (job ids are assigned by the controller's monotonic id
|
||||
* generator, starting near 1, nowhere near UINT64_MAX) and distinct from
|
||||
* jbuf's own pre-existing 0 "end of session" sentinel -- same reserved-value
|
||||
* idiom this codebase already uses elsewhere for an analogous "not a real
|
||||
* value" marker (see SCHED_SIM_START_TS_LATEST, sim.h). */
|
||||
#define XCE_SCHED_RECONCILE_WAKE ((uint64_t)-1)
|
||||
|
||||
struct xce_sched_core_s {
|
||||
mylock_t lock;
|
||||
int stop;
|
||||
|
|
@ -198,6 +208,26 @@ struct xce_sched_core_s {
|
|||
pthread_t allocator;
|
||||
cbuf_t wbuf; /* action to trigger scheduling */
|
||||
cbuf_t jbuf; /* job picked for allocation */
|
||||
/* Recorded-Decision Reconciliation allocator-thread relay (bug-1837):
|
||||
* plugin.h documents on_dispatch()/on_pend() as allocator-thread-only
|
||||
* hooks, but sim_loop.c's sim_reconcile_unaccounted_jobs() (the driver
|
||||
* thread) needs to invoke on_dispatch() for a job it reconstructs from
|
||||
* replay history -- there is no fresh allocation to run through the
|
||||
* normal jbuf/next_job() pipeline for it (the job is already RUN per
|
||||
* history, not a live candidate), so it cannot flow through jbuf's
|
||||
* existing real-jobid protocol. These two dedicated cbufs let the driver
|
||||
* hand the call off to the real sched_allocator() thread (sched_
|
||||
* election.c) instead of calling it directly: reconcile_req carries the
|
||||
* jobid to dispatch, reconcile_ack echoes on_dispatch()'s return code
|
||||
* once applied. Deliberately SEPARATE from jbuf, not a widened jbuf item
|
||||
* type -- jbuf's item size/capacity are a pinned determinism invariant
|
||||
* (test_core_jbuf_capacity_pin.c, Req 5 AC 7) that must never change.
|
||||
* See XCE_SCHED_RECONCILE_WAKE below and sched_allocator()'s matching
|
||||
* branch (sched_election.c) for the full handoff protocol. Normal mode
|
||||
* never populates reconcile_req (only sim_loop.c's reconciliation does),
|
||||
* so this is a true no-op for normal mode's allocator thread. */
|
||||
cbuf_t reconcile_req; /* jobid for on_dispatch, allocator-thread relay */
|
||||
cbuf_t reconcile_ack; /* on_dispatch()'s rc, echoed back once applied */
|
||||
cbuf_t decisions; /* scheduler decisions */
|
||||
/* Req 5 AC 6 (scheduler-core spec, task 4.4): driver-registered
|
||||
* decision callback + opaque driver context, set via
|
||||
|
|
|
|||
|
|
@ -93,6 +93,25 @@ typedef struct {
|
|||
bool interactive;
|
||||
uint64_t snapshot_ts;
|
||||
xce_sched_repl_state_t* repl_state;
|
||||
/* bug-1837 fix: set true by sched_sim_driver_run() (sim_loop.c),
|
||||
* immediately after it successfully spawns the real sched_allocator()
|
||||
* thread (sched_election.c) -- both the write here and every read in
|
||||
* sim_reconcile_unaccounted_jobs() happen on the SAME thread (the one
|
||||
* running sched_sim_driver_run()'s own round loop), in program order, so
|
||||
* this is plain single-threaded state, not a cross-thread flag; no
|
||||
* atomics needed. Tells sim_reconcile_invoke_on_dispatch() (sim_loop.c)
|
||||
* whether a real allocator thread exists to relay on_dispatch() through
|
||||
* (core->reconcile_req/reconcile_ack, core.h) or whether this xce_sched_
|
||||
* sim_driver_t was only ever driven directly (no thread pair spawned at
|
||||
* all -- e.g. test_sim_reconcile_unaccounted.c's own "call sim_
|
||||
* reconcile_unaccounted_jobs() standalone" Testing Strategy), in which
|
||||
* case calling on_dispatch() directly is safe by construction: there is
|
||||
* only ever one thread touching picker state in that case, so plugin.h's
|
||||
* allocator-thread-only contract has nothing to violate. Stays false
|
||||
* forever for a bare xce_sched_sim_driver_t built via fixture_init()-
|
||||
* style direct core init, matching sched_sim_driver_start()'s own
|
||||
* memset(sim, 0, sizeof(*sim)) default. */
|
||||
bool allocator_thread_live;
|
||||
} xce_sched_sim_driver_t;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -162,6 +162,17 @@ int xce_sched_core_init(xce_sched_core_t* core)
|
|||
* indirection for this value without first re-deriving the
|
||||
* determinism argument in AC 7. */
|
||||
cbuf_init(&(core->jbuf), 1, sizeof(uint64_t));
|
||||
|
||||
/* bug-1837 fix: Recorded-Decision Reconciliation's allocator-thread
|
||||
* relay -- see core.h's own doc comment on these two fields for the full
|
||||
* protocol. Capacity 1 each, matching jbuf's own one-request-in-flight
|
||||
* discipline (this is not the jbuf determinism invariant itself, so not
|
||||
* subject to test_core_jbuf_capacity_pin.c's pin -- just a sized-for-
|
||||
* purpose choice, since only one reconciliation request is ever
|
||||
* outstanding at a time by construction). */
|
||||
cbuf_init(&(core->reconcile_req), 1, sizeof(uint64_t));
|
||||
cbuf_init(&(core->reconcile_ack), 1, sizeof(int));
|
||||
|
||||
cbuf_init(&(core->decisions), ndecisions, sizeof(xce_decision_t*));
|
||||
|
||||
core->resmgr = makeInstance(&xkvmgr_type, &resmgr_params);
|
||||
|
|
@ -196,6 +207,8 @@ void xce_sched_core_fini(xce_sched_core_t* core)
|
|||
}
|
||||
|
||||
cbuf_fini(&(core->decisions));
|
||||
cbuf_fini(&(core->reconcile_ack));
|
||||
cbuf_fini(&(core->reconcile_req));
|
||||
cbuf_fini(&(core->jbuf));
|
||||
cbuf_fini(&(core->wbuf));
|
||||
|
||||
|
|
|
|||
|
|
@ -202,6 +202,27 @@ void* sched_allocator(void* arg)
|
|||
continue;
|
||||
}
|
||||
|
||||
if (jobid == XCE_SCHED_RECONCILE_WAKE) {
|
||||
/* bug-1837: Recorded-Decision Reconciliation relay
|
||||
* (sim_reconcile_unaccounted_jobs(), sim_loop.c) -- see core.h's
|
||||
* reconcile_req/reconcile_ack doc comment for the full
|
||||
* handshake. Not a real job: no plugin_job_election()/
|
||||
* sched_make_allocation() call, no decision built, nothing
|
||||
* pushed to core->decisions -- purely a threading-contract
|
||||
* relay so on_dispatch() genuinely runs here, on the allocator
|
||||
* thread, exactly as plugin.h's Post-allocation hooks contract
|
||||
* requires, instead of being called directly by the driver
|
||||
* thread. */
|
||||
uint64_t reconcile_jobid = 0;
|
||||
int reconcile_rc;
|
||||
|
||||
cbuf_get(&(core->reconcile_req), &reconcile_jobid);
|
||||
reconcile_rc = job_picker->picker_iface->on_dispatch(
|
||||
job_picker, reconcile_jobid);
|
||||
cbuf_put(&(core->reconcile_ack), &reconcile_rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
job = sched_get_job(core, jobid);
|
||||
if (job == NULL) {
|
||||
fmt_jobid(jobidString, sizeof(jobidString), jobid);
|
||||
|
|
|
|||
|
|
@ -764,6 +764,56 @@ static void sim_reconcile_toggle_prior_charges(xce_sched_core_t* core,
|
|||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
/*
|
||||
* sim_reconcile_invoke_on_dispatch - bug-1837 fix: invoke picker->
|
||||
* picker_iface->on_dispatch() on behalf of Recorded-Decision Reconciliation
|
||||
* (sim_reconcile_unaccounted_jobs(), below), routed through the real
|
||||
* allocator thread when one is live, per plugin.h's documented Post-
|
||||
* allocation hooks contract ("allocator thread, after each allocation
|
||||
* attempt"). Direct-call fallback when `sim->allocator_thread_live` is
|
||||
* false: no thread pair was ever spawned against this xce_sched_sim_driver_t
|
||||
* (e.g. a standalone unit test that calls sim_reconcile_unaccounted_jobs()
|
||||
* directly, never sched_sim_driver_run() -- test_sim_reconcile_unaccounted.c's
|
||||
* own documented Testing Strategy), so there is provably only one thread in
|
||||
* the whole process touching picker state and nothing to race.
|
||||
*
|
||||
* Protocol when a real allocator thread IS live (core.h's reconcile_req/
|
||||
* reconcile_ack doc comment has the full rationale): push `jobid` onto
|
||||
* core->reconcile_req, THEN push the reserved XCE_SCHED_RECONCILE_WAKE
|
||||
* sentinel onto core->jbuf (never a real jobid, never jbuf's own 0
|
||||
* end-of-session marker) to wake sched_allocator() (sched_election.c) out of
|
||||
* its cbuf_get(&core->jbuf) block. Ordering matters: cbuf_put()'s own
|
||||
* mutex-protected write to reconcile_req happens-before the jbuf push that
|
||||
* follows it (same thread, program order), and cbuf_get()'s own
|
||||
* mutex-protected read on the allocator thread happens-after the matching
|
||||
* cbuf_put() (POSIX mutex unlock->lock ordering) -- so by the time the
|
||||
* allocator thread's cbuf_get(&core->jbuf) returns the wake sentinel,
|
||||
* reconcile_req is guaranteed to already hold this call's jobid. Blocks on
|
||||
* core->reconcile_ack until sched_allocator() echoes on_dispatch()'s return
|
||||
* code back -- exactly one request in flight at a time, matching jbuf's own
|
||||
* capacity-1 rendezvous discipline (never widened; a completely separate
|
||||
* pair of cbufs carries this protocol instead).
|
||||
*/
|
||||
static int sim_reconcile_invoke_on_dispatch(xce_sched_sim_driver_t* sim,
|
||||
xce_sched_plugin_t* picker,
|
||||
uint64_t jobid)
|
||||
{
|
||||
if (!sim->allocator_thread_live) {
|
||||
return picker->picker_iface->on_dispatch(picker, jobid);
|
||||
}
|
||||
|
||||
cbuf_put(&(sim->core.reconcile_req), &jobid);
|
||||
{
|
||||
uint64_t wake = XCE_SCHED_RECONCILE_WAKE;
|
||||
int rc = 0;
|
||||
|
||||
cbuf_put(&(sim->core.jbuf), &wake);
|
||||
cbuf_get(&(sim->core.reconcile_ack), &rc);
|
||||
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* sim_reconcile_unaccounted_jobs - Recorded-Decision Reconciliation
|
||||
* (scheduler-sim-decision-parity spec, design.md's "Recorded-Decision
|
||||
|
|
@ -1163,7 +1213,7 @@ static void sim_reconcile_unaccounted_jobs(xce_sched_sim_driver_t* sim,
|
|||
* confirmed dispatched, so a later round's sim_picker_thread_fn()
|
||||
* never re-offers (and re-PENDs) it. */
|
||||
if (picker != NULL) {
|
||||
picker->picker_iface->on_dispatch(picker, job->jobid);
|
||||
sim_reconcile_invoke_on_dispatch(sim, picker, job->jobid);
|
||||
}
|
||||
|
||||
found = 1;
|
||||
|
|
@ -1349,6 +1399,13 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
|
|||
return -rc;
|
||||
}
|
||||
|
||||
/* bug-1837 fix: recorded exactly once, right here, before the round loop
|
||||
* below can possibly run -- see xce_sched_sim_driver_t's own doc comment
|
||||
* (sim.h) on this field for why this is plain single-threaded state
|
||||
* (same thread sets it and later reads it, in program order) rather than
|
||||
* a cross-thread flag needing atomics. */
|
||||
sim->allocator_thread_live = true;
|
||||
|
||||
/* Req 5 AC 16 (task 11.5, "What-if branching"): once --sim-alt-events
|
||||
* is set, the ONGOING event/session stream comes from
|
||||
* sim->alt_events_file instead of sim->replay_file -- the initial
|
||||
|
|
|
|||
|
|
@ -531,6 +531,30 @@ void* sched_allocator(void* arg)
|
|||
continue;
|
||||
}
|
||||
|
||||
if (jobid == XCE_SCHED_RECONCILE_WAKE) {
|
||||
/* bug-1837: this file's stand-in sched_allocator() must service
|
||||
* the same Recorded-Decision Reconciliation relay protocol the
|
||||
* real sched_election.c implementation does (see that file's
|
||||
* matching branch for the full rationale) -- this file's own
|
||||
* sched_sim_driver_run() calls set sim->allocator_thread_live,
|
||||
* so sim_reconcile_unaccounted_jobs() routes on_dispatch()
|
||||
* through jbuf/reconcile_req/reconcile_ack exactly as
|
||||
* production does; without this branch, a reconciliation
|
||||
* success here would hang forever waiting on an ack this stub
|
||||
* never sends. */
|
||||
uint64_t reconcile_jobid = 0;
|
||||
int reconcile_rc = 0;
|
||||
|
||||
cbuf_get(&(core->reconcile_req), &reconcile_jobid);
|
||||
(void)sched_resolve_job_picker(core, &picker);
|
||||
if (picker != NULL) {
|
||||
reconcile_rc = picker->picker_iface->on_dispatch(
|
||||
picker, reconcile_jobid);
|
||||
}
|
||||
cbuf_put(&(core->reconcile_ack), &reconcile_rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
job = sched_get_job(core, jobid);
|
||||
if (job == NULL) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -483,6 +483,30 @@ void* sched_allocator(void* arg)
|
|||
continue;
|
||||
}
|
||||
|
||||
if (jobid == XCE_SCHED_RECONCILE_WAKE) {
|
||||
/* bug-1837: this file's stand-in sched_allocator() must service
|
||||
* the same Recorded-Decision Reconciliation relay protocol the
|
||||
* real sched_election.c implementation does (see that file's
|
||||
* matching branch for the full rationale) -- this file's own
|
||||
* sched_sim_driver_run() calls set sim->allocator_thread_live,
|
||||
* so sim_reconcile_unaccounted_jobs() routes on_dispatch()
|
||||
* through jbuf/reconcile_req/reconcile_ack exactly as
|
||||
* production does; without this branch, a reconciliation
|
||||
* success here would hang forever waiting on an ack this stub
|
||||
* never sends. */
|
||||
uint64_t reconcile_jobid = 0;
|
||||
int reconcile_rc = 0;
|
||||
|
||||
cbuf_get(&(core->reconcile_req), &reconcile_jobid);
|
||||
(void)sched_resolve_job_picker(core, &picker);
|
||||
if (picker != NULL) {
|
||||
reconcile_rc = picker->picker_iface->on_dispatch(
|
||||
picker, reconcile_jobid);
|
||||
}
|
||||
cbuf_put(&(core->reconcile_ack), &reconcile_rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
job = sched_get_job(core, jobid);
|
||||
if (job == NULL) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
*/
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -2524,6 +2525,192 @@ test_reconcile_removes_reconciled_job_from_picker_candidates(void)
|
|||
"PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Test (bug-1837, TDD): plugin.h documents on_dispatch()/on_pend() as */
|
||||
/* "Post-allocation hooks (MUST) -- allocator thread, after each allocation */
|
||||
/* attempt". Recorded-Decision Reconciliation used to call on_dispatch() */
|
||||
/* directly from ITS OWN caller thread (sim_reconcile_unaccounted_jobs()'s */
|
||||
/* thread -- normally sched_sim_driver_run()'s driver thread in production), */
|
||||
/* violating that contract by construction. Dormant in production only */
|
||||
/* because builtin_fcfs.c happens to guard every hook with its own internal */
|
||||
/* lock -- a defensive measure plugin.h's contract text does not actually */
|
||||
/* require of picker plugins in general ("the picker thread holds no lock */
|
||||
/* the allocator thread needs"). */
|
||||
/* */
|
||||
/* Fix: once a real allocator thread is live for a core (xce_sched_sim_ */
|
||||
/* driver_t.allocator_thread_live, sim.h), sim_reconcile_invoke_on_dispatch()*/
|
||||
/* (sim_loop.c) routes the call through it via core->reconcile_req/ */
|
||||
/* core->jbuf's XCE_SCHED_RECONCILE_WAKE sentinel/core->reconcile_ack */
|
||||
/* (core.h), and sched_election.c's sched_allocator() is what actually */
|
||||
/* invokes on_dispatch() on that thread. */
|
||||
/* */
|
||||
/* This test proves it directly, independent of builtin_fcfs's own internal */
|
||||
/* locking: a minimal test-owned picker plugin records pthread_self() from */
|
||||
/* inside its on_dispatch() implementation, and a one-shot thread standing */
|
||||
/* in for sched_allocator() (relay_one_reconcile_request(), below) services */
|
||||
/* the relay protocol. RED before the bug-1837 fix (on_dispatch() ran on */
|
||||
/* this test's OWN thread -- g_dispatch_calling_tid would equal */
|
||||
/* pthread_self(), and g_seen_jbuf_wake_value would never be observed since */
|
||||
/* nothing was ever pushed onto jbuf at all), GREEN after. */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static pthread_t g_dispatch_calling_tid;
|
||||
static int g_dispatch_call_count;
|
||||
static uint64_t g_seen_jbuf_wake_value;
|
||||
|
||||
static int thread_recording_on_dispatch(void* picker, uint64_t jobid)
|
||||
{
|
||||
(void)picker;
|
||||
(void)jobid;
|
||||
g_dispatch_calling_tid = pthread_self();
|
||||
g_dispatch_call_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int noop_picker_session_hook(void* picker)
|
||||
{
|
||||
(void)picker;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static xce_sched_job_t* noop_picker_next_job_hook(void* picker)
|
||||
{
|
||||
(void)picker;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int noop_picker_jobid_hook(void* picker, uint64_t jobid)
|
||||
{
|
||||
(void)picker;
|
||||
(void)jobid;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static xce_job_picker_iface_t g_thread_recording_picker_iface = {
|
||||
.init = NULL,
|
||||
.fini = NULL,
|
||||
.begin_session = noop_picker_session_hook,
|
||||
.next_job = noop_picker_next_job_hook,
|
||||
.end_session = noop_picker_session_hook,
|
||||
.on_dispatch = thread_recording_on_dispatch,
|
||||
.on_pend = noop_picker_jobid_hook,
|
||||
.on_queue_update = NULL,
|
||||
.on_job_push = NULL,
|
||||
.on_job_state_change = NULL,
|
||||
.on_job_update = NULL,
|
||||
.on_config = NULL,
|
||||
.on_job_rusage = NULL,
|
||||
};
|
||||
|
||||
static xce_sched_plugin_t g_thread_recording_plugin
|
||||
= { .name = (char*)"test_reconcile_thread_picker",
|
||||
.picker_iface = &g_thread_recording_picker_iface };
|
||||
|
||||
/* One-shot stand-in for sched_election.c's real sched_allocator() --
|
||||
* services exactly one Recorded-Decision Reconciliation relay request
|
||||
* (core->jbuf's XCE_SCHED_RECONCILE_WAKE sentinel, then core->reconcile_req,
|
||||
* then acks via core->reconcile_ack) and returns, mirroring that function's
|
||||
* own SIM_RECONCILE_JBUF_WAKE branch exactly (see its own comment,
|
||||
* sched_election.c, for the full protocol this mirrors). Deliberately not a
|
||||
* forever loop: this test only ever sends one request. No TEST_ASSERT_*
|
||||
* calls in here -- Unity's assertion macros longjmp against the MAIN test
|
||||
* thread's context, unsafe to invoke from a spawned thread; this function
|
||||
* only records observations into statics, asserted on after pthread_join()
|
||||
* back on the main test thread. */
|
||||
static void* relay_one_reconcile_request(void* arg)
|
||||
{
|
||||
xce_sched_core_t* core = (xce_sched_core_t*)arg;
|
||||
uint64_t wake_val = 0;
|
||||
uint64_t reconcile_jobid = 0;
|
||||
xce_sched_plugin_t* job_picker = NULL;
|
||||
int reconcile_rc = 0;
|
||||
|
||||
cbuf_get(&(core->jbuf), &wake_val);
|
||||
g_seen_jbuf_wake_value = wake_val;
|
||||
|
||||
cbuf_get(&(core->reconcile_req), &reconcile_jobid);
|
||||
(void)sched_resolve_job_picker(core, &job_picker);
|
||||
if (job_picker != NULL) {
|
||||
reconcile_rc = job_picker->picker_iface->on_dispatch(job_picker,
|
||||
reconcile_jobid);
|
||||
}
|
||||
cbuf_put(&(core->reconcile_ack), &reconcile_rc);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
test_reconcile_invokes_on_dispatch_on_allocator_thread_not_caller(void)
|
||||
{
|
||||
xce_sched_sim_driver_t sim;
|
||||
xce_sched_job_t* job;
|
||||
xce_job_alloc_t* jalloc;
|
||||
replay_session_t* decoded;
|
||||
int residx;
|
||||
pthread_t relay_tid;
|
||||
int prc;
|
||||
|
||||
fixture_init(&sim);
|
||||
|
||||
g_dispatch_calling_tid = pthread_self(); /* sentinel: only meaningful if
|
||||
* overwritten below */
|
||||
g_dispatch_call_count = 0;
|
||||
g_seen_jbuf_wake_value = 0;
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, plugin_register_core(&(sim.core), &g_thread_recording_plugin));
|
||||
sim.core.job_pick_plugin = g_thread_recording_plugin.name;
|
||||
|
||||
residx = register_slots_resource(&(sim.core));
|
||||
add_worker(&(sim.core), "worker1", residx, 1000.0f);
|
||||
|
||||
job = add_job(&(sim.core), 7200ULL, JOB_STAT_RUN, NULL);
|
||||
TEST_ASSERT_NULL(job->accounts);
|
||||
|
||||
jalloc = make_test_job_alloc_with_host(7200ULL, "worker1", "slots", 2.0f);
|
||||
decoded = make_decoded_session(1000ULL, make_dispatch_entry_from_alloc(jalloc));
|
||||
|
||||
/* bug-1837 fix: mark a real allocator thread as live for this core, and
|
||||
* actually spawn its stand-in BEFORE triggering reconciliation -- cbuf's
|
||||
* own mutex+condvar makes the exact spawn/relay-vs-post ordering
|
||||
* irrelevant (see relay_one_reconcile_request()'s own comment). */
|
||||
sim.allocator_thread_live = true;
|
||||
prc = pthread_create(&relay_tid, NULL, relay_one_reconcile_request,
|
||||
&(sim.core));
|
||||
TEST_ASSERT_EQUAL_INT(0, prc);
|
||||
|
||||
sim_reconcile_unaccounted_jobs(&sim, decoded, NULL);
|
||||
|
||||
pthread_join(relay_tid, NULL);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(
|
||||
job->accounts, "fixture sanity: reconciliation must still have found "
|
||||
"and captured the matching recorded decision");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_dispatch_call_count,
|
||||
"on_dispatch() must be invoked exactly "
|
||||
"once");
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(
|
||||
XCE_SCHED_RECONCILE_WAKE, g_seen_jbuf_wake_value,
|
||||
"bug-1837: reconciliation must wake the allocator thread via the "
|
||||
"reserved jbuf sentinel, never by pushing the real jobid onto jbuf "
|
||||
"directly (that would incorrectly route it through the live "
|
||||
"allocation chain)");
|
||||
TEST_ASSERT_FALSE_MESSAGE(
|
||||
pthread_equal(g_dispatch_calling_tid, pthread_self()),
|
||||
"bug-1837: on_dispatch() must run on the allocator thread, never on "
|
||||
"sim_reconcile_unaccounted_jobs()'s own caller thread, once a real "
|
||||
"allocator thread is live for this core -- plugin.h's documented "
|
||||
"Post-allocation hooks contract (\"allocator thread, after each "
|
||||
"allocation attempt\")");
|
||||
TEST_ASSERT_TRUE_MESSAGE(
|
||||
pthread_equal(g_dispatch_calling_tid, relay_tid),
|
||||
"on_dispatch() must run specifically on the spawned allocator-relay "
|
||||
"thread, not some other thread");
|
||||
|
||||
printf("test_reconcile_invokes_on_dispatch_on_allocator_thread_not_"
|
||||
"caller: PASS\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Task 4.2 fixtures (scheduler-sim-live-allocation spec, "Sim-Mode */
|
||||
/* Reconciliation Narrowing" component, Requirement 3.4/3.5): register a */
|
||||
|
|
@ -2816,6 +3003,8 @@ int main(void)
|
|||
RUN_TEST(
|
||||
test_reconcile_info_worker_and_summary_hide_completed_job_after_stray_dispatch);
|
||||
RUN_TEST(test_reconcile_removes_reconciled_job_from_picker_candidates);
|
||||
RUN_TEST(
|
||||
test_reconcile_invokes_on_dispatch_on_allocator_thread_not_caller);
|
||||
RUN_TEST(test_reconcile_registers_global_limit_usage_for_reconciled_job);
|
||||
RUN_TEST(
|
||||
test_reconcile_does_not_reregister_limit_for_already_decided_job);
|
||||
|
|
|
|||
|
|
@ -375,6 +375,30 @@ void* sched_allocator(void* arg)
|
|||
continue;
|
||||
}
|
||||
|
||||
if (jobid == XCE_SCHED_RECONCILE_WAKE) {
|
||||
/* bug-1837: this file's stand-in sched_allocator() must service
|
||||
* the same Recorded-Decision Reconciliation relay protocol the
|
||||
* real sched_election.c implementation does (see that file's
|
||||
* matching branch for the full rationale) -- this file's own
|
||||
* sched_sim_driver_run() calls set sim->allocator_thread_live,
|
||||
* so sim_reconcile_unaccounted_jobs() routes on_dispatch()
|
||||
* through jbuf/reconcile_req/reconcile_ack exactly as
|
||||
* production does; without this branch, a reconciliation
|
||||
* success here would hang forever waiting on an ack this stub
|
||||
* never sends. */
|
||||
uint64_t reconcile_jobid = 0;
|
||||
int reconcile_rc = 0;
|
||||
|
||||
cbuf_get(&(core->reconcile_req), &reconcile_jobid);
|
||||
(void)sched_resolve_job_picker(core, &picker);
|
||||
if (picker != NULL) {
|
||||
reconcile_rc = picker->picker_iface->on_dispatch(
|
||||
picker, reconcile_jobid);
|
||||
}
|
||||
cbuf_put(&(core->reconcile_ack), &reconcile_rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
job = sched_get_job(core, jobid);
|
||||
if (job == NULL) {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -391,6 +391,30 @@ void* sched_allocator(void* arg)
|
|||
continue;
|
||||
}
|
||||
|
||||
if (jobid == XCE_SCHED_RECONCILE_WAKE) {
|
||||
/* bug-1837: this file's stand-in sched_allocator() must service
|
||||
* the same Recorded-Decision Reconciliation relay protocol the
|
||||
* real sched_election.c implementation does (see that file's
|
||||
* matching branch for the full rationale) -- this file's own
|
||||
* sched_sim_driver_run() calls set sim->allocator_thread_live,
|
||||
* so sim_reconcile_unaccounted_jobs() routes on_dispatch()
|
||||
* through jbuf/reconcile_req/reconcile_ack exactly as
|
||||
* production does; without this branch, a reconciliation
|
||||
* success here would hang forever waiting on an ack this stub
|
||||
* never sends. */
|
||||
uint64_t reconcile_jobid = 0;
|
||||
int reconcile_rc = 0;
|
||||
|
||||
cbuf_get(&(core->reconcile_req), &reconcile_jobid);
|
||||
(void)sched_resolve_job_picker(core, &picker);
|
||||
if (picker != NULL) {
|
||||
reconcile_rc = picker->picker_iface->on_dispatch(
|
||||
picker, reconcile_jobid);
|
||||
}
|
||||
cbuf_put(&(core->reconcile_ack), &reconcile_rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
job = sched_get_job(core, jobid);
|
||||
if (job == NULL) {
|
||||
continue;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue