docs(spec): libxce-alloc-preason-equality review fixes; new scheduler-sim-discrete-clock spec
libxce-alloc-preason-equality (requirements, design, tasks; not yet approved): - alloc_eq compares `from` and excludes only instid; the scheduler stamps `from` with the deciding session's start, the same clock reading that is recorded as beginRound, so a replay reproduces it. - every comparator takes an optional char** why: a malloc'd description of the first difference, owned and freed by the caller; --sim-verify mismatch lines print it (resolves bug-1881). - job_preason_eq is new full-match behavior, not a relocation (bug-1845 keeps the simulator main-reason-only); the three white-box test files switch to it and keep their sim_verify.c include. - alloc_eq stays separate from merge_job_alloc()'s same_alloc_slot() (bug-1863), which does match on instid. scheduler-sim-discrete-clock (initialized): remove --sim-speed so the simulator clock moves only at recorded timestamps and a replayed session_start equals the recorded beginRound exactly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f51a4997d
commit
dcf54387ae
6 changed files with 335 additions and 157 deletions
|
|
@ -2,123 +2,169 @@
|
|||
|
||||
## Overview
|
||||
|
||||
**Purpose**: This feature delivers a single, shared implementation of structural equality for the scheduler's allocation and pending-reason data structures, published as public libxce API.
|
||||
**Purpose**: This feature delivers a single, shared implementation of structural equality for the scheduler's allocation and pending-reason data structures, published as public libxce API, that can explain *why* two objects differ, together with a reproducible allocation start time (`from`) so that `from` can take part in equality.
|
||||
|
||||
**Users**: Scheduler engineers (today: `--sim-verify` replay checking and three scheduler test files) and, in future, controller engineers (`persist.c`, `resync.c`, `job.c` already carry these structs) who need to ask "do these two objects represent the same thing".
|
||||
**Users**: Scheduler engineers (today: `--sim-verify` replay checking and three scheduler test files) and, in future, controller engineers (`persist.c`, `resync.c`, `job.c` already carry these structs) who need to ask "do these two objects represent the same thing, and if not, where do they differ".
|
||||
|
||||
**Impact**: Changes the current state by extracting four `static` comparison functions out of `src/scheduler/src/sim_verify.c` and into `src/lib/xcealloc.c`/`src/lib/preason.c`, and by switching the simulator and three white-box test consumers to the public API. The allocation comparators keep their documented `from`/`instid` exclusion; the pending-reason comparator becomes a full structural match (all fields, detail lists included). The decision-level dispatcher (`sim_decision_eq`) and mismatch reporting (`sim_log_mismatch`) stay in the scheduler, and `sim_decision_eq`'s PEND branch keeps a simulator-local main-reason-only comparison (bug-1845). No wire format or struct layout changes.
|
||||
**Impact**:
|
||||
- Moves the three `static` allocation comparators out of `src/scheduler/src/sim_verify.c` into `src/lib/xcealloc.c`. `alloc_eq` now compares `from` and excludes only `instid`.
|
||||
- Adds a **new** full-structural-match pending-reason comparator to `src/lib/preason.c`. No full-match comparator exists today; bug-1845 narrowed the simulator's to main-reason-only.
|
||||
- Gives every comparator an optional `char** why` out-parameter that returns a newly allocated description of the first difference, owned by the caller. `sim_log_mismatch` prints it (resolves bug-1881).
|
||||
- Changes the scheduler to stamp `from` with the scheduling session's start time, a single clock reading shared with the recorded `beginRound`, instead of `time(NULL)`. That a replayed session starts at exactly the recorded `beginRound` comes from the separate spec `scheduler-sim-discrete-clock` (removes `--sim-speed`; the simulator clock moves only at recorded timestamps).
|
||||
- Switches the simulator and three white-box test consumers to the public API. The three test files' checks become stricter: full-match pending reasons (Decision 7) and `from`-inclusive allocations.
|
||||
- `sim_decision_eq` and `sim_log_mismatch` stay in the scheduler, and `sim_decision_eq`'s PEND branch keeps a simulator-local main-reason-only comparison (bug-1845). No wire format or struct layout changes.
|
||||
|
||||
### Goals
|
||||
- Publish `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` in `xcealloc.h` and `job_preason_eq` in `preason.h`.
|
||||
- Publish `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` in `xcealloc.h` and `job_preason_eq` in `preason.h`, each with an optional first-difference description.
|
||||
- Make `job_preason_eq` a full structural match (main reason + host-based reasons + resource-based reasons, in order).
|
||||
- Make the existing consumers use the public API, removing the white-box `#include` dependency on `sim_verify.c` for the type-level comparisons.
|
||||
- Preserve `--sim-verify` behavior bit-for-bit (no new false/missed mismatches), which means the simulator's PEND check remains main-reason-only.
|
||||
- Add libxce unit tests that pin the allocation-exclusion, full-match preason, and NULL-handling semantics.
|
||||
- Make `from` reproducible under replay, then compare it.
|
||||
- Make `--sim-verify` mismatch logs name the differing field and values.
|
||||
- Make the existing consumers call the public API instead of `sim_verify.c`'s private `static` comparators. (The three test files keep their white-box `#include "../src/sim_verify.c"`: they also white-box-include `sim_loop.c`, which calls `sim_decision_eq`/`sim_log_mismatch`.)
|
||||
- Keep `--sim-verify` free of false mismatches for recordings made after this change.
|
||||
- Add libxce unit tests that pin the `instid` exclusion, `from` inclusion, full-match preason, description, and NULL-handling semantics.
|
||||
|
||||
### Non-Goals
|
||||
- Do not move `sim_decision_eq`, `sim_log_mismatch`, or the `describe_*` helpers (simulator-specific; depend on capnp/replay types and `ERROR()`).
|
||||
- Do not change the simulator's PEND outcome check from main-reason-only to full-match — that would reintroduce bug-1845's false mismatches on cache-hit PENDs (see Decision 3).
|
||||
- Do not introduce any new comparison call site beyond the existing four consumers.
|
||||
- Do not touch the controller's persistence/resync code paths.
|
||||
- Do not touch the controller's persistence/resync code paths, or how the controller/worker interpret `from`.
|
||||
- Do not change `make_alloc()`'s signature or its own `time(NULL)` default (controller and many tests call it); the scheduler overwrites `from` after construction (Decision 8).
|
||||
- Do not touch `merge_job_alloc()`/`same_alloc_slot()` (bug-1863); `alloc_eq` neither replaces nor reuses `same_alloc_slot` (see Decision 6). Their NULL-dereference defect is bug-1882.
|
||||
- Do not remove the test files' white-box `#include "../src/sim_verify.c"`.
|
||||
- Do not make recordings made before this change verify cleanly; their wall-clock `from` values are an accepted limitation.
|
||||
- Do not change the simulator's clock or `--sim-speed`; that is spec `scheduler-sim-discrete-clock`, which this spec depends on.
|
||||
|
||||
## Boundary Commitments
|
||||
|
||||
### This Spec Owns
|
||||
- The public equality functions `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` (`xcealloc.{c,h}`) and `job_preason_eq` (`preason.{c,h}`), and their documented equality contracts.
|
||||
- The public equality functions `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` (`xcealloc.{c,h}`) and `job_preason_eq` (`preason.{c,h}`), their equality contracts and their difference-description format.
|
||||
- The shared NULL-safe string-equality helper both modules need (see Decision 5).
|
||||
- The migration of `sim_verify.c` and the three white-box test files onto those functions.
|
||||
- libxce unit tests for the new functions (`test_xcealloc.c`, `test_preason.c`).
|
||||
- The scheduler's stamping of `from` (scheduler.c) and the single normal-mode session-start reading shared by `core->session_start`, the recorded `beginRound`, and `from` (driver.c).
|
||||
- The migration of `sim_verify.c` and the three white-box test files onto those functions, and the difference text in `sim_log_mismatch`.
|
||||
- libxce unit tests for the new functions (`test_xcealloc.c`, `test_preason.c`) and scheduler tests for `from` stamping.
|
||||
|
||||
### Out of Boundary
|
||||
- The decision-level comparison and mismatch reporting that remain in `sim_verify.c`, including the main-reason-only PEND outcome check (simulator-local).
|
||||
- The decision-level comparison and mismatch reporting structure that remain in `sim_verify.c`, including the main-reason-only PEND outcome check (simulator-local).
|
||||
- The `.replay` wire format and the `replay_decision_t`/`xce_decision_t` types.
|
||||
- Any change to the controller's consumption of these structs.
|
||||
- `merge_job_alloc()` and its `same_alloc_slot()` slot-matching helper in `xcealloc.c`.
|
||||
- The simulator clock and `--sim-speed` (spec `scheduler-sim-discrete-clock`).
|
||||
|
||||
### Allowed Dependencies
|
||||
- `src/include/xce/types.h` (the struct definitions — unchanged).
|
||||
- `xcealloc.c`/`xcealloc.h`, `preason.c`/`preason.h`, and `utils.{c,h}` (the modules that already own construction of these structs, plus the shared string helper).
|
||||
- `string.h` (`strcmp`) for the string-field comparisons.
|
||||
- `string.h` (`strcmp`) and `stdio.h` (`vasprintf`) for comparisons and descriptions.
|
||||
- Scheduler: `driver.c`, `scheduler.c`, `sim_verify.c`, `sim_loop.c` (the `sim_decision_eq` call only).
|
||||
- Spec `scheduler-sim-discrete-clock`: after it, a replayed session's `core->session_start` equals the recorded `beginRound` exactly.
|
||||
|
||||
### Revalidation Triggers
|
||||
- A change to the `xce_alloc_t`/`xce_host_alloc_t`/`xce_job_alloc_t`/`xce_job_preason_t` struct layouts must re-check the field lists in the equality functions and the exclusion documentation.
|
||||
- A change to the `xce_alloc_t`/`xce_host_alloc_t`/`xce_job_alloc_t`/`xce_job_preason_t` struct layouts must re-check the field lists, the exclusion documentation, and the description text.
|
||||
- A change to `same_alloc_slot()`'s matching rule, or a proposal to merge it with `alloc_eq`, must re-check Decision 6 (the two answer different questions and disagree on `instid`, `from`, and `value`).
|
||||
- A change to the pending-reason construction paths (cache-hit vs. full-computation) that alters whether detail lists are populated must re-check the simulator's main-reason-only PEND rationale (bug-1845) and the full-match `job_preason_eq` contract.
|
||||
- Any new path that builds an `xce_alloc_t` in the scheduler outside a session, a second clock read between session start and recording, or a change to `scheduler-sim-discrete-clock`'s clock guarantee, must re-check Decision 8.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Existing Architecture Analysis
|
||||
`src/scheduler/src/sim_verify.c` (474 lines) owns two layers:
|
||||
1. **Type-level comparators** — five `static` functions (`sim_verify_str_eq`, `sim_verify_alloc_eq`, `sim_verify_host_alloc_eq`, `sim_verify_job_alloc_eq`, `sim_verify_job_preason_eq`) that walk two libxce structs field-by-field. They have zero dependency on the scheduler, capnp, or the replay layer. Four of them are cleanly movable; `sim_verify_job_preason_eq` is movable but its current contract (main-reason-only) is wrong for a *public* equality function — it encodes a simulator replay asymmetry, not an object-identity property.
|
||||
2. **Decision-level dispatcher + reporting** — `sim_decision_eq` (dispatches on `xce_decision_t.op` vs `ReplayDecision_body_which`), `sim_log_mismatch`, and two `describe_*` helpers. These depend on `replay_decision_t`, capnp enums, and `ERROR()`.
|
||||
1. **Type-level comparators** — five `static` functions (`sim_verify_str_eq`, `sim_verify_alloc_eq`, `sim_verify_host_alloc_eq`, `sim_verify_job_alloc_eq`, `sim_verify_job_preason_eq`) that walk two libxce structs field-by-field and return only 0/1. They have zero dependency on the scheduler, capnp, or the replay layer. `sim_verify_alloc_eq` excludes `from` (bug-1838-followup) and `instid` (bug-1844). `sim_verify_job_preason_eq` compares only `main_reason` (bug-1845), a simulator replay asymmetry rather than an object-identity property.
|
||||
2. **Decision-level dispatcher + reporting** — `sim_decision_eq` (dispatches on `xce_decision_t.op` vs `ReplayDecision_body_which`), `sim_log_mismatch`, and two `describe_*` helpers. These depend on `replay_decision_t`, capnp enums, and `ERROR()`. `sim_log_mismatch` prints only jobid/nhosts for each side, so a mismatch below that level is invisible (bug-1881).
|
||||
|
||||
The four allocation/pending-reason structs are defined in `src/include/xce/types.h` and already constructed/deep-copied by `src/lib/xcealloc.c` (`make_alloc`, `make_host_alloc`, `make_job_alloc`, `dup_job_alloc`) and `src/lib/preason.c` (`add_job_preason`, `make_pendreason`). The controller consumes these same structs in `persist.c`, `resync.c`, and `job.c` with no equality API available.
|
||||
|
||||
Since this design was first drafted, bug-1863 (`aea34738`) added `merge_job_alloc()` to `xcealloc.c` with a private `same_alloc_slot()` helper that matches two `xce_alloc_t` records by `resname`, `instid`, and `reserve`, to decide whether they are the same reservation slot to merge. It is a near neighbour of `alloc_eq` but has a different contract (see Decision 6).
|
||||
|
||||
**How `from` is set today.** `make_alloc()` (`xcealloc.c:30`) stamps `from = time(NULL)`. The scheduler's only production constructor call is `scheduler.c:1893`, inside a scheduling session. `from` is on the wire (`controller.capnp` `from @4: UInt64 # alloc start time`) and `dup_alloc()` copies it.
|
||||
|
||||
**How session time is read today.** Two separate clock reads per session:
|
||||
- `xce_core_begin_session()` sets `core->session_start = xce_core_now(core)` (`core_session.c:429`).
|
||||
- The recording then reads the clock again for `beginRound`: `driver.c:1030` after `plugin_begin_session`/`sched_account_jobs`, and `sim_loop.c` before `xce_core_begin_session`.
|
||||
|
||||
In simulation, `sim_advance_virtual_clock(sim, begin_round)` re-anchors the virtual clock to the recorded `beginRound`. At `--sim-speed` ≥ 1 the clock keeps advancing from there, so `session_start` lands slightly after `begin_round`. At `--sim-speed 0` it is frozen and exact. The simulator never sleeps, so `--sim-speed` paces nothing; it only adds drift. Spec `scheduler-sim-discrete-clock` removes it, which makes `session_start == begin_round` in every replay.
|
||||
|
||||
Three scheduler test files already reuse two of the leaf comparators — but via white-box `#include "../src/sim_verify.c"` + direct calls to the `static` functions: `test_sim_equivalence.c` (lines 894, 902), `test_repl_equivalence.c` (lines 734, 742), and `test_repl_limit_table.c` (lines 2662, 2670).
|
||||
|
||||
### Architecture Pattern & Boundary Map
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph scheduler["src/scheduler"]
|
||||
subgraph scheduler
|
||||
SV["sim_verify.c\n(sim_decision_eq, sim_log_mismatch, describe_*,\nmain-reason-only PEND outcome check)"]
|
||||
DR["driver.c\nbeginRound = core.session_start"]
|
||||
SC["scheduler.c\nalloc->from = session_start / 1e9"]
|
||||
CS["core_session.c\nsession_start"]
|
||||
DC["spec scheduler-sim-discrete-clock\nreplayed session_start == recorded beginRound"]
|
||||
TE["test_sim_equivalence.c / test_repl_equivalence.c / test_repl_limit_table.c"]
|
||||
TSV["test_sim_verify.c"]
|
||||
end
|
||||
subgraph libxce["src/lib + src/include"]
|
||||
XA["xcealloc.{c,h}\nalloc_eq / host_alloc_eq / job_alloc_eq"]
|
||||
PR["preason.{c,h}\njob_preason_eq (full match)"]
|
||||
subgraph libxce
|
||||
XA["xcealloc.{c,h}\nalloc_eq / host_alloc_eq / job_alloc_eq (+why)"]
|
||||
PR["preason.{c,h}\njob_preason_eq (full match, +why)"]
|
||||
UT["utils.{c,h}\nstr_eq (NULL-safe)"]
|
||||
TY["xce/types.h\n(struct defs, unchanged)"]
|
||||
end
|
||||
SV -->|"DISPATCH: job_alloc_eq (public API)"| XA
|
||||
TE -.->|"calls (replaces white-box static calls)"| XA
|
||||
TE -.->|"calls"| PR
|
||||
TSV -->|"exercises via sim_decision_eq"| SV
|
||||
SV --> XA
|
||||
TE --> XA
|
||||
TE --> PR
|
||||
XA --> UT
|
||||
PR --> UT
|
||||
XA --> TY
|
||||
PR --> TY
|
||||
DR --> CS
|
||||
SC --> CS
|
||||
DC -.-> CS
|
||||
```
|
||||
|
||||
**Architecture Integration**:
|
||||
- Selected pattern: co-locate equality with the module that already owns each struct's construction and deep-copy — `alloc` family in `xcealloc.c` (mirrors `dup_job_alloc`), `job_preason_eq` in `preason.c` (mirrors `add_job_preason`). No new module is introduced.
|
||||
- Domain boundary: the library owns *struct-level* equality (full structural match); the simulator owns *decision-level* comparison and reporting, including its one deliberate loosening (main-reason-only PEND). This is the clean seam: everything below it is libxce-typed and identity-honest, everything above it touches `replay_decision_t`/capnp and may narrow for replay reasons.
|
||||
- Existing patterns preserved: public headers in `src/include/`, implementations in `src/lib/`, `const`-qualified read-only parameters, lowercase unprefixed function names (`make_*`/`dup_*`/`erase_*`).
|
||||
- Steering compliance: no changes to `types.h` layouts, no new persistence surface, no HA/RBAC/persistence semantics touched.
|
||||
- Domain boundary: the library owns *struct-level* equality and its explanation (full structural match); the simulator owns *decision-level* comparison and reporting, including its one deliberate loosening (main-reason-only PEND). The scheduler core owns the session-start value that makes `from` reproducible.
|
||||
- Steering compliance: no changes to `types.h` layouts or the wire schema; `from` keeps its unit and meaning ("alloc start time"), now defined as the start of the deciding session. Persistence: `from` already round-trips through the snapshot (`dup_job_alloc`) and the controller's capnp encoding, so no new save/restore field is introduced.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Layer | Choice / Version | Role in Feature | Notes |
|
||||
|-------|------------------|-----------------|-------|
|
||||
| Language | C (gnu17) | equality functions + tests | unchanged from libxce |
|
||||
| Build | Meson via `xce-build` | compile libxce + run `src/lib/testsuite` | unchanged |
|
||||
| Test | Unity | `test_xcealloc.c`, `test_preason.c` | existing libxce test harness |
|
||||
| Language | C (gnu17) | equality functions, `from` stamping, tests | unchanged |
|
||||
| Build | Meson via `xce-build` | compile libxce + scheduler, run `src/lib/testsuite` and `src/scheduler/testsuite` | unchanged |
|
||||
| Test | Unity | `test_xcealloc.c`, `test_preason.c`, scheduler tests | existing harnesses |
|
||||
|
||||
## File Structure Plan
|
||||
|
||||
### Modified Files
|
||||
- `src/lib/xcealloc.c` — add `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` (using the shared `str_eq` helper).
|
||||
- `src/include/xcealloc.h` — declare the three allocation equality functions.
|
||||
- `src/lib/preason.c` — add `job_preason_eq` (full match, using the shared `str_eq` helper).
|
||||
- `src/include/preason.h` — declare `job_preason_eq`.
|
||||
- `src/lib/utils.c` / `src/include/utils.h` — add a NULL-safe `str_eq(const char*, const char*)` helper (see Decision 5).
|
||||
- `src/scheduler/src/sim_verify.c` — delete the five `static` comparators; have `sim_decision_eq` call `job_alloc_eq` for DISPATCH and keep a simulator-local `static` main-reason-only helper for PEND. Keep `sim_log_mismatch` + describe helpers.
|
||||
- `src/lib/testsuite/test_xcealloc.c` — add allocation equality unit tests.
|
||||
- `src/lib/testsuite/test_preason.c` — add `job_preason_eq` full-match unit tests.
|
||||
- `src/scheduler/testsuite/test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c` — replace direct calls to `sim_verify_job_alloc_eq`/`sim_verify_job_preason_eq` with `job_alloc_eq`/`job_preason_eq`; add `#include "xcealloc.h"`/`#include "preason.h"`.
|
||||
- `src/scheduler/testsuite/test_sim_verify.c` — remove the DISPATCH alloc-level unit tests that now belong to libxce; keep the PEND/decision-level tests (they exercise `sim_decision_eq`'s simulator-local main-reason-only behavior) and the integration-level tests.
|
||||
- `src/lib/xcealloc.c` / `src/include/xcealloc.h` — add `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` with the optional `char** why` description.
|
||||
- `src/lib/preason.c` / `src/include/preason.h` — add `job_preason_eq` (full match) with the optional description.
|
||||
- `src/scheduler/src/driver.c` — record `beginRound` from `sched->core.session_start` instead of a second `xce_core_now()` read.
|
||||
- `src/scheduler/src/scheduler.c` — after `make_alloc()` at `:1893`, set `allocs[i]->from = sched->core.session_start / 1000000000ULL`.
|
||||
- `src/scheduler/src/sim_verify.c` — delete the five `static` comparators; `sim_decision_eq` gains an optional `char** why` and calls `job_alloc_eq` for DISPATCH; keep a simulator-local `static` main-reason-only helper for PEND; `sim_log_mismatch` asks `sim_decision_eq` for the description, appends it, and frees it. Update the header comments that describe `from` as excluded.
|
||||
- `src/scheduler/src/sim_loop.c` — pass the description through (its `extern` declaration of `sim_decision_eq` gets the new parameters).
|
||||
- `src/lib/testsuite/test_xcealloc.c`, `src/lib/testsuite/test_preason.c` — unit tests.
|
||||
- `src/scheduler/testsuite/test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c` — replace direct calls with `job_alloc_eq(..., NULL)`/`job_preason_eq(..., NULL)` (or take the description, print it on failure, and free it); add `#include "xcealloc.h"`/`#include "preason.h"`; comment at each `job_preason_eq` call why full match is valid (Decision 7); keep the white-box `#include "../src/sim_verify.c"`.
|
||||
- `src/scheduler/testsuite/test_sim_verify.c` — drop the DISPATCH alloc-level tests now owned by libxce; replace the "`from` is ignored" test with "`from` from the same session matches"; add a test that the mismatch log names the differing field; keep PEND/decision-level and integration tests.
|
||||
- A scheduler test (new or in an existing session/allocation test file) — allocations made in one session carry `session_start / 1e9` as `from`, and the recorded `beginRound` equals `session_start`.
|
||||
- `src/scheduler/testsuite/test_sim_live_allocation.c`, `test_sim_reconcile_unaccounted.c`, and the header comments of `test_sim_equivalence.c`/`test_repl_equivalence.c` — update comments that name `sim_verify_*_eq` or describe `from` as excluded.
|
||||
|
||||
## System Flows
|
||||
|
||||
The comparison flow is unchanged from today — this feature only relocates the leaf logic. `sim_decision_eq` remains the entry point:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["sim_decision_eq(expected, actual)"] --> B{"actual->op"}
|
||||
B -->|DISPATCH| C["job_alloc_eq(expected->body.dispatch->alloc, actual->u.alloc)"]
|
||||
B -->|PEND| D["jobid equal AND main_reason equal (sim-local, NOT job_preason_eq)"]
|
||||
A["sim_decision_eq(expected, actual, why)"] --> B{"actual->op"}
|
||||
B -->|DISPATCH| C["job_alloc_eq(expected alloc, actual alloc, why)"]
|
||||
B -->|PEND| D["jobid equal AND main_reason equal (sim-local, NOT job_preason_eq); why names which"]
|
||||
B -->|PREEMPT| E["log + report mismatch (unchanged)"]
|
||||
C --> F["host_alloc_eq per host"] --> G["alloc_eq per resource (excl. from/instid)"]
|
||||
C --> F["host_alloc_eq per host"] --> G["alloc_eq per resource (incl. from, excl. instid)"]
|
||||
D --> H["main-reason only, detail ignored (bug-1845)"]
|
||||
C --> M["sim_log_mismatch appends *why, then free()"]
|
||||
D --> M
|
||||
```
|
||||
|
||||
`from` stamping per session:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Drv as driver.c
|
||||
participant Core as core_session.c
|
||||
participant Sch as scheduler.c
|
||||
participant Rec as recording
|
||||
Drv->>Core: xce_core_begin_session()
|
||||
Core-->>Core: session_start = xce_core_now()<br/>(replay: == recorded beginRound, via scheduler-sim-discrete-clock)
|
||||
Drv->>Rec: beginRound = session_start
|
||||
Sch->>Sch: make_alloc(...); from = session_start / 1e9
|
||||
```
|
||||
|
||||
## Requirements Traceability
|
||||
|
|
@ -126,14 +172,19 @@ flowchart TD
|
|||
| Requirement | Summary | Components | Interfaces |
|
||||
|-------------|---------|------------|------------|
|
||||
| 1.1–1.3 | allocation equality for `xce_alloc_t`/`xce_host_alloc_t`/`xce_job_alloc_t` | `xcealloc.c` | `alloc_eq`, `host_alloc_eq`, `job_alloc_eq` |
|
||||
| 1.4 | exclude `from`/`instid` | `xcealloc.c` | `alloc_eq` field list |
|
||||
| 1.5–1.6 | NULL handling + read-only | all four functions | const params |
|
||||
| 1.4 | exclude `instid` only; document difference from `merge_job_alloc` | `xcealloc.c/.h` | `alloc_eq` field list + header comment |
|
||||
| 1.5–1.7 | NULL handling, malformed-record precondition, read-only | all four functions | const params |
|
||||
| 2.1–2.3 | pending-reason full structural equality | `preason.c` | `job_preason_eq` |
|
||||
| 3.1–3.2, 3.5 | simulator behavior preserved | `sim_verify.c` | `sim_decision_eq` |
|
||||
| 3.3 | simulator uses libxce allocation comparison | `sim_verify.c` | `job_alloc_eq` |
|
||||
| 3.4 | simulator-local main-reason-only PEND check | `sim_verify.c` | local static helper |
|
||||
| 4.1–4.2 | single shared implementation | `sim_verify.c` + 3 test files | public API |
|
||||
| 4.3–4.4 | libxce unit tests + no regressions | `test_xcealloc.c`, `test_preason.c` | — |
|
||||
| 3.1–3.6 | first-difference description, caller-owned | all four functions | `char** why` |
|
||||
| 4.1–4.2, 4.4–4.5 | reproducible `from` (normal mode) | `driver.c`, `scheduler.c` | `core->session_start` |
|
||||
| 4.3 | replayed session start == recorded `beginRound` | spec `scheduler-sim-discrete-clock` | — |
|
||||
| 5.1 | simulator uses libxce allocation comparison | `sim_verify.c` | `job_alloc_eq` |
|
||||
| 5.2 | simulator-local main-reason-only PEND check | `sim_verify.c` | local static helper |
|
||||
| 5.3 | mismatch log names the difference (bug-1881) | `sim_verify.c`, `sim_loop.c` | `sim_decision_eq(..., why)`, `sim_log_mismatch` |
|
||||
| 5.4 | zero-mismatch self-replay | integration tests | — |
|
||||
| 6.1–6.3 | single shared implementation; test files use full-match preason; include stays | `sim_verify.c` + 3 test files | public API |
|
||||
| 6.4–6.6 | libxce + scheduler tests, no other behavior change | test files | — |
|
||||
| 6.7 | stale comment references updated | scheduler sources/tests | — |
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
|
|
@ -143,33 +194,37 @@ flowchart TD
|
|||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Public structural equality for the allocation structs |
|
||||
| Requirements | 1.1–1.6 |
|
||||
| Intent | Public structural equality for the allocation structs, with an optional first-difference description |
|
||||
| Requirements | 1.1–1.7, 3.1–3.6 |
|
||||
|
||||
```c
|
||||
/* src/include/xcealloc.h */
|
||||
extern int alloc_eq(const xce_alloc_t* a, const xce_alloc_t* b);
|
||||
extern int host_alloc_eq(const xce_host_alloc_t* a, const xce_host_alloc_t* b);
|
||||
extern int job_alloc_eq(const xce_job_alloc_t* a, const xce_job_alloc_t* b);
|
||||
extern int alloc_eq(const xce_alloc_t* a, const xce_alloc_t* b, char** why);
|
||||
extern int host_alloc_eq(const xce_host_alloc_t* a, const xce_host_alloc_t* b,
|
||||
char** why);
|
||||
extern int job_alloc_eq(const xce_job_alloc_t* a, const xce_job_alloc_t* b,
|
||||
char** why);
|
||||
```
|
||||
|
||||
- **Preconditions**: none (NULL-safe).
|
||||
- **Postconditions**: returns non-zero iff the two arguments are equal per the documented contract; does not mutate either argument.
|
||||
- **Invariants**: `alloc_eq(a, b)` ignores `from` and `instid`; `host_alloc_eq`/`job_alloc_eq` are order-sensitive (both sides come from the same deterministic construction, so array order is expected to match, not just multiset).
|
||||
- **Preconditions**: a non-zero entry count implies a non-NULL entry array (a malformed record is not handled). NULL top-level arguments, NULL list entries, and NULL string fields are handled. `why` may be NULL; `*why` on entry is ignored (never read or freed).
|
||||
- **Postconditions**: returns non-zero iff the two arguments are equal per the documented contract; does not mutate either argument. If `why != NULL`: on equal, `*why = NULL`; on not-equal, `*why` is a `malloc`'d, NUL-terminated description of the first difference (see Data Models), sized by the function and never truncated; the caller owns it and releases it with `free()`. If that allocation fails, `*why = NULL` and the return value is unaffected.
|
||||
- **Invariants**: `alloc_eq(a, b)` answers "same allocation decision", compares `from`, and ignores `instid`; the header comment states that this differs from `merge_job_alloc()`'s slot matching, where `instid` is significant; `host_alloc_eq`/`job_alloc_eq` are order-sensitive (both sides come from the same deterministic construction, so array order is expected to match, not just multiset). The result never depends on `why` or on whether the description could be allocated.
|
||||
|
||||
#### Pending-reason equality (`preason.c`)
|
||||
|
||||
| Field | Detail |
|
||||
|-------|--------|
|
||||
| Intent | Public full structural equality for `xce_job_preason_t` |
|
||||
| Requirements | 2.1–2.3 |
|
||||
| Intent | Public full structural equality for `xce_job_preason_t`, with an optional first-difference description |
|
||||
| Requirements | 2.1–2.3, 3.1–3.6 |
|
||||
|
||||
```c
|
||||
/* src/include/preason.h */
|
||||
extern int job_preason_eq(const xce_job_preason_t* a, const xce_job_preason_t* b);
|
||||
extern int job_preason_eq(const xce_job_preason_t* a,
|
||||
const xce_job_preason_t* b,
|
||||
char** why);
|
||||
```
|
||||
|
||||
- **Postconditions**: returns non-zero iff `main_reason` matches, the host-based reason list matches in order (each entry by host name + reason), and the resource-based reason list matches in order (each entry by resource name + host name + reason); NULL-safe per the shared convention.
|
||||
- **Postconditions**: returns non-zero iff `main_reason` matches, the host-based reason list matches in order (each entry by host name + reason), and the resource-based reason list matches in order (each entry by resource name + host name + reason); NULL-safe per the shared convention; `why` as for the allocation functions.
|
||||
- **Invariants**: this is a full match — it is the honest "are these two pending reasons identical" predicate, not the simulator's replay loosening (see Decision 3).
|
||||
|
||||
#### Shared string helper (`utils.c`)
|
||||
|
|
@ -181,25 +236,45 @@ extern int str_eq(const char* a, const char* b);
|
|||
|
||||
- **Postconditions**: returns non-zero iff `a` and `b` are both NULL, or both non-NULL and `strcmp(a, b) == 0`; zero otherwise.
|
||||
|
||||
### Scheduler
|
||||
|
||||
#### Recording (`driver.c`)
|
||||
- Normal mode: `sched_record_session_begin(sched, sched->core.session_start)` replaces the separate `xce_core_now()` read, so the recorded `beginRound` is the session start.
|
||||
- Simulation: no change here. Spec `scheduler-sim-discrete-clock` guarantees `core->session_start == begin_round` for a replayed session (clock frozen between recorded timestamps).
|
||||
|
||||
#### `from` stamping (`scheduler.c`)
|
||||
- After `make_alloc()` at `:1893`: `allocs[i]->from = sched->core.session_start / 1000000000ULL` (seconds, floor).
|
||||
|
||||
#### Simulator verification (`sim_verify.c`)
|
||||
- `int sim_decision_eq(const replay_decision_t* expected, const xce_decision_t* actual, char** why)`: DISPATCH calls `job_alloc_eq(..., why)`; PEND sets `*why` to `jobid A != B` or `main_reason A != B`; kind mismatch sets `kind DISPATCH != PEND`. Same ownership contract as the libxce functions.
|
||||
- `sim_log_mismatch` calls `sim_decision_eq(expected, actual, &why)`, appends ` -- diff: <why>` to its existing line (or ` -- diff: <unavailable>` if `why` is NULL), then `free(why)`.
|
||||
|
||||
## Data Models
|
||||
|
||||
No struct is added or modified. The equality contract itself is the only new "model", captured in the field lists below.
|
||||
No struct is added or modified. The equality contract and the description format are the only new "models".
|
||||
|
||||
- `alloc_eq` compares: `value` (exact `==`, documented bit-exact-safe because both sides are exact copies of the same `double`), `duration`, `resname`, `reserve`. **Excluded**: `from` (wall-clock `time(NULL)` stamp), `instid` (per-process `nextid++` counter) — both runtime bookkeeping, not identity.
|
||||
- `host_alloc_eq` compares: `host`, then `allocs[i]` in order for `i < nallocs`.
|
||||
- `job_alloc_eq` compares: `jobid`, then `allocs[i]` in order for `i < nhosts`, then `allocs_shared[i]` in order for `i < nres_shared`.
|
||||
- `job_preason_eq` compares: `main_reason`; then `host_based_reasons[i]` in order for `i < nhost_reasons`, each entry by `host` and `reason`; then `res_based_reasons[i]` in order for `i < nres_reasons`, each entry by `resource`, `host`, and `reason`. **Nothing is excluded** — this is a full structural match.
|
||||
- `alloc_eq` compares, in order: `resname`, `reserve`, `value` (exact `==`, documented bit-exact-safe because both sides are exact copies of the same `double`), `duration`, `from`. **Excluded**: `instid` (per-process `nextid++` counter).
|
||||
- `host_alloc_eq` compares: `host`, then `nallocs`, then `allocs[i]` in order.
|
||||
- `job_alloc_eq` compares: `jobid`, `nhosts`, `nres_shared`, then `allocs[i]` in order, then `allocs_shared[i]` in order.
|
||||
- `job_preason_eq` compares: `main_reason`; then `nhost_reasons` and `host_based_reasons[i]` in order, each entry by `host` and `reason`; then `nres_reasons` and `res_based_reasons[i]` in order, each entry by `resource`, `host`, and `reason`. **Nothing is excluded.**
|
||||
- **Description format** (first difference only, path from the outermost struct compared):
|
||||
- scalar: `allocs[1](host=h1).allocs[0](resname=mem).value: 2 != 3`
|
||||
- string: `allocs[0](host=h0).host: "h0" != "h2"`
|
||||
- NULL: `allocs_shared[0]: NULL != non-NULL`
|
||||
- length: `nhosts: 1 != 2`
|
||||
- `jobid` formatted with libxce's own `fmt_jobid()` (`utils.h`), so it reads like the rest of the scheduler log; `from` as integer seconds; `value` with `%.17g` so an exact-compare difference is always visible.
|
||||
|
||||
## Error Handling
|
||||
|
||||
These are pure read-only functions with no allocation, no I/O, and no failure path. The only "error" case is NULL input, which is handled by the shared convention (identical pointers including both-NULL → equal; exactly one NULL → not-equal) rather than an error return. No logging is added.
|
||||
These are read-only functions with no I/O and no failure path for the comparison itself. The only allocation is the optional description; if it fails, `*why` is NULL and the result is unchanged. The only "error" case is NULL input, which is handled by the shared convention (identical pointers including both-NULL → equal; exactly one NULL → not-equal) rather than an error return. A malformed record (non-zero count, NULL array) is a documented precondition violation, not a handled case. No logging is added; the description is built with `vasprintf` (glibc, already used under `_GNU_SOURCE`), and returned to the caller.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit (libxce)**: `test_xcealloc.c` — match/mismatch on each compared field, the `from`/`instid` exclusion, ordered-list sensitivity, and NULL handling at top and nested levels; `test_preason.c` — full-match: main-reason match/mismatch, host-based reason list match/mismatch (per field + order), resource-based reason list match/mismatch (per field + order), and NULL handling.
|
||||
- **Unit (scheduler)**: `test_sim_verify.c` keeps `sim_decision_eq`-level tests (DISPATCH/PEND match and mismatch, kind mismatch, PREEMPT always-false, NULL defense) and drops the DISPATCH alloc-level tests that now belong to libxce. The PEND tests stay because they pin the simulator-local main-reason-only behavior.
|
||||
- **Integration (scheduler)**: `test_sim_verify.c`'s `--sim-verify` round-trips (zero-mismatch self-replay, exact-mismatch altered replay, reconcile cases) continue to assert unchanged behavior, proving the relocation is behavior-preserving.
|
||||
- **Regression**: `test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c` must stay green after migrating to the public API.
|
||||
- **Unit (libxce)**: `test_xcealloc.c` — match/mismatch on each compared field with its expected description, `instid` ignored, `from` compared, ordered-list sensitivity, list-length mismatch, NULL handling at top level, nested entries, and string fields, NULL `why`, `*why == NULL` on equal, a failed description allocation (`--wrap` on `vasprintf` or `malloc` in a dedicated test binary: `*why` NULL, same result); every test frees the returned description, so ASAN/LeakSanitizer proves ownership; an existing `merge_job_alloc` test still passes. `test_preason.c` — full-match: main-reason, host-based and resource-based lists per field + order + length, NULL handling, description cases; plus `str_eq` NULL/NULL and NULL/non-NULL.
|
||||
- **Unit (scheduler)**: `test_sim_verify.c` keeps `sim_decision_eq`-level tests (DISPATCH/PEND match and mismatch, kind mismatch, PREEMPT always-false, NULL defense), asserts the mismatch description, and drops the alloc-level tests now owned by libxce. A session-level test asserts every allocation of one session (including a COMPOUND merge) carries `session_start / 1e9`, and that the recorded `beginRound` equals `session_start`.
|
||||
- **Integration (scheduler)**: `test_sim_verify.c`'s `--sim-verify` round-trips (zero-mismatch self-replay, exact-mismatch altered replay, reconcile cases); the self-replay reports zero mismatches. Until `scheduler-sim-discrete-clock` lands, run them at `--sim-speed 0`.
|
||||
- **Regression**: `test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c` stay green under the stricter checks (full-match preason, `from` compared). A failure there is investigated as a real divergence.
|
||||
- **Live**: record a session on a local cluster, replay it with `--sim-verify` at the default speed: zero mismatches.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
|
|
@ -220,19 +295,46 @@ These are pure read-only functions with no allocation, no I/O, and no failure pa
|
|||
### Decision 3: `job_preason_eq` is a full structural match; the simulator keeps its own main-reason-only PEND check
|
||||
- **Context**: the current `sim_verify_job_preason_eq` compares only `main_reason`, deliberately (bug-1845): the simulator has no alloc cache, so its full-detail PEND can never match a recorded cache-hit's main-reason-only shape, and requiring detail to match produced systematic false mismatches. That narrowing is a property of *recorded-vs-live replay comparison*, not of "are these two pending-reason objects identical".
|
||||
- **Alternatives**: (a) publish a full-deep `job_preason_eq` and keep the main-reason-only comparison as a simulator-local static helper (selected); (b) publish main-reason-only `job_preason_eq` (rejected — leaks a replay artifact into a general-purpose equality API and contradicts the identity semantics a controller engineer would expect).
|
||||
- **Selected**: `job_preason_eq(a, b)` returns non-zero iff `main_reason` matches and both detail lists match in order, field-for-field. The simulator's `sim_decision_eq` PEND branch keeps a small `static` main-reason-only helper (documented with the bug-1845 rationale) instead of calling `job_preason_eq`.
|
||||
- **Selected**: `job_preason_eq(a, b, why)` returns non-zero iff `main_reason` matches and both detail lists match in order, field-for-field. The simulator's `sim_decision_eq` PEND branch keeps a small `static` main-reason-only helper (documented with the bug-1845 rationale) instead of calling `job_preason_eq`.
|
||||
- **Rationale**: a public "same pending reason" predicate must be identity-honest — a controller comparing two pending reasons will want the detail to matter. The replay asymmetry is a scheduler-only concern and stays where it belongs.
|
||||
- **Trade-offs**: two PEND-comparison shapes now exist (full-match in libxce, main-reason-only in sim_verify.c). This is intentional: they answer different questions ("identical objects" vs. "same replay outcome"). The sim-local helper is `static` and small, so the duplication is negligible and clearly scoped.
|
||||
- **Follow-up**: none expected; the sim-local helper is not exported.
|
||||
|
||||
### Decision 4: Preserve NULL and order semantics exactly
|
||||
### Decision 4: Preserve NULL and order semantics
|
||||
- **Context**: NULL handling and list-order sensitivity.
|
||||
- **Selected**: keep today's behavior — identical pointer (including both NULL) → equal; exactly one NULL → not-equal; ordered (not multiset) list comparison.
|
||||
- **Rationale**: the point of the relocation is zero behavioral change; both sides of every current comparison come from the same deterministic construction, so order-sensitivity is correct. The full-match `job_preason_eq` uses the same convention.
|
||||
- **Selected**: keep today's behavior — identical pointer (including both NULL) → equal; exactly one NULL → not-equal; ordered (not multiset) list comparison. A malformed record (non-zero count, NULL array) is not handled — it is a documented precondition, matching today's comparators.
|
||||
- **Rationale**: both sides of every current comparison come from the same deterministic construction, so order-sensitivity is correct. The new full-match `job_preason_eq` uses the same convention.
|
||||
|
||||
### Decision 5: Promote the NULL-safe string comparison to a shared `str_eq` in `utils.{c,h}`
|
||||
- **Context**: with `job_preason_eq` now a full match, both `xcealloc.c` (host/resname/reserve strings) and `preason.c` (host/resource strings) need NULL-safe string comparison. Keeping it `static` in one file forces the other to duplicate it.
|
||||
- **Alternatives**: (a) shared `str_eq` in `utils.{c,h}` (selected); (b) duplicate a `static` helper in each of the two files.
|
||||
- **Selected**: add `int str_eq(const char* a, const char* b)` to `utils.{c,h}`, beside the existing `float_equal` helper.
|
||||
- **Rationale**: one consumer each would have justified a `static`; two consumers and a third on the horizon (controller) justify a shared helper. `utils.h` already centralizes small generic C helpers (`fmt_jobid`, `float_equal`).
|
||||
- **Trade-offs**: a new public symbol in `utils.h`; negligible surface, and it will be reused by the controller when it adopts these comparators.
|
||||
- **Rationale**: two consumers and a third on the horizon (controller) justify a shared helper. `utils.h` already centralizes small generic C helpers (`fmt_jobid`, `float_equal`). It could also make `same_alloc_slot()` NULL-safe when bug-1882 is fixed.
|
||||
- **Trade-offs**: a new public symbol in `utils.h`; negligible surface.
|
||||
|
||||
### Decision 6: `alloc_eq` and `same_alloc_slot` stay separate
|
||||
- **Context**: bug-1863 added a private `same_alloc_slot(a, b)` to `xcealloc.c` (`resname` + `instid` + `reserve`), used by `merge_job_alloc()` to decide whether a source allocation should be added into an existing destination entry. `alloc_eq` will live in the same file and also compares `xce_alloc_t` records.
|
||||
- **Alternatives**: (a) keep them separate and document the difference (selected); (b) implement `same_alloc_slot` on top of `alloc_eq` or vice versa.
|
||||
- **Selected**: keep them separate. `alloc_eq` answers "do these two records describe the same allocation decision" (resname, reserve, value, duration, from; `instid` excluded). `same_alloc_slot` answers "are these the same reservation slot to accumulate into" (resname, instid, reserve; `value` excluded because merging adds values). The `alloc_eq` header comment names the difference.
|
||||
- **Rationale**: the two predicates disagree on `instid`, `value`, `duration`, and `from`, so neither can be expressed through the other without changing behavior. `merge_job_alloc()` is out of scope.
|
||||
- **Follow-up**: `same_alloc_slot` calls `strcmp(a->resname, …)` without a NULL check, and `resname` is NULL when `make_alloc()`'s unchecked `strdup()` fails; tracked as bug-1882, not part of this spec.
|
||||
|
||||
### Decision 7: The three test files adopt the full-match `job_preason_eq`
|
||||
- **Context**: `test_sim_equivalence.c`, `test_repl_equivalence.c`, and `test_repl_limit_table.c` compare two decoded `replay_decision_t` entries and today call the main-reason-only `sim_verify_job_preason_eq`.
|
||||
- **Alternatives**: (a) switch them to the full-match `job_preason_eq` (selected); (b) give them their own main-reason-only helper.
|
||||
- **Selected**: (a). None of these tests registers `unsatisfiable_lookup_cb` (the alloc cache), so both sides of every pending-reason comparison are produced by the same full per-host computation and their detail lists are expected to match. The bug-1845 asymmetry (recorded cache-hit vs. live full detail) does not apply.
|
||||
- **Rationale**: the stricter check catches detail divergence the old one missed, at no false-positive cost for these fixtures. Each call site gets a short comment stating why full match is valid there.
|
||||
- **Trade-offs**: if a future test records with the alloc cache active, it must use a main-reason-only comparison instead; the call-site comment and the Revalidation Trigger on pending-reason construction paths cover this.
|
||||
|
||||
### Decision 8: `from` is the deciding session's start, from one clock reading
|
||||
- **Context**: `alloc_eq` must compare `from`, but `make_alloc()` stamps `time(NULL)`, which a replay cannot reproduce (bug-1838-followup). The scheduler already has a virtualized session start (`core->session_start`), but it is a different clock read from the recorded `beginRound`, and in simulation at `--sim-speed` ≥ 1 it drifts past the recorded `beginRound`.
|
||||
- **Alternatives**: (a) `from = session_start / 1e9`, with the recorded `beginRound` taken from `session_start`; the simulator clock stops drifting (spec `scheduler-sim-discrete-clock`) so a replayed `session_start` is exactly the recorded `beginRound` (selected); (a′) as (a), but with an explicit `xce_core_begin_session_at(core, begin_round)` in the simulator instead of removing the drift (superseded: the user chose to remove `--sim-speed`, which fixes the drift for every clock reader, not just `from`); (b) keep excluding `from` from equality (rejected by the user: `from` is part of what was decided); (c) `from = xce_core_now()` at allocation time (rejected: at `--sim-speed` ≥ 1 it drifts with replay pace, and a second-boundary crossing produces a false mismatch); (d) change `make_alloc()`'s signature to take `from` (rejected: churns the controller and ~20 test callers for no benefit; the scheduler is the only producer that needs a different value).
|
||||
- **Selected**: (a). The recording's `beginRound` is taken from `core->session_start`; `scheduler.c` overwrites `from` after `make_alloc()`; the simulator side comes from `scheduler-sim-discrete-clock`.
|
||||
- **Rationale**: every allocation of one session gets the same `from`, which a replay reproduces bit-for-bit. `from` changes meaning only slightly — from "second the record was built" to "second the deciding session started", normally the same second and at most one session's duration earlier.
|
||||
- **Trade-offs**: recorded `beginRound` moves a few milliseconds earlier (to `xce_core_begin_session`, before `plugin_begin_session`/`sched_account_jobs`). Recordings made before this change carry wall-clock `from` values and may show a false DISPATCH mismatch when an allocation's `time(NULL)` fell in a later second than its session start; the difference description names `from`, so these are recognisable. `make_alloc()` keeps `time(NULL)` for its other callers.
|
||||
|
||||
### Decision 9: First-difference description returned through `char** why`, owned by the caller
|
||||
- **Context**: a yes/no comparator leaves a mismatch undiagnosable (bug-1881).
|
||||
- **Alternatives**: (a) optional `char** why`: the function allocates a string sized to fit and hands ownership to the caller (selected, per the user); (b) caller buffer `char* why, size_t whylen` (superseded: the caller must guess a size, and a long path through many hosts gets truncated); (c) a separate `*_diff()` function family; (d) a list of every difference; (e) a callback per difference.
|
||||
- **Selected**: (a). One code path computes both the result and the explanation, so they cannot disagree; callers that do not care pass `NULL`. On not-equal the function sets `*why` to a `vasprintf`'d string; on equal, or if the allocation fails, `*why = NULL`. The caller frees with `free()`.
|
||||
- **Rationale**: the function knows the length; the caller only decides whether to ask. Allocation happens only on the not-equal path with `why` requested, i.e. only when something is about to be reported, so the equal fast path stays allocation-free. The first difference is the one that matters for a deterministic replay; later ones usually follow from it.
|
||||
- **Trade-offs**: every caller that asks must free (ASAN/LeakSanitizer in the test suites catches a missed `free`); only the first difference is reported.
|
||||
|
|
|
|||
|
|
@ -2,50 +2,82 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
Engineers maintaining the XCE scheduler and controller — anyone who needs to determine whether two scheduler allocation or pending-reason objects represent the same thing — currently have no shared way to do so. The structural equality comparators for the shared data structures `xce_alloc_t`, `xce_host_alloc_t`, `xce_job_alloc_t`, and `xce_job_preason_t` (all defined in libxce's `src/include/xce/types.h`) live as `static` functions inside the scheduler's simulation-verification module `src/scheduler/src/sim_verify.c`, written for `--sim-verify` replay checking. Three other scheduler test files already reuse two of these comparators (`sim_verify_job_alloc_eq`/`sim_verify_job_preason_eq`) — but only by white-box `#include "../src/sim_verify.c"` and calling the `static` functions directly, a fragile mechanism that drags the entire simulator translation unit into each consumer. Meanwhile the controller already consumes these same shared structs (`persist.c`, `resync.c`, `job.c`) with no equality API available at all. This should change so that the type-level equality comparators for these shared structures become public libxce API — `alloc_eq`/`host_alloc_eq`/`job_alloc_eq` in `xcealloc.{c,h}` (the module that already constructs and deep-copies them) and `job_preason_eq` in `preason.{c,h}` (the module that already owns pending-reason construction) — so any component or test can compare these shared objects by linking libxce, while the decision-level dispatcher (`sim_decision_eq`) and mismatch reporting (`sim_log_mismatch`) that are genuinely simulator-specific stay in `sim_verify.c`.
|
||||
Engineers maintaining the XCE scheduler who need to determine whether two scheduler allocation or pending-reason objects represent the same decision currently have no shared way to do so, and when two objects differ nothing tells them *where*. The structural equality comparators for the shared data structures `xce_alloc_t`, `xce_host_alloc_t`, and `xce_job_alloc_t` (all defined in libxce's `src/include/xce/types.h`) live as `static` functions inside the scheduler's simulation-verification module `src/scheduler/src/sim_verify.c`, written for `--sim-verify` replay checking, beside a pending-reason comparator for `xce_job_preason_t` that deliberately compares only the main reason code (bug-1845). They return only yes/no, so a `--sim-verify` mismatch log cannot say which field differs (bug-1881: 43 mismatch lines in one replay, none showing the difference). Three scheduler test files (`test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c`) reuse two of these comparators only because they white-box `#include "../src/sim_verify.c"` and can therefore call the `static` functions directly.
|
||||
|
||||
This should change so that:
|
||||
- the allocation equality comparators become public libxce API — `alloc_eq`/`host_alloc_eq`/`job_alloc_eq` in `xcealloc.{c,h}` (the module that already constructs and deep-copies them);
|
||||
- libxce gains a new, full-structural-match pending-reason comparator `job_preason_eq` in `preason.{c,h}` (new behavior, not a relocation: bug-1845 narrowed the simulator's comparator to main-reason-only);
|
||||
- every comparator can, on request, return a newly allocated description of the first difference it found, and `--sim-verify` puts that description in its mismatch log;
|
||||
- an allocation's reservation start time `from` becomes part of allocation equality. Today `make_alloc()` stamps it with the wall clock (`time(NULL)`), so a replay can never reproduce it and the simulator had to ignore it (bug-1838-followup). Instead the scheduler stamps `from` with the start time of the scheduling session that made the allocation, a value a replay reproduces exactly.
|
||||
|
||||
The decision-level dispatcher (`sim_decision_eq`), its main-reason-only pending-decision check, and mismatch reporting (`sim_log_mismatch`) are simulator-specific and stay in `sim_verify.c`. The controller also carries these structs (`persist.c`, `resync.c`, `job.c`) and is a likely future consumer, but adopting the API there is not part of this feature.
|
||||
|
||||
## Boundary Context
|
||||
|
||||
- **In scope**: extracting the four type-level comparators (`xce_alloc_t`, `xce_host_alloc_t`, `xce_job_alloc_t`, `xce_job_preason_t`) from the scheduler simulator into public libxce API; giving the allocation comparators a documented "identity" contract (excluding `from`/`instid`) and the pending-reason comparator a full structural-match contract (all fields, detail lists included); migrating every in-repo consumer of these comparators (the simulator plus the scheduler test files that currently reuse the static functions) to the public API; libxce unit tests for the new functions.
|
||||
- **Out of scope**: the decision-level comparison dispatcher (`sim_decision_eq`) and mismatch reporting (`sim_log_mismatch` and its describe helpers), which depend on `replay_decision_t`, capnp enums, and `ERROR()` logging and remain in the scheduler; any change to the *semantics* of what is compared (the deliberate field exclusions are preserved, not re-litigated); any new comparison call site beyond the existing consumers; any change to the controller's persistence or resync behavior; any change to the `.replay` wire format.
|
||||
- **Adjacent expectations**: the `.replay` wire format and the `replay_decision_t`/`xce_decision_t` decision types are unchanged; libxce's existing constructors and deep-copy helpers (`make_alloc`, `make_host_alloc`, `make_job_alloc`, `dup_job_alloc`, `make_pendreason`) and the `xce/types.h` struct layouts are unchanged; the controller's Cap'n Proto persistence round-trips for these structures are unchanged.
|
||||
- **In scope**: moving the three allocation comparators from the scheduler simulator into public libxce API with a documented "same allocation decision" contract that compares `from` and excludes only `instid`; adding a new full-structural-match pending-reason comparator to libxce; an optional out-parameter on every comparator that returns a newly allocated description of the first difference, owned by the caller; stamping `from` with the scheduling session's start time, taken from the one timestamp that is also recorded as the session's `beginRound`; using the difference description in `--sim-verify` mismatch logs (resolves bug-1881); migrating the simulator and the three scheduler test files that call the static comparators to the public API; libxce unit tests for the new functions; updating comments that name the removed `sim_verify_*_eq` functions.
|
||||
- **Out of scope**: moving the decision-level comparison dispatcher (`sim_decision_eq`) or mismatch reporting (`sim_log_mismatch` and its describe helpers) out of the scheduler; changing the simulator's main-reason-only pending-decision check (bug-1845); `merge_job_alloc()` and its `same_alloc_slot()` helper in `xcealloc.c` (bug-1863), which answer a different question — whether two records are the same reservation slot to merge, where `instid` *is* significant — and are neither replaced by nor reimplemented on top of `alloc_eq` (their NULL-dereference defect is bug-1882); removing the three test files' white-box `#include "../src/sim_verify.c"`, which they still need because they also white-box-include `sim_loop.c`, which calls `sim_decision_eq`/`sim_log_mismatch`; any new comparison call site beyond the existing consumers, including the controller; any change to how the controller or worker interpret `from`; any change to the `.replay` wire format; removing `--sim-speed` and making the simulator clock move only at recorded event and round timestamps, which is the separate spec `scheduler-sim-discrete-clock`.
|
||||
- **Adjacent expectations**: this feature depends on spec `scheduler-sim-discrete-clock` for Requirement 4.3 (a replayed session starts at exactly the recorded `beginRound`); the `.replay` wire format and the `replay_decision_t`/`xce_decision_t` decision types are unchanged; the `xce/types.h` struct layouts are unchanged, and `from` keeps its unit (seconds since the Unix epoch); libxce's deep-copy and merge helpers (`dup_job_alloc`, `merge_job_alloc`) keep copying `from` unchanged; the controller's Cap'n Proto persistence round-trips for these structures are unchanged. Recordings made before this change carry wall-clock `from` values; replaying them with `--sim-verify` may report a DISPATCH mismatch when an allocation's original `time(NULL)` fell in a later second than its session's start. That is an accepted limitation for old recordings, and the new difference description names `from` as the cause.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Shared equality API for allocation structures
|
||||
**Objective:** As a scheduler or controller engineer who needs to compare two allocation objects, I want a public libxce equality function for each allocation structure, so that I can compare them correctly by linking libxce rather than reimplementing the comparison or white-box-including scheduler source.
|
||||
**Objective:** As a scheduler engineer who needs to compare two allocation objects, I want a public libxce equality function for each allocation structure, so that I can compare them correctly by linking libxce rather than reimplementing the comparison or calling another module's private helpers.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The library shall provide a public equality function for `xce_alloc_t` that reports equal if and only if the two records have the same resource name, reserve, value, and duration.
|
||||
1. The library shall provide a public equality function for `xce_alloc_t` that reports equal if and only if the two records have the same resource name, reserve, value, reservation start time (`from`), and duration, where `value` is compared exactly (bit-for-bit `==`), not within a floating-point tolerance.
|
||||
2. The library shall provide a public equality function for `xce_host_alloc_t` that reports equal if and only if the two records have the same host name and, in order, the same per-resource `xce_alloc_t` records.
|
||||
3. The library shall provide a public equality function for `xce_job_alloc_t` that reports equal if and only if the two records have the same job id, the same per-host `xce_host_alloc_t` records in order, and the same shared-resource `xce_alloc_t` records in order.
|
||||
4. The allocation equality functions shall not consider a record's reservation start time (`from`) or allocation-instance id (`instid`) when determining equality, because these are runtime bookkeeping values, not part of an allocation's identity.
|
||||
5. When the two arguments to an allocation equality function are the identical pointer (including two NULL pointers), the function shall report equal; when exactly one of the two arguments is NULL at the top level or at any nested level, the function shall report not-equal without crashing.
|
||||
6. The allocation equality functions shall not modify either argument.
|
||||
4. The allocation equality functions shall answer "do these two records describe the same allocation decision" and shall not consider a record's allocation-instance id (`instid`), because it is taken from a per-process counter (`sched_get_id()`) whose value depends on how many allocations that process built before, so a recording and its replay give the same allocation different `instid` values (bug-1844). This contract is distinct from `merge_job_alloc()`'s slot matching, where `instid` is significant; the library's documentation of `alloc_eq` shall state that difference.
|
||||
5. When the two arguments to an allocation equality function are the identical pointer (including two NULL pointers), the function shall report equal; when exactly one of the two arguments is NULL at the top level, or exactly one of two corresponding list entries or string fields is NULL at any nested level, the function shall report not-equal without crashing.
|
||||
6. A record whose entry count is non-zero while its entry array pointer is NULL is malformed; the allocation equality functions are not required to handle it, and the library's documentation of these functions shall state that precondition.
|
||||
7. The allocation equality functions shall not modify either argument.
|
||||
|
||||
### Requirement 2: Shared equality API for the pending-reason structure
|
||||
**Objective:** As a scheduler or controller engineer who needs to compare two pending-reason objects, I want a public libxce equality function that reports a full structural match, so that I can determine whether two pending reasons are identical by linking libxce rather than reimplementing the comparison.
|
||||
**Objective:** As a scheduler engineer who needs to compare two pending-reason objects, I want a public libxce equality function that reports a full structural match, so that I can determine whether two pending reasons are identical by linking libxce rather than reimplementing the comparison.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The library shall provide a public equality function for `xce_job_preason_t` that reports equal if and only if the two records have the same main reason code, the same host-based reason entries in order, and the same resource-based reason entries in order.
|
||||
1. The library shall provide a public equality function for `xce_job_preason_t` that reports equal if and only if the two records have the same main reason code, the same host-based reason entries in order, and the same resource-based reason entries in order. This is new comparison behavior; it is intentionally stricter than the simulator's existing main-reason-only check.
|
||||
2. The pending-reason equality function shall compare each host-based reason entry by its host name and reason code, and each resource-based reason entry by its resource name, host name, and reason code.
|
||||
3. The pending-reason equality function shall handle NULL arguments the same way as the allocation equality functions (identical pointers including both NULL report equal; exactly one NULL at the top level or at any nested level reports not-equal) and shall not modify either argument.
|
||||
3. The pending-reason equality function shall handle NULL arguments and malformed records the same way as the allocation equality functions (Requirement 1, criteria 5 and 6) and shall not modify either argument.
|
||||
|
||||
### Requirement 3: Simulator verification behavior is preserved
|
||||
**Objective:** As an operator relying on `--sim-verify` replay checking, I want the simulator's decision verification to behave exactly as it did before this change, so that no new false mismatch or missed mismatch is introduced.
|
||||
### Requirement 3: Description of the first difference
|
||||
**Objective:** As an engineer debugging a mismatch, I want an equality function to tell me which field differs and the two values, so that I can diagnose the mismatch without re-running it under a debugger.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The simulator shall continue to compare a recorded decision against a live decision using the same equality semantics as before this change.
|
||||
2. The simulator shall continue to report a verification mismatch with the same logical-time and decision-detail context as before this change.
|
||||
3. The simulator shall obtain its allocation comparison from the libxce public API rather than defining it itself.
|
||||
4. The simulator shall keep its pending-decision comparison as a simulator-local main-reason-only check, because a recorded cache-hit pending reason carries no host/resource detail while a live replayed one always does — this documented asymmetry is unchanged by this feature and remains a simulator concern, not a property of the full-match libxce `job_preason_eq`.
|
||||
5. Replaying a recording against its own previously-recorded output with verification enabled shall continue to report zero mismatches.
|
||||
1. Each public equality function (Requirements 1 and 2) shall take an optional out-parameter `char** why`. When `why` is NULL, the function shall behave exactly as without it: no description is produced and nothing is allocated.
|
||||
2. When `why` is non-NULL and the two arguments are not equal, the function shall set `*why` to a newly allocated, NUL-terminated, human-readable description of the first difference found, in comparison order, naming: the path to the differing field (for example the host-list index and host name, the resource-list index and resource name), the field name, and the two differing values (or which side is NULL, or the two list lengths). The function sizes the string itself; the description is never truncated.
|
||||
3. When `why` is non-NULL and the two arguments are equal, the function shall set `*why` to NULL.
|
||||
4. Ownership of a returned description passes to the caller, who releases it with `free()`. The function shall never read or free the value `*why` held on entry.
|
||||
5. If allocating the description fails, the function shall set `*why` to NULL and still return the correct equal/not-equal result; the comparison result never depends on whether a description was produced.
|
||||
6. Producing the description shall not log.
|
||||
|
||||
### Requirement 4: Single shared implementation across all consumers
|
||||
**Objective:** As a maintainer, I want the allocation and pending-reason comparisons to exist exactly once, in libxce, so that future changes to their semantics are made in one place and every consumer picks them up.
|
||||
### Requirement 4: Reproducible reservation start time
|
||||
**Objective:** As an operator relying on `--sim-verify`, I want an allocation's `from` to be a value a replay reproduces exactly, so that `from` can be part of allocation equality without causing false mismatches.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. Every in-repo consumer of these comparisons — the simulator and the scheduler test files that currently reuse the static comparators — shall obtain the allocation comparison and the full pending-reason comparison from the libxce public API; the simulator's main-reason-only pending-decision check is the single documented simulator-local exception.
|
||||
2. A consumer that needs only the type-level comparisons shall be able to obtain them without white-box-including the simulator's verification source.
|
||||
3. The library shall provide unit test coverage for each new equality function, covering a match, a mismatch on each compared field, and the NULL-handling cases.
|
||||
4. Adding these functions shall not alter the behavior of any existing libxce function.
|
||||
1. The scheduler shall set each allocation's `from` to the start time of the scheduling session that produced the allocation, in whole seconds since the Unix epoch (the session start in nanoseconds, divided by 10^9 and rounded down), instead of the wall-clock time at which the record was built.
|
||||
2. In normal mode, the session start time used for `from` shall be the same single clock reading that the session records as its `beginRound` when recording is enabled; the scheduler shall not read the clock separately for the two.
|
||||
3. In simulation mode, the session start time used for `from` shall be exactly the recorded session's `beginRound`, so that a replayed allocation's `from` equals the recorded one. This is provided by spec `scheduler-sim-discrete-clock` (the simulator clock does not advance between recorded timestamps); this feature shall not add its own simulator clock mechanism.
|
||||
4. All allocations produced in one session, including every primary of a COMPOUND request merged by `merge_job_alloc()`, shall carry the same `from`.
|
||||
5. Deep copies (`dup_job_alloc()`) and merges (`merge_job_alloc()`) shall keep copying `from` from their source, as they do today.
|
||||
|
||||
### Requirement 5: Simulator verification behavior
|
||||
**Objective:** As an operator relying on `--sim-verify` replay checking, I want the simulator's decision verification to keep catching real divergence without new false mismatches, and to tell me what differs, so that a mismatch is both trustworthy and diagnosable.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The simulator shall compare a recorded DISPATCH decision against a live one with the libxce allocation comparison (Requirement 1), which now also compares `from`, rather than defining its own.
|
||||
2. The simulator shall keep its pending-decision comparison as a simulator-local main-reason-only check, and shall not use the full-match `job_preason_eq`, because a recorded cache-hit pending reason carries no host/resource detail while a live replayed one always does (bug-1845). This asymmetry is a simulator concern, not a property of pending-reason equality.
|
||||
3. When the simulator reports a verification mismatch, the log line shall keep its existing logical-time and decision context and shall add the description of the first difference (Requirement 3), or, for a PEND mismatch, the two jobids or the two main reason codes. This resolves bug-1881.
|
||||
4. Replaying a recording made after this change against its own previously-recorded output with verification enabled shall report zero mismatches.
|
||||
|
||||
### Requirement 6: Single shared implementation across all consumers
|
||||
**Objective:** As a maintainer, I want the allocation comparisons and the full pending-reason comparison to exist exactly once, in libxce, so that future changes to their semantics are made in one place and every consumer picks them up.
|
||||
|
||||
#### Acceptance Criteria
|
||||
1. The simulator and the three scheduler test files that currently call the static comparators (`test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c`) shall obtain the allocation comparison from the libxce public API, and no scheduler source or test shall retain a copy of it. The simulator's main-reason-only pending-decision check (Requirement 5, criterion 2) is the single documented simulator-local pending-reason comparison.
|
||||
2. The three test files shall compare pending reasons with the full-match `job_preason_eq`. This makes their pending-decision checks stricter than today's main-reason-only comparison. The change is valid because both sides of each of their comparisons are produced without the scheduler's unsatisfiable-allocation cache (none of these tests registers `unsatisfiable_lookup_cb`), so the detail lists are built by the same full per-host computation on both sides and are expected to match; each test file shall document this reasoning where it calls `job_preason_eq`. Their allocation comparisons also become stricter (`from` is compared). If a test fails after either change, the failure shall be investigated as a real divergence, not resolved by loosening the comparison.
|
||||
3. A consumer shall be able to call the public comparators by including `xcealloc.h`/`preason.h` and linking libxce, without white-box-including any scheduler source. The three test files may keep their existing white-box `#include "../src/sim_verify.c"`, since they need it for the decision-level functions that `sim_loop.c` calls, but shall no longer call any comparator that is private to `sim_verify.c`.
|
||||
4. The library shall provide unit test coverage for each new equality function, covering: a match; a mismatch on each compared field, with the expected difference description; that an `instid` difference does not affect allocation equality; that a `from` difference does; that list order matters (the same entries in a different order compare not-equal); a list-length mismatch; the NULL-handling cases at the top level and at nested entries and string fields; a NULL `why`; `*why` set to NULL on equal; and a failed description allocation (`*why` NULL, same result).
|
||||
5. The scheduler shall have tests showing that allocations made in one session carry that session's start time as `from`, and that the recorded `beginRound` equals that start time.
|
||||
6. Adding these functions shall not alter the behavior of any existing libxce function other than the `from` stamping required by Requirement 4, and shall not alter `merge_job_alloc()`'s slot matching.
|
||||
7. Comments in scheduler sources and tests that name the removed `sim_verify_alloc_eq`, `sim_verify_host_alloc_eq`, `sim_verify_job_alloc_eq`, or `sim_verify_job_preason_eq` functions, or that describe `from` as excluded from comparison, shall be updated.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"feature_name": "libxce-alloc-preason-equality",
|
||||
"created_at": "2026-09-24T04:42:20Z",
|
||||
"updated_at": "2026-09-24T05:12:00Z",
|
||||
"updated_at": "2026-09-28T08:40:00Z",
|
||||
"language": "en",
|
||||
"phase": "tasks-generated",
|
||||
"approvals": {
|
||||
|
|
|
|||
|
|
@ -2,68 +2,77 @@
|
|||
|
||||
## Task Format
|
||||
|
||||
Work is sequenced test-first where the deliverable is new library API (TDD: red → green → refactor), then the scheduler-side migration is a behavior-preserving refactor verified by the existing `--sim-verify` tests.
|
||||
Work is sequenced test-first (TDD: red → green → refactor). New library API gets its unit tests first; the `from` stamping change gets a failing scheduler test first; the scheduler-side migration is then verified by the existing and extended `--sim-verify` tests.
|
||||
|
||||
---
|
||||
|
||||
- [ ] 1. Add a shared NULL-safe string helper to libxce (`utils.c`/`utils.h`)
|
||||
- Add `int str_eq(const char* a, const char* b)` beside the existing `float_equal` helper: returns non-zero iff both arguments are NULL, or both non-NULL and `strcmp(a, b) == 0`.
|
||||
- Document that it is the shared NULL-safe string-equality helper for the equality functions (and any future controller consumer).
|
||||
- _Requirements: 1.1–1.6, 2.1–2.3 (supporting)_
|
||||
- Test it in `test_preason.c` or a `utils` test: NULL/NULL, NULL/non-NULL, equal, different.
|
||||
- _Requirements: 1.5, 2.3 (supporting)_
|
||||
- _Boundary: utils.c, utils.h_
|
||||
|
||||
- [ ] 2. Add allocation equality API to libxce (`xcealloc.c`/`xcealloc.h`)
|
||||
- Implement `alloc_eq(const xce_alloc_t*, const xce_alloc_t*)` comparing `value`, `duration`, `resname`, `reserve` (excluding `from`/`instid`), with the shared NULL convention.
|
||||
- Implement `host_alloc_eq(const xce_host_alloc_t*, const xce_host_alloc_t*)` comparing `host` then `allocs[i]` in order.
|
||||
- Implement `job_alloc_eq(const xce_job_alloc_t*, const xce_job_alloc_t*)` comparing `jobid`, `allocs[i]` in order, then `allocs_shared[i]` in order.
|
||||
- Use `str_eq` from `utils.h` for the string fields.
|
||||
- Declare all three in `src/include/xcealloc.h` with a per-function doc comment capturing the equality contract (including the `from`/`instid` exclusion and NULL/order semantics).
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
|
||||
- _Boundary: xcealloc.c, xcealloc.h_
|
||||
- [ ] 2. Allocation equality API with first-difference description (`xcealloc.c`/`xcealloc.h`), test-first
|
||||
- RED: in `test_xcealloc.c`, add tests for `alloc_eq`/`host_alloc_eq`/`job_alloc_eq`: match; mismatch per field (`resname`, `reserve`, `value`, `duration`, `from`) with the expected description in `*why` (freed by the test); `instid` difference still equal; host name, order and list-length sensitivity; `jobid`, ordered hosts, ordered shared, length mismatch; NULL at top level, nested entries and string fields; NULL `why`; `*why == NULL` on equal. In a dedicated test binary with `-Wl,--wrap=vasprintf` (bug-1843's `--wrap` idiom), a failed description allocation leaves `*why` NULL with the same result.
|
||||
- GREEN: implement the three functions with signature `(a, b, char** why)`, comparing in the order given in design.md Data Models, `value` with exact `==` (not `float_equal`), strings via `str_eq`, descriptions via `vasprintf` in the design's path format (`jobid` via `fmt_jobid`, `value` with `%.17g`); a nested call's description is prefixed with the parent's path segment and the inner string freed.
|
||||
- Declare them in `src/include/xcealloc.h` with a doc comment covering: "same allocation decision" equality, `from` compared and `instid` excluded (and why), the difference from `merge_job_alloc()`'s slot matching, NULL/order semantics, the malformed-record precondition, and the `why` ownership contract (caller frees with `free()`; `*why` NULL on equal or on allocation failure).
|
||||
- Do not change `make_alloc()`, `merge_job_alloc()` or `same_alloc_slot()`; confirm existing `test_xcealloc.c` tests still pass.
|
||||
- _Requirements: 1.1–1.7, 3.1–3.5, 6.4, 6.6_
|
||||
- _Boundary: xcealloc.c, xcealloc.h, test_xcealloc.c_
|
||||
- _Depends: 1_
|
||||
|
||||
- [ ] 3. Add pending-reason equality API to libxce (`preason.c`/`preason.h`)
|
||||
- Implement `job_preason_eq(const xce_job_preason_t*, const xce_job_preason_t*)` as a full structural match: `main_reason`, then `host_based_reasons[i]` in order (each by `host` via `str_eq` + `reason`), then `res_based_reasons[i]` in order (each by `resource` + `host` via `str_eq` + `reason`), with the shared NULL convention.
|
||||
- Declare it in `src/include/preason.h` with a doc comment capturing the full-match contract (nothing excluded) and the NULL/order semantics.
|
||||
- _Requirements: 2.1, 2.2, 2.3_
|
||||
- _Boundary: preason.c, preason.h_
|
||||
- [ ] 3. Pending-reason equality API with description (`preason.c`/`preason.h`), test-first
|
||||
- RED: in `test_preason.c`, add tests for `job_preason_eq`: match; main-reason mismatch; host-based list mismatch per field, order and length; resource-based list mismatch per field, order and length; NULL handling; each with the expected description (freed by the test); NULL `why`; `*why == NULL` on equal.
|
||||
- GREEN: implement `job_preason_eq(a, b, char** why)` as a full structural match with the shared NULL convention.
|
||||
- Declare it in `src/include/preason.h` with a doc comment covering the full-match contract (nothing excluded), NULL/order semantics, malformed-record precondition, the `why` ownership contract, and that the simulator deliberately uses a main-reason-only check instead (bug-1845).
|
||||
- _Requirements: 2.1–2.3, 3.1–3.5, 6.4_
|
||||
- _Boundary: preason.c, preason.h, test_preason.c_
|
||||
- _Depends: 1_
|
||||
|
||||
- [ ] 4. Add libxce unit tests for the new equality functions (TDD, before relying on them)
|
||||
- `test_xcealloc.c`: `alloc_eq` match/mismatch per field (value, duration, resname, reserve), `from`/`instid` exclusion, `host_alloc_eq` host-name and order sensitivity, `job_alloc_eq` jobid + ordered hosts + ordered shared, NULL at top and nested levels.
|
||||
- `test_preason.c`: `job_preason_eq` full match — main-reason match/mismatch, host-based list match/mismatch per field and order, resource-based list match/mismatch per field and order, NULL handling; plus `str_eq` NULL/NULL and NULL/non-NULL cases.
|
||||
- Confirm `src/lib/testsuite` builds and passes (`/xce-build lib`).
|
||||
- _Requirements: 4.3, 4.4_
|
||||
- _Boundary: test_xcealloc.c, test_preason.c_
|
||||
- _Depends: 2, 3_
|
||||
- [ ] 4. Reproducible `from`: one session-start reading, stamped on every allocation, test-first
|
||||
- RED: scheduler test(s) asserting (a) every allocation produced in one normal-mode session, including a COMPOUND request merged by `merge_job_alloc()`, has `from == session_start / 1000000000`; (b) with recording on, the recorded `beginRound == session_start`.
|
||||
- GREEN:
|
||||
- `driver.c`: pass `sched->core.session_start` to `sched_record_session_begin()` instead of a second `xce_core_now()` read.
|
||||
- `scheduler.c:1893`: after `make_alloc()`, set `allocs[i]->from = sched->core.session_start / 1000000000ULL`.
|
||||
- Check there is no other scheduler path that builds an `xce_alloc_t` for a decision outside a session; if one exists, stop and raise it (Revalidation Trigger).
|
||||
- Simulation side (Requirement 4.3) is spec `scheduler-sim-discrete-clock`; once it lands, add a test that a replayed allocation's `from` equals the recorded one.
|
||||
- _Requirements: 4.1, 4.2, 4.4, 4.5, 6.5_
|
||||
- _Boundary: driver.c, scheduler.c, new/extended scheduler test_
|
||||
|
||||
- [ ] 5. Migrate the simulator to the public API (`sim_verify.c`)
|
||||
- Delete the five `static` comparators (`sim_verify_str_eq`, `sim_verify_alloc_eq`, `sim_verify_host_alloc_eq`, `sim_verify_job_alloc_eq`, `sim_verify_job_preason_eq`).
|
||||
- Add a new `static` simulator-local `sim_verify_pend_outcome_eq(const xce_job_preason_t*, const xce_job_preason_t*)` (or an inline check) that compares `main_reason` only, documented with the bug-1845 rationale.
|
||||
- In `sim_decision_eq`, replace the DISPATCH payload comparison with `job_alloc_eq(...)` and the PEND payload comparison with `jobid` equality plus the local main-reason-only check.
|
||||
- Add `#include "xcealloc.h"` and `#include "preason.h"`; keep `sim_log_mismatch` and the `describe_*` helpers unchanged.
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
||||
- _Boundary: sim_verify.c_
|
||||
- _Depends: 2, 3_
|
||||
- [ ] 5. Migrate the simulator to the public API and put the difference in the mismatch log (`sim_verify.c`, `sim_loop.c`)
|
||||
- Delete the five `static` comparators (`sim_verify_str_eq`, `sim_verify_alloc_eq`, `sim_verify_host_alloc_eq`, `sim_verify_job_alloc_eq`, `sim_verify_job_preason_eq`) and their `from`-exclusion comments.
|
||||
- Add a `static` simulator-local `sim_verify_pend_outcome_eq(a, b, char** why)` comparing `main_reason` only, documented with the bug-1845 rationale.
|
||||
- Change `sim_decision_eq` to `(expected, actual, char** why)`: DISPATCH → `job_alloc_eq(..., why)`; PEND → jobid equality plus the local main-reason check, describing whichever differs; kind mismatch described too. Update `sim_loop.c`'s `extern` declaration and call.
|
||||
- `sim_log_mismatch`: get the description from `sim_decision_eq`, append ` -- diff: <why>` to the existing line, then `free()` it (resolves bug-1881).
|
||||
- Add `#include "xcealloc.h"`; keep the `describe_*` helpers.
|
||||
- _Requirements: 5.1, 5.2, 5.3_
|
||||
- _Boundary: sim_verify.c, sim_loop.c_
|
||||
- _Depends: 2, 4_
|
||||
|
||||
- [ ] 6. Migrate the three white-box test consumers to the public API
|
||||
- `test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c`: replace direct calls to `sim_verify_job_alloc_eq` → `job_alloc_eq` and `sim_verify_job_preason_eq` → `job_preason_eq`; add the two headers.
|
||||
- Where a file no longer needs `sim_decision_eq`/`sim_log_mismatch`, drop its `#include "../src/sim_verify.c"`; otherwise keep it for the decision-level functions only.
|
||||
- Note: these fixtures produce zero-detail PEND reasons on both sides, so the switch from main-reason-only to full-match `job_preason_eq` is behavior-preserving for them; verify with the full scheduler suite.
|
||||
- _Requirements: 4.1, 4.2_
|
||||
- _Boundary: test_sim_equivalence.c, test_repl_equivalence.c, test_repl_limit_table.c_
|
||||
- _Depends: 5_
|
||||
- `test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c`: replace `sim_verify_job_alloc_eq` → `job_alloc_eq` and `sim_verify_job_preason_eq` → `job_preason_eq` (take the description, print it in the failure message and free it, or pass `NULL`); add the two headers.
|
||||
- Keep each file's white-box `#include "../src/sim_verify.c"`: the files also include `sim_loop.c`, which calls `sim_decision_eq`/`sim_log_mismatch`.
|
||||
- The switch to full-match `job_preason_eq` is valid because none of these tests registers `unsatisfiable_lookup_cb`, so both sides build detail lists by the same full per-host computation; add a short comment at each `job_preason_eq` call stating this. `from` is now compared too. If a test fails after either change, investigate it as a real divergence; do not loosen the comparison.
|
||||
- Update comments that name `sim_verify_*_eq` or describe `from` as excluded in these files and in `test_sim_live_allocation.c` (~702, ~718) and `test_sim_reconcile_unaccounted.c` (~550).
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.7_
|
||||
- _Boundary: test_sim_equivalence.c, test_repl_equivalence.c, test_repl_limit_table.c, test_sim_live_allocation.c, test_sim_reconcile_unaccounted.c_
|
||||
- _Depends: 2, 3, 4, 5_
|
||||
|
||||
- [ ] 7. Reconcile `test_sim_verify.c` with the relocated comparators
|
||||
- Remove the DISPATCH alloc-level unit tests that only re-prove the allocation comparators (identical-content match, per-field mismatch, `from`/`instid` exclusion) — these are now owned by libxce tests from task 4.
|
||||
- Keep the PEND unit tests (identical-content match, jobid/main-reason mismatch, detail-ignored) — these now pin `sim_decision_eq`'s simulator-local main-reason-only behavior, not a libxce function.
|
||||
- Keep the `sim_decision_eq`-level tests (kind mismatch, PREEMPT always-false, NULL defense) and all integration-level `--sim-verify` round-trip tests, which now prove the relocation is behavior-preserving.
|
||||
- _Requirements: 3.1, 3.4, 4.3_
|
||||
- Remove the DISPATCH alloc-level unit tests that only re-prove the allocation comparators — now owned by libxce tests from task 2. Replace the "`from` difference is ignored" test with one that shows same-session allocations verify cleanly and a changed `from` is reported as a mismatch naming `from`.
|
||||
- Keep the PEND unit tests (identical-content match, jobid/main-reason mismatch, detail-ignored) — they pin `sim_decision_eq`'s simulator-local main-reason-only behavior.
|
||||
- Add a test that `sim_log_mismatch`'s line contains the differing field and both values.
|
||||
- Run the `--sim-verify` round-trip tests (zero-mismatch self-replay, altered replay, reconcile cases). Until `scheduler-sim-discrete-clock` lands, at `--sim-speed 0`.
|
||||
- _Requirements: 5.2, 5.3, 5.4, 6.4_
|
||||
- _Boundary: test_sim_verify.c_
|
||||
- _Depends: 5_
|
||||
|
||||
- [ ] 8. Full build + test verification
|
||||
- [ ] 8. Full build, test and live verification
|
||||
- `/xce-build lib` (debug + ASAN + tests) green.
|
||||
- `/xce-build scheduler` (debug + ASAN + tests) green, including `test_sim_verify.c`, `test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c`.
|
||||
- _Requirements: 3.4, 4.4_
|
||||
- _Depends: 4, 5, 6, 7_
|
||||
- `/xce-build scheduler --incremental` (debug + ASAN + tests) green, including `test_sim_verify.c`, `test_sim_equivalence.c`, `test_repl_equivalence.c`, `test_repl_limit_table.c`.
|
||||
- `/xce-build controller --incremental` green (consumes `xcealloc.h`; confirms nothing else depended on the removed names).
|
||||
- Live (after `scheduler-sim-discrete-clock`): record a session on a local deployment, replay it with `--sim-verify`: zero mismatches.
|
||||
- `grep -rn 'sim_verify_\(host_\|job_\)\?\(alloc\|preason\)_eq' src` returns no matches.
|
||||
- After merge: `buglog.py status bug-1881 fixed --commit <sha>` with the Verification section filled in.
|
||||
- _Requirements: 5.4, 6.6, 6.7_
|
||||
- _Depends: 2, 3, 4, 5, 6, 7_
|
||||
|
|
|
|||
11
.kiro/specs/scheduler-sim-discrete-clock/requirements.md
Normal file
11
.kiro/specs/scheduler-sim-discrete-clock/requirements.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Requirements Document
|
||||
|
||||
## Project Description (Input)
|
||||
**Who has the problem:** operators and engineers who replay scheduler recordings with `xce_scheduler --sim` / `--sim-verify` or the simulation REPL, and the libxce-alloc-preason-equality work, which needs a replay to reproduce values derived from a session's start time bit-for-bit.
|
||||
|
||||
**Current situation:** the simulator is already event-driven: `sim_loop.c` never sleeps, and it jumps the virtual clock with `sim_advance_virtual_clock()` to each recorded event timestamp and to each recorded session's `beginRound`. `--sim-speed` (default 1, help text "1=real-time") does not pace anything. It only scales how far the clock drifts with real elapsed time between those jumps (`xce_core_now() = virtual_clock_base + elapsed_real * speed_num / speed_den`). So at the default speed, every clock reading in a replay (`core->session_start`, the `--sim-record` `beginRound`, decision timestamps, REPL position) depends on how fast the host happens to run. For example, a replayed session's `session_start` lands slightly after the recorded `beginRound`. Only `--sim-speed 0` is deterministic. The REPL exposes the same knob as its `speed` parameter.
|
||||
|
||||
**What should change:** make the simulator a pure discrete-event simulation. Remove `--sim-speed`, the REPL `speed` parameter, and the simulator's use of `speed_num`/`speed_den`, so the virtual clock moves only when the replay reaches a recorded event or round timestamp and never drifts with real time. Guarantee that a replayed session's `core->session_start` equals the recorded ReplaySession `beginRound` exactly, so values derived from it (for example `xce_alloc_t.from`, spec libxce-alloc-preason-equality Requirement 4.3) reproduce under `--sim-verify`. Amend the scheduler-core spec where it defines the speed modes (Req 5 AC 3 mode table, AC 13 simulation startup). Expected touch points: `main.c` CLI, `sim.c` `sim_set_virtual_clock()`, `sim_loop.c`, `repl_params.c`/`repl_info.c`, `core_clock.c`/`core.h`, about 20 scheduler tests that set a speed, and `src/docs/deployment.md`. Normal-mode (live) scheduling is unchanged.
|
||||
|
||||
## Requirements
|
||||
<!-- Will be generated in /kiro-spec-requirements phase -->
|
||||
24
.kiro/specs/scheduler-sim-discrete-clock/spec.json
Normal file
24
.kiro/specs/scheduler-sim-discrete-clock/spec.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"feature_name": "scheduler-sim-discrete-clock",
|
||||
"created_at": "2026-09-28T08:46:06Z",
|
||||
"updated_at": "2026-09-28T08:46:06Z",
|
||||
"language": "en",
|
||||
"phase": "initialized",
|
||||
"approvals": {
|
||||
"requirements": {
|
||||
"generated": false,
|
||||
"approved": false
|
||||
},
|
||||
"design": {
|
||||
"generated": false,
|
||||
"approved": false
|
||||
},
|
||||
"tasks": {
|
||||
"generated": false,
|
||||
"approved": false
|
||||
}
|
||||
},
|
||||
"ready_for_implementation": false
|
||||
}
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue