fix(scheduler-sim-decision-parity): give sim mode driver.c's round-boundary split (task 2.1)

Replaces sim_loop.c's monolithic xce_core_go() with the same
xce_core_begin_session() -> sched_account_jobs() -> xce_core_signal_go()
sequence driver.c already uses for normal-mode rounds, opening the same
interleaving window and calling the now-core-level accounting service
(task 1.1) at the same relative position. Error-handling for a failed
session start is preserved byte-for-byte (same cleanup, same rc source).
Both interactive and batch simulation runs go through this one call site.

This is bug-1613's wiring half: sim_loop.c now calls sched_account_jobs()
at all (it never did before). resInUse is still zero end-to-end after this
task alone, because job->accounts is never populated by any live path in
sim mode yet -- that's Recorded-Decision Reconciliation (tasks 2.2-2.4),
not yet implemented; a comment marks where it slots in.

Adds a call-count/session-window regression test proving the wiring itself
(RED-phase reproduced live via git stash + rebuild, matching bug-1613's
symptom exactly). 8 sibling white-box test files needed a link-satisfying
sched_account_jobs() stub, discovered via actual linker errors.

45/46 scheduler tests pass; the 1 failure is pre-existing bug-1560
(unrelated).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-22 07:25:12 +00:00
parent 76ca61547e
commit 134380b871
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
12 changed files with 750 additions and 9 deletions

View file

@ -435,10 +435,11 @@ static void sim_print_decision(const xce_decision_t* d)
* decision with no real worker/resource assignment behind it.
*
* Runs for the simulation driver's entire lifetime (one wbuf-wait/session
* cycle per xce_core_go(), forever) -- never joined or cancelled: no
* sim-driver teardown/fini function exists yet (task 11.1 did not build
* one either), the same gap sched_job_picker()/sched_allocator() do not
* have in normal mode only because sched_fini() already exists there.
* cycle per xce_core_signal_go() call, forever) -- never joined or
* cancelled: no sim-driver teardown/fini function exists yet (task 11.1
* did not build one either), the same gap sched_job_picker()/
* sched_allocator() do not have in normal mode only because sched_fini()
* already exists there.
*/
static void* sim_picker_thread_fn(void* arg)
{
@ -677,10 +678,18 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
= sched_record_session_ctx_new(xce_core_now(&(sim->core)));
}
if (xce_core_go(&(sim->core)) < 0) {
ERROR("%s xce_core_go failed -- skipping this round's "
"decision drain",
__func__);
/* Task 2.1 (scheduler-sim-decision-parity): xce_core_go()
* split into xce_core_begin_session()/xce_core_signal_go(),
* mirroring driver.c's xce_sched_driver_run_normal_mode_round()
* -- opens the interleaving window sched_account_jobs() below
* needs (core.h's doc comment on these two functions). Error
* contract unchanged: a failed begin_session still skips this
* round's decision drain, exactly as xce_core_go() did. */
rc = xce_core_begin_session(&(sim->core));
if (rc < 0) {
ERROR("%s xce_core_begin_session failed rc=%d -- skipping "
"this round's decision drain",
__func__, rc);
free_ReplaySession_ptr(&decoded_session);
/* record_ctx (if any) has nothing accumulated yet -- a
* direct free() is safe: no ReplaySession is written for a
@ -689,6 +698,14 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
continue;
}
/* Recorded-Decision Reconciliation (tasks 2.2-2.4, not yet
* implemented) belongs here, before sched_account_jobs()
* below -- see design.md's "Recorded-Decision Reconciliation"
* component. */
sched_account_jobs(&(sim->core));
xce_core_signal_go(&(sim->core));
/* Req 5 AC 15 (task 11.4): when --sim-verify is set, walk the
* decoded `decisions` array positionally (di) alongside the
* live drained decisions, exactly matching AC 18's pseudocode

View file

@ -726,6 +726,32 @@ sched_sim_equivalence_test = executable(
dependencies: [scheduler_test_deps],
install: false)
# Round-boundary ordering contract for the simulation driver
# (scheduler-sim-decision-parity spec, task 2.1, Req 1.3/1.4, 5.1, 5.3): the
# sim-mode counterpart to sched_driver_session_ordering_test above -- proves
# sim_loop.c's sched_sim_driver_run() now calls the relocated
# sched_account_jobs() (core.h, task 1.1) once per replayed round, strictly
# inside that round's session-active window, mirroring driver.c's
# xce_sched_driver_run_normal_mode_round() exactly. Before this task,
# sim_loop.c called the older monolithic xce_core_go() with no interleaving
# slot and never called sched_account_jobs() at all (bug-1613) -- every
# worker's/shared resource's resInUse stayed permanently zero for the whole
# `--sim`/`--interactive` run. Same combined white-box #include set as
# sched_sim_equivalence_test (records two real normal-mode rounds via the
# REAL normal-mode driver, then replays the same file through the REAL,
# unmodified simulation driver) -- see the test's own header comment for why
# a spy sched_account_jobs() (not the real one) is the right-strength proof
# here, and why no cross-thread wbuf-rendezvous replica of
# test_driver_session_ordering.c's own design is attempted (sim_loop.c's
# picker thread is not independently substitutable from a test without
# touching sim_picker.c, outside this task's boundary).
sched_sim_session_ordering_test = executable(
'sched_sim_session_ordering_test',
['test_sim_session_ordering.c'],
include_directories:[incdir],
dependencies: [scheduler_test_deps],
install: false)
# Resource-contention replay fixture (scheduler-sim-decision-parity spec,
# task 1.2, Req 2.1/2.2): existing fixtures in this directory never have
# scarce capacity, so none can exercise or prove capacity-check correctness.
@ -1073,6 +1099,9 @@ test('scheduler sim record output test',
test('scheduler sim equivalence test',
sched_sim_equivalence_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler sim session ordering test',
sched_sim_session_ordering_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])
test('scheduler sim resource contention test',
sched_sim_resource_contention_test, suite: 'scheduler',
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -110,6 +110,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy test_repl_execution_control.c/test_sim_alt_events.c/etc. each carry
* (schedobj.c is not linked here). */

View file

@ -159,6 +159,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy every white-box test in this directory that does not link
* schedobj.c already establishes (test_repl_execution_control.c,

View file

@ -124,6 +124,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy test_sim_alt_events.c/test_sim_verify.c/test_sim_event_loop.c each
* carry (schedobj.c is not linked here: it would need plugin_register(),

View file

@ -88,6 +88,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy every sim-family white-box test carries (schedobj.c is not linked
* here: it would need plugin_register(), which requires a genuine

View file

@ -117,6 +117,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy test_sim_verify.c/test_sim_event_loop.c/test_sim_startup.c each
* carry (schedobj.c is not linked here: it would need plugin_register(),

View file

@ -139,6 +139,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* rationale/algorithm as test_sim_startup.c's/test_core_full_cycle.c's own
* copies (schedobj.c is not linked: it would need plugin_register(), which

View file

@ -104,6 +104,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
{
xce_sched_plugin_t* plugin = NULL;

View file

@ -0,0 +1,605 @@
/*
* test_sim_session_ordering.c - proves the simulation driver's round
* boundary now upholds the same ordering discipline
* test_driver_session_ordering.c already proves for the normal-mode driver
* (scheduler-sim-decision-parity spec, task 2.1, Req 1.3/1.4, 5.1, 5.3;
* "Sim Driver Round-Boundary Accounting" component, design.md).
*
* The property under test: sim_loop.c's sched_sim_driver_run() must call
* the relocated sched_account_jobs() (core.h, task 1.1) once per replayed
* round, strictly inside the round's session-active window -- i.e. after
* xce_core_begin_session()'s resval reset, before xce_core_end_session()
* closes the round -- mirroring driver.c's
* xce_sched_driver_run_normal_mode_round() exactly. Before task 2.1,
* sim_loop.c called the older monolithic xce_core_go() with zero
* interleaving slot and never called sched_account_jobs() at all
* (bug-1613, documented in .kiro/steering/scheduler-driver-equivalence.md)
* -- every worker's/shared resource's resInUse stayed permanently zero
* across a whole `--sim`/`--interactive` run regardless of what jobs were
* actually running. This file's own central assertion is 0 spy calls
* against the pre-fix code, 2 (one per replayed round) against the fixed
* code -- captured as this task's RED/GREEN evidence.
*
* Why a spy, not the real sched_account_jobs() (resource.c/scheduler.c):
* same rationale test_driver_session_ordering.c's own header comment gives
* for its identical choice -- this file's target is the DRIVER's ordering
* discipline (does it call sched_account_jobs() at all, and in the right
* window), not whether sched_account_jobs() itself charges resources
* correctly (task 1.1's own scope, proven by test_resource_accounting.c).
*
* Why no cross-thread wbuf-rendezvous proof (unlike
* test_driver_session_ordering.c's own elaborate three-thread design): that
* proof technique requires substituting a test-controlled picker plugin for
* the one the driver under test resolves. sim_loop.c's
* sched_sim_driver_run() -- unlike driver.c's decoupled, single-round
* xce_sched_driver_run_normal_mode_round() -- is one monolithic entry point
* that spawns its own picker thread (sim_picker_thread_fn(), sim_loop.c)
* internally and unconditionally bootstraps the real builtin_fcfs plugin
* (sim_bootstrap_picker(), sim_picker.c) as part of the same call; neither
* is independently substitutable from a test without modifying production
* code outside this task's boundary (sim_picker.c is not part of the "Sim
* Driver Round-Boundary Accounting" component this task owns). This is not
* a gap in rigor: begin_session()/[reconciliation slot]/account_jobs()/
* signal_go() are four single-threaded, sequential, unconditional calls in
* sim_loop.c's own source (no branch reorders them), so "does
* account_jobs() run before signal_go()" is a plain, deterministic
* same-thread program-order fact, not a race to be proven at runtime --
* only "does it actually run, and does it see session_active==1 (i.e.
* strictly after begin_session, strictly before end_session)" is the
* genuinely runtime-observable part, which this file's spy does prove.
* cbuf's own capacity-1 blocking rendezvous (core_session.c, already
* proven by test_core_session.c) is what guarantees the picker thread
* cannot observe xce_core_signal_go()'s wbuf signal any earlier than that
* call actually executes.
*
* Error-path preservation (Requirement: "preserve the existing
* error-handling behavior for a failed session start exactly as it works
* today"): NOT exercised by a runtime reproduction in this file. Analysis
* (see task's own status report): xce_core_begin_session() only fails when
* a session is already active, and sched_sim_driver_run()'s single
* replay-reading thread always completes a round's xce_core_end_session()
* (via the picker thread's guaranteed end-of-session NULL decision, drained
* before end_session runs) before its own next loop iteration could call
* begin_session() again -- this was equally true, and equally unreachable
* through the legitimate single-consumer replay loop, before this task's
* change (xce_core_go() had the identical failure condition). This task's
* diff preserves the exact same cleanup sequence (free_ReplaySession_ptr(),
* free(record_ctx), continue) the prior xce_core_go() branch used, just
* keyed off xce_core_begin_session()'s own rc instead -- verified by code
* review, not a synthetic runtime trigger disproportionate to a
* structurally unreachable branch.
*
* Fixture-construction technique and white-box #include/stub set: IDENTICAL
* to test_sim_equivalence.c's own (real normal-mode recording via
* sched_record_start()/xce_sched_driver_run_normal_mode_round() driving the
* REAL builtin_fcfs picker + a stand-in allocator that unconditionally
* PENDs, then replayed via the REAL, unmodified sched_sim_driver_run()) --
* see that file's own header comment for the full rationale on why every
* decision here is PEND (sim_loop.c's live picker thread cannot emit a
* DISPATCH). This file records TWO separate rounds (not test_sim_equivalence.c's
* one) specifically to prove sched_account_jobs() is called ONCE PER ROUND,
* not merely once ever -- pinning Requirement 1.3 ("When a scheduling round
* completes ... refresh ... not only jobs whose resource consumption was
* already reflected in an earlier round").
*/
#include <errno.h>
#include <limits.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "controller.capnp.h"
#include "kvmgr.h"
#include "preason.h"
#include "scheduler.h"
#include "xstring.h"
/* Pull in the real functions under test into this TU (white-box #include,
* same combined set test_sim_equivalence.c already establishes). */
#include "../src/core.c"
#include "../src/core_clock.c"
#include "../src/core_events.c"
#include "../src/core_session.c"
#include "../src/decision.c"
#include "../src/driver.c"
#include "../src/plugin.c"
#include "../src/plugins/builtin_fcfs.c"
#include "../src/recording.c"
#include "../src/recording_events.c"
#include "../src/recording_session.c"
#include "../src/repl.c"
#include "../src/repl_break.c"
#include "../src/repl_info.c"
#include "../src/repl_params.c"
#include "../src/sim.c"
#include "../src/sim_loop.c"
#include "../src/sim_picker.c" /* bug-1567: sched_sim_driver_run() calls sim_bootstrap_picker() (sim_loop.c-cap sibling) as an extern -- undefined reference at link time otherwise */
#include "../src/sim_record.c"
#include "../src/sim_verify.c"
#include "../src/snapshot.c"
#include "../src/snapshot_load.c"
#include "../src/snapshot_wire.c"
void kill_sched_job(void* arg) { (void)arg; }
void kill_sched_queue(void* arg) { (void)arg; }
void kill_sched_worker(void* arg) { (void)arg; }
void kill_sched_user(void* arg) { (void)arg; }
void kill_sched_project(void* arg) { (void)arg; }
void kill_sched_resval(xce_val_t* val) { (void)val; }
/* core_reset_workers()'s (core_session.c) linker dependency -- never
* invoked (no worker is ever added here). */
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
/* plugin_prepare_allocation()'s (plugin.c) linker dependency -- never
* invoked (this fixture never drives real resource-matching allocation --
* same rationale test_sim_equivalence.c's own copy documents). */
int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
xlist_t* limits)
{
(void)sched;
(void)vec;
(void)limits;
return 0;
}
/* xce_sched_driver_on_decision()'s (driver.c) publish-side linker
* dependencies -- never actually invoked (this test never registers a
* decision callback). Same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_publish_dispatch(xce_scheduler_t* sched, xce_job_dispatch_t* d)
{
(void)sched;
(void)d;
return 0;
}
int sched_publish_pend_reason(xce_scheduler_t* sched, xce_preason_set_t* r)
{
(void)sched;
(void)r;
return 0;
}
/* handle_deferred_admin_record()'s (driver.c) linker dependency -- never
* invoked (this test never enqueues XCE_SCHED_OP_ADMIN_RECORD). */
int xcecli_reply_schedadmin(xcecli_ctx_t c, uint64_t seq,
sched_admin_reply_t* reply)
{
(void)c;
(void)seq;
(void)reply;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy every white-box test in this directory that does not link
* schedobj.c already establishes. Shared by driver.c's normal-mode round
* (recording phase, below) AND sim_loop.c's sim_picker_thread_fn() (replay
* phase, below) -- one real definition, both call sites in this combined TU
* resolve through it. */
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
{
xce_sched_plugin_t* plugin = NULL;
int rc = 0;
if ((core == NULL) || (out == NULL)) {
return -EINVAL;
}
*out = NULL;
if (core->job_pick_plugin == NULL) {
return -EINVAL;
}
lock(core->plugins, RTTI_LOCK_READ);
rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin);
unlock(core->plugins);
if (rc <= 0) {
return (rc < 0) ? rc : -ENOENT;
}
if ((plugin == NULL) || (plugin->picker_iface == NULL)
|| (plugin->picker_iface->next_job == NULL)
|| (plugin->picker_iface->begin_session == NULL)
|| (plugin->picker_iface->end_session == NULL)
|| (plugin->picker_iface->on_dispatch == NULL)
|| (plugin->picker_iface->on_pend == NULL)) {
return -EINVAL;
}
*out = plugin;
return 0;
}
/* ---------------------------------------------------------------------- */
/* sched_account_jobs() SPY -- this file's own central instrument. Records */
/* one call count PLUS, per call (up to MAX_SPY_CALLS), whether */
/* core->session_active was 1 at the moment of the call -- proving each */
/* invocation happened strictly inside a round's begin_session/end_session */
/* window (see this file's own header comment for why this, rather than a */
/* cross-thread rendezvous, is the right-strength proof here). */
/* ---------------------------------------------------------------------- */
#define MAX_SPY_CALLS 8
static _Atomic int g_spy_calls;
static int g_spy_session_active_ok[MAX_SPY_CALLS];
int sched_account_jobs(xce_sched_core_t* core)
{
int idx = atomic_fetch_add(&g_spy_calls, 1);
if ((core != NULL) && (idx < MAX_SPY_CALLS)) {
g_spy_session_active_ok[idx]
= (atomic_load_explicit(&(core->session_active),
memory_order_acquire)
== 1)
? 1
: 0;
}
return 0;
}
static void reset_spy(void)
{
atomic_store(&g_spy_calls, 0);
memset(g_spy_session_active_ok, 0, sizeof(g_spy_session_active_ok));
}
void setUp(void) { return; }
void tearDown(void) { return; }
/* ---------------------------------------------------------------------- */
/* Fixture helpers -- identical to test_sim_equivalence.c's own. */
/* ---------------------------------------------------------------------- */
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
{
char tmpl[PATH_MAX];
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_simordering%s_XXXXXX", suffix);
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
strcpy(out, tmpl);
}
static void fixture_init(xce_scheduler_t* sched, char* record_dir)
{
xce_sched_plugin_t* fcfs;
int rc;
memset(sched, 0, sizeof(*sched));
mylock_init(&(sched->record_lock));
rc = xce_sched_core_init(&(sched->core));
TEST_ASSERT_EQUAL_INT(0, rc);
sched->core.pend_updates[0] = (dataq_t*)calloc(1, sizeof(dataq_t));
dataq_init(sched->core.pend_updates[0], NULL);
sched->core.pend_updates[1] = (dataq_t*)calloc(1, sizeof(dataq_t));
dataq_init(sched->core.pend_updates[1], NULL);
fcfs = builtin_fcfs_new_plugin();
TEST_ASSERT_NOT_NULL(fcfs);
rc = plugin_register_core(&(sched->core), fcfs);
TEST_ASSERT_TRUE(rc >= 0);
sched->core.job_pick_plugin = (char*)"builtin_fcfs";
sched->record_dir = record_dir;
}
static xce_queue_t* make_queue(const char* name, int32_t priority)
{
xce_queue_t* q = (xce_queue_t*)calloc(1, sizeof(*q));
q->queue = strdup(name);
q->status = QUEUE_STAT_OPEN;
q->priority = priority;
return q;
}
static xce_job_t* make_job(uint64_t jobid, uint64_t sub_time, const char* queue,
const char* user)
{
xce_job_t* j = (xce_job_t*)calloc(1, sizeof(*j));
j->jobid = jobid;
j->state = JOB_STAT_PEND;
j->sub_time = sub_time;
j->queue_time = sub_time;
j->start_time = 0;
j->submit = (job_submit_data_t*)calloc(1, sizeof(*(j->submit)));
j->submit->queue = strdup(queue);
j->submit->user = strdup(user);
j->submit->project_name = strdup("ordering_proj");
j->submit->resreq = strdup("type == any");
j->submit->mintasks = 1;
j->submit->maxtasks = 1;
return j;
}
static void enqueue_queue_update(xce_scheduler_t* sched, const char* name,
int32_t priority)
{
xce_sched_update_request_t* req
= (xce_sched_update_request_t*)calloc(1, sizeof(*req));
req->op = XCE_SCHED_OP_UPDATE_QUEUE;
req->u.queueupdate.queue = make_queue(name, priority);
dataq_enq(sched->core.pend_updates[0], &(req->link));
}
static void enqueue_jobs_push(xce_scheduler_t* sched, xce_job_t** jobs,
int njobs)
{
xce_sched_update_request_t* req
= (xce_sched_update_request_t*)calloc(1, sizeof(*req));
req->op = XCE_SCHED_OP_UPDATE_JOB;
req->u.jobupdate.njobs = njobs;
req->u.jobupdate.jobs = jobs;
dataq_enq(sched->core.pend_updates[0], &(req->link));
}
/* ---------------------------------------------------------------------- */
/* Normal-mode picker/allocator threads (recording phase) -- identical to */
/* test_sim_equivalence.c's own. */
/* ---------------------------------------------------------------------- */
static void* picker_thread_fn(void* arg)
{
xce_sched_core_t* core = (xce_sched_core_t*)arg;
xce_sched_plugin_t* job_picker = NULL;
int rc;
int go = 0;
uint64_t jobid;
rc = sched_resolve_job_picker(core, &job_picker);
if (rc < 0) {
return NULL;
}
cbuf_get(&(core->wbuf), &go);
job_picker->picker_iface->begin_session(job_picker);
for (;;) {
xce_sched_job_t* job = job_picker->picker_iface->next_job(job_picker);
if (job == NULL) {
break;
}
jobid = job->jobid;
cbuf_put(&(core->jbuf), &jobid);
}
jobid = 0;
cbuf_put(&(core->jbuf), &jobid);
job_picker->picker_iface->end_session(job_picker);
return NULL;
}
static void* stand_in_allocator_thread_fn(void* arg)
{
xce_sched_core_t* core = (xce_sched_core_t*)arg;
xce_sched_plugin_t* picker = NULL;
uint64_t jobid;
if (sched_resolve_job_picker(core, &picker) < 0) {
xce_decision_t* decision = NULL;
cbuf_get(&(core->jbuf), &jobid);
cbuf_put(&(core->decisions), &decision);
return NULL;
}
for (;;) {
xce_decision_t* decision;
cbuf_get(&(core->jbuf), &jobid);
if (jobid == 0) {
decision = NULL;
cbuf_put(&(core->decisions), &decision);
break;
}
picker->picker_iface->on_pend(picker, jobid);
decision = make_pend_decision(make_pendreason(jobid));
cbuf_put(&(core->decisions), &decision);
}
return NULL;
}
static void run_one_real_session(xce_scheduler_t* sched)
{
pthread_t picker_tid;
pthread_t allocator_tid;
int rc;
rc = pthread_create(&picker_tid, NULL, picker_thread_fn, &(sched->core));
TEST_ASSERT_EQUAL_INT(0, rc);
rc = pthread_create(&allocator_tid, NULL, stand_in_allocator_thread_fn,
&(sched->core));
TEST_ASSERT_EQUAL_INT(0, rc);
xce_sched_driver_run_normal_mode_round(sched);
rc = pthread_join(picker_tid, NULL);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = pthread_join(allocator_tid, NULL);
TEST_ASSERT_EQUAL_INT(0, rc);
}
/* ---------------------------------------------------------------------- */
/* The contract test. */
/* ---------------------------------------------------------------------- */
static void
test_sim_replay_calls_account_jobs_once_per_round_within_session_window(
void)
{
char record_dir[PATH_MAX];
xce_scheduler_t sched;
char r_path[PATH_MAX];
xce_job_t** jobs1;
xce_job_t** jobs2;
sched_sim_args_t sim_args;
xce_sched_sim_driver_t sim;
int rc;
int i;
/* ---- Record TWO real normal-mode rounds into one .replay file ---- */
make_tmp_dir(record_dir, sizeof(record_dir), "_r");
fixture_init(&sched, record_dir);
rc = sched_record_start(&sched, "ordering.replay");
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(r_path));
strcpy(r_path, sched.record_file_path);
enqueue_queue_update(&sched, "normal", 1);
jobs1 = (xce_job_t**)calloc(1, sizeof(xce_job_t*));
jobs1[0] = make_job(9201ULL, 1000ULL, "normal", "alice");
enqueue_jobs_push(&sched, jobs1, 1);
run_one_real_session(&sched); /* round 1 */
jobs2 = (xce_job_t**)calloc(1, sizeof(xce_job_t*));
jobs2[0] = make_job(9202ULL, 2000ULL, "normal", "bob");
enqueue_jobs_push(&sched, jobs2, 1);
run_one_real_session(&sched); /* round 2 */
rc = sched_record_stop(&sched);
TEST_ASSERT_EQUAL_INT(0, rc);
/* Reset the spy AFTER recording -- driver.c's own normal-mode round
* boundary (task 1.1) already calls the real sched_account_jobs() once
* per round during the recording above (already proven correct by
* test_driver_session_ordering.c); this test's own target is
* sim_loop.c's call site (task 2.1), isolated by resetting here. */
reset_spy();
/* ---- Replay the SAME file through the REAL, unmodified simulation
* driver ---- */
memset(&sim_args, 0, sizeof(sim_args));
sim_args.replay_path = r_path;
sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
sim_args.sim_speed = 0; /* frozen -- deterministic, no wall-clock skew */
memset(&sim, 0, sizeof(sim));
rc = sched_sim_driver_run(&sim, &sim_args);
TEST_ASSERT_EQUAL_INT(0, rc);
/* THE central regression proof (task 2.1's own scope, Req 1.3/1.4):
* before this task, this count is 0 (sim_loop.c never called
* sched_account_jobs() at all, bug-1613) -- after, it is 2, one per
* replayed round, not merely once ever. */
TEST_ASSERT_EQUAL_INT(2, atomic_load(&g_spy_calls));
for (i = 0; i < 2; i++) {
TEST_ASSERT_TRUE_MESSAGE(
g_spy_session_active_ok[i],
"sched_account_jobs() must run with core->session_active==1 -- "
"i.e. strictly after xce_core_begin_session(), strictly before "
"xce_core_end_session(), matching driver.c's own ordering "
"contract");
}
printf("test_sim_replay_calls_account_jobs_once_per_round_within_"
"session_window: PASS\n");
}
/*
* test_sim_replay_still_completes_interactive_and_batch_identically -
* Requirement 5.1: interactive and batch simulation runs must go through
* the SAME round-boundary call site with no divergence. sched_sim_driver_run()
* is the one entry point BOTH `xce_scheduler -s` (batch) and `-i -s`
* (interactive) call -- gated only by sim->interactive inside REPL hook
* calls this file's own fixture never triggers (scheduler/CLAUDE.md: "drives
* the exact same sched_sim_driver_run() as batch --sim, gated only by
* sim->interactive"). This test simply confirms the round-boundary spy
* fires identically whether sim->interactive is left at its default 0
* (batch) -- there is no separate call site to diverge from; this is a
* structural guarantee proven by the single call site itself, re-asserted
* here as an explicit regression pin rather than left implicit.
*/
static void test_sim_replay_batch_mode_uses_the_same_round_boundary(void)
{
char record_dir[PATH_MAX];
xce_scheduler_t sched;
char r_path[PATH_MAX];
xce_job_t** jobs1;
sched_sim_args_t sim_args;
xce_sched_sim_driver_t sim;
int rc;
make_tmp_dir(record_dir, sizeof(record_dir), "_b");
fixture_init(&sched, record_dir);
rc = sched_record_start(&sched, "batch.replay");
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < sizeof(r_path));
strcpy(r_path, sched.record_file_path);
enqueue_queue_update(&sched, "normal", 1);
jobs1 = (xce_job_t**)calloc(1, sizeof(xce_job_t*));
jobs1[0] = make_job(9301ULL, 1000ULL, "normal", "carol");
enqueue_jobs_push(&sched, jobs1, 1);
run_one_real_session(&sched);
rc = sched_record_stop(&sched);
TEST_ASSERT_EQUAL_INT(0, rc);
reset_spy();
memset(&sim_args, 0, sizeof(sim_args));
sim_args.replay_path = r_path;
sim_args.sim_start_ts = SCHED_SIM_START_TS_LATEST;
sim_args.sim_speed = 0;
memset(&sim, 0, sizeof(sim));
TEST_ASSERT_EQUAL_INT(0, sim.interactive); /* batch: default-zero, never set */
rc = sched_sim_driver_run(&sim, &sim_args);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, atomic_load(&g_spy_calls));
TEST_ASSERT_TRUE(g_spy_session_active_ok[0]);
printf("test_sim_replay_batch_mode_uses_the_same_round_boundary: "
"PASS\n");
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(
test_sim_replay_calls_account_jobs_once_per_round_within_session_window);
RUN_TEST(test_sim_replay_batch_mode_uses_the_same_round_boundary);
return UNITY_END();
}

View file

@ -130,6 +130,17 @@ int sched_fill_limit_requests(xce_scheduler_t* sched, xvec_idval_t* vec,
return 0;
}
/* sched_sim_driver_run()'s (sim_loop.c, scheduler-sim-decision-parity task
* 2.1) new round-boundary call to the relocated sched_account_jobs()
* (core.h) -- a no-op stand-in here since this file does not link
* scheduler.c/resource.c, same rationale test_sim_equivalence.c's own copy
* documents. */
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
/* Real reimplementation of schedobj.c's sched_resolve_job_picker -- same
* copy test_sim_event_loop.c/test_sim_startup.c/test_core_full_cycle.c each
* carry (schedobj.c is not linked here: it would need plugin_register(),