- src/schema/controller.capnp: JobSubmitData gains
fromHost @10: Text $C.mapname("from_host") -- auto-regenerates
controller.capnp.c/h via the existing meson capnpc-c custom_target.
- src/include/xce/types.h: job_submit_data_t gains char* from_host.
- src/lib/jobj.c: jobsub_init/jobsub_destroy (the hand-written,
ref-counted, content-deduped job-submit interning object -- not
auto-generated) thread from_host through the deep-copy, the dedup
identity comparison, and the destroy path.
Closes the wire-format half of the round-trip: from_host now survives
from the apiserver's submission all the way through the controller and
back on every subsequent job-state push, and rides along for free in
the existing WAL_JOB_SUBMIT persistence across a controller restart.
Did not need to touch job.c's build_job_submit_payloads() (bare
pointer borrow of the already-decoded submit struct) or any WAL
apply-path code (capnp_encode_job/decode_Job already generically
handle the whole xce_job_t including its full .submit sub-struct).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
CreateJob now does FromHost: req.FromHost, a straight passthrough of
whatever xcecli (or any other client) sends in JobSubmitRequest.FromHost,
instead of deriving it server-side via ctx.ClientIP().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
JobSubmitRequest gains a FromHost field, populated in BuildJobRequest()
via os.Hostname(). Matches how traditional batch schedulers report the
submitting host, and sidesteps having the server derive it from the
connection (which apiserver was doing via ctx.ClientIP() -- an IP not
a hostname, and spoofable via X-Forwarded-For since this apiserver
never configures gin's TrustedProxies).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
OnHostConfigUpdated only applied config-sourced fields (MaxSlot,
HostModel, HostType, DisptWindow) onto a host whose name exact-matched
an entry already in the store. hosts.yaml commonly defines only a
wildcard "default" entry, so a dynamically-joined worker never
received its own config push and MaxSlot stayed at Go's zero-value
forever -- decoupled from the C-side scheduling path, which already
resolves the real value via worker.c's wildcard fallback.
The "default" pseudo-host's config is now cached and retroactively
backfilled onto any registered host with no explicit config of its
own, in either arrival order (config-before-worker or
worker-before-config). The -1 HOSTCONFIG_MXJ_AUTO_DETECT sentinel
(bug-1556) is resolved to the host's real NumCPUs when applied via
fallback, mirroring worker.c's own resolution, so the display shows
usable capacity rather than a bare -1.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
GetJobsInfo/GetJobByIDs validated the -o/-l field list against
models.JobSubmitRequest{} -- the job-submission payload -- instead of
models.JobInfo{}, the query/response model (the already-correct
pattern used elsewhere in this file at line 1131). Response-only
fields like pending_reason don't exist on JobSubmitRequest, so any
such field made the whole listing fail outright with "unsupported
query field", not just omit that column.
core_limit/mem_limit exist on JobSubmitRequest but not on JobInfo --
switching wholesale would have newly broken those two previously-valid
fields. Added CoreLimit/MemLimit to JobAdditionalFields, the existing
merge-in bucket already used for pids/pgids/interactive_job_url.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
sync_bugs() read buglog.json as a bare list and wrote it back the
same way, but the real format is {"version": 1, "bugs": [...]}. Any
real run would have crashed on the first bug entry (enumerate() over
a dict yields its keys, not bug objects) and, had it somehow gotten
past that, a write-back would have destroyed the wrapper. Never
actually run against production Gitea, per the buglog entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
ExtractErrorMessage()'s connection-refused branch returned the bare
OS error string verbatim. A CLI call right after apiserver startup
(before its listener is up) surfaced a raw "get the request failed:
[connection refused]" with no indication it's a transient startup
race, not a real failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
The prior verification pass (commit 1c9cfc1, same session) restarted
controller+scheduler+worker but left the apiserver running, so it only
ever saw the apiserver's stale, still-correct in-memory job cache from
before the restart -- never the controller's actual post-recovery
state. Repeating the test with the apiserver also restarted (matching
the original report's "restart all daemons" exactly) reproduces the
bug immediately: EXEC_HOST stays permanently blank across 15 polls/30s
while STATUS correctly shows RUN throughout.
Root cause fully traced: dispatch_a_job_() (job.c:1343-1372) sets
job.allocs in memory at dispatch-commit time but the durable
WAL_JOB_DISPATCH fact (persist_submit_job_fact, wal_jobstate_t
{jobid,state}) never carries the allocation. On a full restart,
restore_job() replays only the pre-dispatch WAL_JOB_SUBMIT payload
(allocs empty by construction) and apply_committed_entry()'s
WAL_JOB_DISPATCH case routes through apply_job_update_transition(),
which by design "only ever touches state" -- job.allocs is never
reconstructed. Controller log corroborates: recovery trace shows the
WAL_JOB_DISPATCH replay applying silently (no log on the APPLIED
path), followed by each job's WAL_JOB_START fact correctly logging a
now-redundant "duplicate UPDATE ... no-op".
Fix direction confirmed with the user: WAL_JOB_DISPATCH's payload
needs to carry the scheduler's resource allocation, not just
{jobid, state}, with a corresponding decode/apply path in
apply_committed_entry() -- not implemented here, this is a
reproduce-and-root-cause pass only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
Live-reproduced and root-caused on branch scheduler-framework via a
single-node deploy:
- bug-1673 (gitea #40): apiserver hosts-list MAX_SLOT stuck at 0 for
workers not exact-name-matched in hosts.yaml -- display-only, C-side
scheduling correctly uses the wildcard 'default' entry's slot count.
Reporter's secondary "hard 8-job cap" claim did NOT reproduce.
- bug-1674 (gitea #41): `xcecli jobs list -l` fails outright (not just
missing timestamp fields as reported) -- two call sites validate
field lists against JobSubmitRequest instead of JobInfo.
- bug-1675 (gitea #42): connection-refused right after apiserver
startup surfaces a raw OS error string instead of a friendly message.
- bug-1676 (gitea #44): FROM_HOST always blank -- capnp schema has no
from_host field, and OnJobUpdated's blind store overwrite wipes the
apiserver-local value set at creation on the first controller push.
Gitea issue #43 (EXEC_HOST lost after daemon restart) was NOT
reproduced after two restart cycles with WAL-preserving state --
EXEC_HOST/state recovered correctly in every poll. Not logged as a bug;
a related design gap (WAL_JOB_DISPATCH only carries {jobid, state}, not
allocs) was flagged as an unconfirmed latent risk, not a live finding.
bug-1677: scripts/gitea-sync.py's sync_bugs() reads buglog.json as a
bare list but the real format is the {version, bugs: [...]} wrapper --
would crash on any real run. Found via static review while checking
the gitea_issue cross-reference field convention; not executed live.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
src/lib/controller.capnp.{c,h} are AUTO GENERATED (regenerated from
src/schema/controller.capnp by the build), never checked in before,
and were showing up as untracked noise every session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
- build-time-versioning, dirty-content-fingerprint: both fully
implemented (21/21 tasks) with all three approvals granted, but
spec.json.phase was never advanced past tasks-generated. Flipped to
implementation-complete.
- resource-value-verification, scheduler-core, component-failover-adoption:
every "unchecked" task was a section-header checkbox ("- [ ] N. ...")
whose numbered subtasks (N.1, N.2, ...) were already 100% [x] --
the header itself was just never flipped when its children finished.
Checked all 23 such headers (8/10/5 respectively) now that every
child under each is confirmed done. No actual unfinished work in
any of the three.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
39 duplicate IDs (one tripled: bug-909) in the bug-875-bug-949 range,
where a restarted counter re-used IDs already held by June 21-22
entries. Kept each original (earliest timestamp) untouched and
renumbered the 40 later collisions to bug-1633..bug-1672 (new global
max, up from 1632), preserving array order. Verified no related_bugs
reference pointed at a renumbered entry -- the 10 existing references
to bug-924 all predate the Aug 17 collision and correctly resolve to
the original ENOSPC/overlayfs entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
Committed at 90be04bd. Dashboard is LAN-only and never exposed
externally, so no history scrub -- just stop tracking the token file
so it's not re-added on the next wolf bookkeeping commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
New scheduler-sim-eager-reconciliation requirements spec; weekly_report skill;
weekly status report for Aug 16-22. Also records cross-spec dependency notes
on scheduler-allocation and scheduler-preemption reflecting how
scheduler-dispatch-commit's redefined RUN-state timing affects their designs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
All 12 tasks implemented, independently reviewed, and committed.
Final cross-task validation (kiro-validate-impl): GO -- 18/18
acceptance criteria mapped and code-verified, no boundary spillover,
no design drift. Full regression clean (scheduler/controller suites
at documented pre-existing baselines, sequential + chaotic HA soak
both PASS). Unblocks scheduler-allocation per this spec's own
sequencing_note.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
Fixes test_commit_barrier_reply.c's test_jobdispatch_receipt_ack_no_wal_append
(owned by ha-commit-barrier-rollout), the last remaining assertion this
feature's earlier tasks left stale:
- WAL-count delta corrected from "must be zero" to lsn_before + 1 -- the
batch's successful dispatch now appends one WAL_JOB_DISPATCH fact
(task 3.1); mark_job_fail() was traced and confirmed to append
nothing via any path.
- The batch's terminal-failure job was replaced with a genuinely
retryable one (WORKER_NULL site, unregistered host, new job id)
rather than degrading the notify-count assertion to zero, which
would also have silently killed the test's own reply-before-notify
ordering pin (nothing left to order against). This keeps the test
meaningfully exercising notify_dispatch()'s wiring through
handle_jobdispatch().
Full regression, independently re-verified by review:
- Scheduler suite at documented baseline (51/52, 1 pre-existing
unrelated failure). Investigated and logged bug-1632, a new sibling
of the already-known bug-1598 stack-lifetime pattern in a different
test file -- confirmed pre-existing, untouched by this feature, and
non-deterministic.
- Controller suite clean: 1133 tests, only the 2 pre-existing bug-1561
failures.
- Sequential HA soak: PASS (10 rounds, 36 jobs, zero loss, all 3 nodes
led).
- Chaotic HA soak: PASS (10 rounds, committed_divergence=0
committed_job_loss=0).
This completes the scheduler-dispatch-commit spec: bug-1624
(resource-in-use round-boundary loss) and bug-1625 (scheduler discards
dispatch-failure notify) are both fixed and regression-tested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
Adds test_handle_jobdispatch_terminal_no_notify_no_leak to
test_jobdispatch_no_double_notify.c, closing a gap neither existing
test fully covered: test_dispatch_fail_terminal.c (task 3.2) bypasses
handle_jobdispatch() entirely via dispatch_a_job_test(), so it can't
exercise the double-notify race at all; test_jobdispatch_no_double_notify.c
(task 4.1) only proves an aggregate 3-job-batch notify count, not a
direct per-job zero-notify proof.
The new test drives a single terminal-classified job through the real
handle_jobdispatch() entry point and asserts, in one run: JOB_STAT_EXIT
with allocs left NULL, a direct (non-aggregate) zero-count read from
the notify-capture spy proving no jobDispatchFail was produced for this
job, and (via task 3.2's canary-reference technique, armed before the
call under test) no reference over-release.
Test-only; job.c/handler.c are untouched. RED phase verified via an
actual revert-rebuild-rerun of handle_jobdispatch() to its pre-task-4.1
shape. Independently reviewed and approved -- the reviewer notably
declined to trust a relayed build report and re-ran verification
first-hand before approving.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
Adds test_rpc_failure_bound_resets_after_intervening_success to
test_dispatch_fail_retry.c, proving Requirement 4.3's literal claim
end-to-end through the real dispatch_a_job_test() entry point: a fail,
then a real success, then another fail resets the consecutive-failure
counter so the second failure is evaluated as failure #1, not #2.
The first draft of this test also asserted job.state/job.allocs revert
to PEND/NULL on that post-success failure -- this genuinely fails
against current code (none of dispatch_a_job_()'s four retryable
branches revert state/allocs when failing from an already-RUN job,
since they were only ever designed for a job whose every attempt starts
from PEND). A debug investigation, independently re-verified even more
exhaustively during review (full trace of every pendJobs-reinsertion
and controller-side state-mutation site), confirmed this scenario is
unreachable via any real caller today -- the scheduler's picker
permanently removes a job from its PEND pool the instant it dispatches
it, and nothing already-implemented ever re-adds a RUN job to it. Only
the not-yet-implemented scheduler-preemption spec could someday create
that path. Logged as bug-1631, deliberately deferred rather than fixed
now (no reachable caller to regression-test against); the two
over-reaching assertions were trimmed, keeping only the counter proof
Requirement 4.3 actually asks for. Independently reviewed and approved.
Scenarios 1 (retryable -> PEND-eligible) and 2 (bound exceeded -> EXIT)
were already fully covered by tasks 2.2's and 3.3's existing tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
Extends task 2.1's test_scheduler_dispatch_commit.c with two tests
proving resource-in-use accounting survives more than a single round
boundary between dispatch and worker confirmation, per task 5.1's
"several rounds" / "matching a same-round confirmation" language:
- test_dispatch_commit_survives_several_rounds_before_confirmation:
loops 5 round-boundary cycles, asserting resInUse after EVERY round,
not just once.
- test_dispatch_commit_multi_round_gap_matches_same_round_confirmation:
compares resInUse after 1 round vs after 5 rounds and asserts
equality -- a same-round confirmation is modeled as nrounds==1 since
design.md documents re-applying JOB_STAT_RUN to an already-RUN job as
a confirmed no-op, so resInUse depends only on job->state, never on
confirmation timing.
Test-only; scheduler.c is untouched. RED phase independently verified
by both implementer and reviewer via an actual revert-rebuild-rerun
cycle (4/5 tests fail without the task 2.1 fix). Independently reviewed
and approved.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
dispatch_a_job_() (tasks 3.1-3.3) now owns notification entirely for
every outcome -- notify_dispatch() on a retryable failure, mark_job_fail()'s
own hook chain on a terminal failure, nothing on success. handle_jobdispatch()'s
loop still unconditionally called notify_dispatch() itself for every job
regardless, producing two conflicting outcome reports for any failed
dispatch. Removed that caller-side call; the loop is now a thin
dispatch-and-free loop with no notification logic of its own.
New test_jobdispatch_no_double_notify.c drives a 3-job batch (success,
retryable, terminal) through handle_jobdispatch() and confirms exactly
one notification fires (from the retryable job only), not four.
Independently reviewed and approved. Confirmed handle_jobdispatch()'s
sole caller never depended on its notification behavior (always returns
0 unconditionally). Flagged that test_commit_barrier_reply.c's
test_jobdispatch_receipt_ack_no_wal_append is now stale for a second,
independent reason (notify count, not just WAL count) -- both left for
task 5.4 per Requirement 5.3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
The four retryable-classified failure sites in dispatch_a_job_() (worker
lookup, worker null, worker not ready, RPC failure) now resolve instead
of just returning an error code: lock(j, RTTI_LOCK_WRITE) -> genuinely
classify via xce_classify_dispatch_fail() -> increment dispatch_fail_count
-> within XCE_DISPATCH_FAIL_RETRY_BOUND (3) resolves retryable (nothing
further under the lock), past it resolves terminal via mark_job_fail()
exactly like task 3.2's sites -> unlock(j) -> dereference(j) (closing
bug-1627's 4-site reference leak) -> outside the lock, notify_dispatch()
fires only on the retryable outcome. Each site's natural error code is
captured into a stable local before any intervening call, since errno
isn't guaranteed stable once lock()/ERROR()/free_launchinfo() sit between
the original failure and the eventual return.
handle_jobdispatch() is intentionally untouched -- it still unconditionally
calls notify_dispatch() itself for every outcome, so the retryable path
now temporarily double-notifies until task 4.1 removes that caller-side
call. Expected, not a defect.
New test_dispatch_fail_retry.c (4 tests, including a bounded-escalation
test driving 4 consecutive RPC failures on the same job: 3rd retries,
4th resolves EXIT) plus a fixed stale assertion in test_dispatch_commit.c
and additive, read-only notify-capture accessors in
test_commit_barrier_reply.c (the binary's sole notify() stub host).
Independently reviewed and approved, including explicit scrutiny of the
cross-file test accessor addition and the stale-assertion fix -- both
confirmed legitimate, not scope creep or regression-hiding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
The two terminal-classified failure sites in dispatch_a_job_() (invalid
allocation, queue-exec-info lookup failure) now actually resolve the job
instead of leaving it stuck at PEND forever with a dangling reference to
nowhere. Each site: lock(j, RTTI_LOCK_WRITE) -> genuinely calls
xce_classify_dispatch_fail(site) and branches on the result (not a
hardcoded assumption) -> mark_job_fail() under the lock (stage=0 routes
apply_job_fail_transition() to JOB_STAT_EXIT) -> unlock(j) -> the existing
dereference(j) (kept exactly where it was, not duplicated) -> the existing
return code, unchanged. No notify_dispatch() added -- mark_job_fail()'s
own hook chain is the sole notification for a terminal outcome per
design.md; handle_jobdispatch()'s still-unconditional notify_dispatch()
call is untouched (task 4.1's job).
New test_dispatch_fail_terminal.c proves both sites resolve to
JOB_STAT_EXIT with allocs left NULL, using a canary-reference technique
to prove no reference over-release (job_t's real refcount has no test
accessor). Independently reviewed and approved; applied the reviewer's
one non-blocking fast-follow (arm the canary before the call under test,
not after, so it's an independent proof rather than relying on other
assertions to crash first) and re-verified with a clean incremental build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
dispatch_a_job_()'s previously-unconditional job.allocs commit is now
gated on xce_startjob_req() actually succeeding (rc >= 0). In that same
successful branch, under a newly-acquired lock(j, RTTI_LOCK_WRITE) (a
real pre-existing locking gap this call site had -- it took no lock on
the job object itself before this), the job transitions to JOB_STAT_RUN,
dispatch_fail_count resets to 0, and a WAL_JOB_DISPATCH fact is appended
for durability. The failure path is otherwise unchanged -- classification
and resolution of the six failure sites is tasks 3.2/3.3's job, not this
one's.
Adds an XCE_TESTING-only dispatch_a_job_test() wrapper (dispatch_a_job_()
is static) and test_dispatch_commit.c, proving: state->RUN, allocs
committed correctly, counter resets, and durability survives a real
persist_recover() replay with no WAL_JOB_START present.
Independently reviewed and approved. Two pre-existing, out-of-boundary
findings surfaced and logged for later tasks: test_commit_barrier_reply.c's
test_jobdispatch_receipt_ack_no_wal_append will need updating by task 5.4
(Req 5.3 anticipates this), and bug-1429's hang was reconfirmed a 3rd time
(unrelated to this change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
Adds a case for NotifyData_data_jobDispatchFail to sched_handle_notify()'s
switch, fixing bug-1625 -- the notify was previously silently discarded
(falling to default: -EINVAL), leaving job->accounts/job->allocs stale
forever on any dispatch failure. No classification logic here: the
controller has already decided PEND is correct before sending the notify;
this reuses the same XCE_SCHED_OP_JSTATE_CHANGE enqueue path the adjacent
schedExitJob/schedFailJob cases already use, and relies on core_events.c's
existing free-on-non-RUN branch to release the stale accounts/allocs.
Independently reviewed and approved: full scheduler suite passes (51/52,
one pre-existing unrelated flake -- bug-1560).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
sched_commit_decision()'s DISPATCH branch now sets job->state = JOB_STAT_RUN
in the same operation that already commits job->allocs/job->accounts, per
Requirement 1.1/1.2/1.3. sched_account_jobs()'s existing JOB_IS_RUN gate is
unmodified -- this closes bug-1624 by giving it the input it was missing: a
dispatched job now survives a round boundary in resource-in-use totals
regardless of how many rounds pass before worker confirmation arrives.
Regression test drives the real, unmodified sched_account_jobs() across a
simulated round boundary (resInUse reset, no confirmation event) and
independently reverting the one-line change reproduces bug-1624 exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add uint32_t dispatch_fail_count to the job_t wrapper struct (xce/types.h,
alongside submit_lsn, same in-memory-only/reset-on-failover precedent),
per Requirement 4.3. Data model only -- xce_job_t (wire type) untouched,
job.c untouched; increment/reset wiring and the bounded-retry threshold
check are tasks 3.1/3.3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New wal_entry_type WAL_JOB_DISPATCH=12, reusing wal_jobstate_t verbatim
(_pad=0, absolute-value semantics), wired into both apply_wal_entry()'s
and apply_committed_entry()'s existing job-state fall-through groups per
Requirement 2.4. Restart-replay test writes only WAL_JOB_SUBMIT+
WAL_JOB_DISPATCH to a real on-disk WAL and confirms job.state==RUN after
a fresh persist_recover(), independent of WAL_JOB_START.
Review found apply_wal_entry() has zero production call sites today
(persist_recover() and standby catch-up both route through
apply_committed_entry() post the 2026-07-10 ha_apply_single_entry
refactor) -- design.md's stale characterization corrected; the dual
case-label registration itself is unaffected and correct.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Define xce_dispatch_fail_site_t/xce_dispatch_fail_class_t and
xce_classify_dispatch_fail() in job.c, mapping each of dispatch_a_job_()'s
six post-lookup failure points to retryable or terminal per Requirement
4.1/4.2. Standalone, not yet wired into dispatch_a_job_() (deferred to
tasks 3.2/3.3). Also adds this spec's requirements/design/research/tasks
docs (first commit touching this spec).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The original motivating recording (scheduler-replay-20260820-070103.replay,
named in bug-1611) no longer exists anywhere on disk -- confirmed via
exhaustive filesystem search. Substituted a real, driven 24-round/12-job
resource-constrained recording using actual driver.c/builtin_fcfs/resource.c
code, replayed through the real, unmodified interactive REPL.
This also surfaced and honestly documents a real structural limit:
Recorded-Decision Reconciliation only succeeds when a job's recorded
DISPATCH decision and its JOB_STATE_CHANGE-to-RUN confirmation land in the
SAME replay round (reconciliation searches only that round's own decoded
data). Constructing that exact coincidence from organic, continuous
scheduling is structurally impossible -- a job can't be both "freshly PEND
for this round's picker" and "already RUN before this round's picker runs"
at once. One job (ANCHOR_ID) engineers this coincidence using entirely
real constructors (matching the same precedent tasks 3 and 4.1 already
established); the other 11 jobs arrive/contend/dispatch/finish with zero
special-casing across all 24 real rounds. Recorded in tasks.md's
Implementation Notes as a scope finding for follow-up: in a real deployment
with genuine dispatch-to-confirmation latency, this narrows the feature's
real-world coverage of bug-1611/1613's dominant manifestation.
RED phase: reverting task 3's reconciliation call site makes this test
fail with the exact bug-1611 symptom in its own assertion message, plus 3
sibling tests break too; restored and green. No production code touched
(final task in the feature -- all production work landed in task 3).
50/51 scheduler tests pass; the 1 failure is pre-existing bug-1560
(unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four checks in one run: (a) REPL-vs-batch decision equivalence unmodified
and still passing; (b) --sim-verify reports no false mismatch for a
recording with multiple already-resolved jobs across rounds (non-adjacent
memmove compaction, no cross-round bleed); (c) a replay starting from a
snapshot with a job already RUN-state and no in-window recorded decision
degrades gracefully (no crash, no fabricated value, a second ordinary job
unaffected) -- the first test to exercise reconciliation's "not found"
path through the real sched_sim_driver_run()/xce_sched_core_load_snapshot()
path rather than a hand-built core; (d) normal-mode behavior is byte-for-
byte unchanged (zero production diff, the strongest possible evidence).
Found and fixed a real test-authoring gap while building (b): builtin_fcfs
re-offers a still-PEND job as a fresh PEND every round (confirmed
pre-existing, unrelated, also visible in test_repl_equivalence.c's own
fixture) -- the first draft's round-2 fixture omitted this recurrence.
No production code touched (task 3 already completed the feature) --
git diff on sim_loop.c/resource.c/scheduler.c/driver.c/sim_verify.c is
empty. 49/50 scheduler tests pass; the 1 failure is pre-existing bug-1560
(unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds an integration test comparing round-by-round resInUse and scheduling
outcomes between a real driven normal-mode recording and a simulation-mode
replay of the identical contention scenario (worker capacity 10.0, jobs
wanting 8.0/5.0/2.0, plus a 4th job proving cross-round persistence).
Confirmed (by direct read of sim_picker_thread_fn(), independently
re-verified in review) that sim's live picker is structurally PEND-only --
it has no capacity-check/dispatch path for a freshly-evaluated job. This
means task 1.2's single-round fixture cannot be literally replayed-and-
diffed for full parity: that comparison would be tautological for
freshly-arriving jobs (sim always PENDs them regardless of accounting
correctness) and can't prove multi-round persistence anyway. Built instead
as a real recording plus a hand-crafted sim fixture modeling the identical
facts, including an already-resolved-before-replay job to exercise
reconciliation -- the same documented-carve-out approach test_sim_equivalence.c
already established for PEND-only proofs, applied here to reconciled jobs too.
No production code touched (task 3 already completed the feature). RED
phase proven by temporarily reverting task 3's reconciliation call site:
new test failed with "Expected 10 Was 0", restored and green.
48/49 scheduler tests pass; the 1 failure is pre-existing bug-1560
(unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This is the integration point that makes the whole feature real
end-to-end. Calls sim_reconcile_unaccounted_jobs() (tasks 2.2-2.4) at
exactly the position design.md's mermaid diagram requires: after
xce_core_begin_session()'s reset, strictly before sched_account_jobs()
(so the same round's re-charge picks up every newly-reconstructed job,
Req 1.3/1.4) and before xce_core_signal_go() unblocks the live picker (so
still-pending jobs' capacity checks this round see correct resInUse, Req
2.1/2.2). decoded_session and record_ctx are the same per-round locals
sched_sim_driver_run() already declares -- no new plumbing.
sim_verify.c needs zero changes: reconciliation consumes/compacts
decoded_session->decisions before the verify drain loop runs each round,
which is what keeps the existing positional comparison correct (Req 5.2)
-- confirmed by extending test_sim_verify.c with a no-false-mismatch test
against a recording containing already-resolved jobs.
New end-to-end test proves the ordering matters for real: a reconciled
job's resInUse is charged before a live sched_check_host_satisfy() call
for a different job in the same round correctly denies it (5.0 wanted vs
2.0 remaining) and grants an exact-fit request (2.0) -- using the real
scheduler.c/resource.c accounting and capacity-check code, not stubs.
Found and fixed a test-authoring bug (get_resvals() returns nres on
success, not 0) during GREEN verification -- logged as bug-1622, a test
bug, not production.
47/48 scheduler tests pass; the 1 failure is pre-existing bug-1560
(unrelated).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>