- .wolf/anatomy*, memory.md: hook-maintained index and session log through the bug-1851..1854 work. - .kiro/specs/libxce-alloc-preason-equality/: requirements, design, research and tasks generated; none approved yet (spec.json phase tasks-generated). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
19 KiB
Design Document
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.
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".
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.
Goals
- Publish
alloc_eq,host_alloc_eq,job_alloc_eqinxcealloc.handjob_preason_eqinpreason.h. - Make
job_preason_eqa 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
#includedependency onsim_verify.cfor the type-level comparisons. - Preserve
--sim-verifybehavior 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.
Non-Goals
- Do not move
sim_decision_eq,sim_log_mismatch, or thedescribe_*helpers (simulator-specific; depend on capnp/replay types andERROR()). - 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.
Boundary Commitments
This Spec Owns
- The public equality functions
alloc_eq,host_alloc_eq,job_alloc_eq(xcealloc.{c,h}) andjob_preason_eq(preason.{c,h}), and their documented equality contracts. - The shared NULL-safe string-equality helper both modules need (see Decision 5).
- The migration of
sim_verify.cand the three white-box test files onto those functions. - libxce unit tests for the new functions (
test_xcealloc.c,test_preason.c).
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
.replaywire format and thereplay_decision_t/xce_decision_ttypes. - Any change to the controller's consumption of these structs.
Allowed Dependencies
src/include/xce/types.h(the struct definitions — unchanged).xcealloc.c/xcealloc.h,preason.c/preason.h, andutils.{c,h}(the modules that already own construction of these structs, plus the shared string helper).string.h(strcmp) for the string-field comparisons.
Revalidation Triggers
- A change to the
xce_alloc_t/xce_host_alloc_t/xce_job_alloc_t/xce_job_preason_tstruct layouts must re-check the field lists in the equality functions and the exclusion documentation. - 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_eqcontract.
Architecture
Existing Architecture Analysis
src/scheduler/src/sim_verify.c (474 lines) owns two layers:
- Type-level comparators — five
staticfunctions (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_eqis 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. - Decision-level dispatcher + reporting —
sim_decision_eq(dispatches onxce_decision_t.opvsReplayDecision_body_which),sim_log_mismatch, and twodescribe_*helpers. These depend onreplay_decision_t, capnp enums, andERROR().
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.
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
flowchart LR
subgraph scheduler["src/scheduler"]
SV["sim_verify.c\n(sim_decision_eq, sim_log_mismatch, describe_*,\nmain-reason-only PEND outcome check)"]
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)"]
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
XA --> UT
PR --> UT
XA --> TY
PR --> TY
Architecture Integration:
- Selected pattern: co-locate equality with the module that already owns each struct's construction and deep-copy —
allocfamily inxcealloc.c(mirrorsdup_job_alloc),job_preason_eqinpreason.c(mirrorsadd_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 insrc/lib/,const-qualified read-only parameters, lowercase unprefixed function names (make_*/dup_*/erase_*). - Steering compliance: no changes to
types.hlayouts, no new persistence surface, no HA/RBAC/persistence semantics touched.
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 |
File Structure Plan
Modified Files
src/lib/xcealloc.c— addalloc_eq,host_alloc_eq,job_alloc_eq(using the sharedstr_eqhelper).src/include/xcealloc.h— declare the three allocation equality functions.src/lib/preason.c— addjob_preason_eq(full match, using the sharedstr_eqhelper).src/include/preason.h— declarejob_preason_eq.src/lib/utils.c/src/include/utils.h— add a NULL-safestr_eq(const char*, const char*)helper (see Decision 5).src/scheduler/src/sim_verify.c— delete the fivestaticcomparators; havesim_decision_eqcalljob_alloc_eqfor DISPATCH and keep a simulator-localstaticmain-reason-only helper for PEND. Keepsim_log_mismatch+ describe helpers.src/lib/testsuite/test_xcealloc.c— add allocation equality unit tests.src/lib/testsuite/test_preason.c— addjob_preason_eqfull-match unit tests.src/scheduler/testsuite/test_sim_equivalence.c,test_repl_equivalence.c,test_repl_limit_table.c— replace direct calls tosim_verify_job_alloc_eq/sim_verify_job_preason_eqwithjob_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 exercisesim_decision_eq's simulator-local main-reason-only behavior) and the integration-level tests.
System Flows
The comparison flow is unchanged from today — this feature only relocates the leaf logic. sim_decision_eq remains the entry point:
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)"]
B -->|PREEMPT| E["log + report mismatch (unchanged)"]
C --> F["host_alloc_eq per host"] --> G["alloc_eq per resource (excl. from/instid)"]
D --> H["main-reason only, detail ignored (bug-1845)"]
Requirements Traceability
| 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 |
| 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 |
— |
Components and Interfaces
libxce
Allocation equality (xcealloc.c)
| Field | Detail |
|---|---|
| Intent | Public structural equality for the allocation structs |
| Requirements | 1.1–1.6 |
/* 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);
- 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)ignoresfromandinstid;host_alloc_eq/job_alloc_eqare order-sensitive (both sides come from the same deterministic construction, so array order is expected to match, not just multiset).
Pending-reason equality (preason.c)
| Field | Detail |
|---|---|
| Intent | Public full structural equality for xce_job_preason_t |
| Requirements | 2.1–2.3 |
/* src/include/preason.h */
extern int job_preason_eq(const xce_job_preason_t* a, const xce_job_preason_t* b);
- Postconditions: returns non-zero iff
main_reasonmatches, 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. - 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)
/* src/include/utils.h */
extern int str_eq(const char* a, const char* b);
- Postconditions: returns non-zero iff
aandbare both NULL, or both non-NULL andstrcmp(a, b) == 0; zero otherwise.
Data Models
No struct is added or modified. The equality contract itself is the only new "model", captured in the field lists below.
alloc_eqcompares:value(exact==, documented bit-exact-safe because both sides are exact copies of the samedouble),duration,resname,reserve. Excluded:from(wall-clocktime(NULL)stamp),instid(per-processnextid++counter) — both runtime bookkeeping, not identity.host_alloc_eqcompares:host, thenallocs[i]in order fori < nallocs.job_alloc_eqcompares:jobid, thenallocs[i]in order fori < nhosts, thenallocs_shared[i]in order fori < nres_shared.job_preason_eqcompares:main_reason; thenhost_based_reasons[i]in order fori < nhost_reasons, each entry byhostandreason; thenres_based_reasons[i]in order fori < nres_reasons, each entry byresource,host, andreason. Nothing is excluded — this is a full structural match.
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.
Testing Strategy
- Unit (libxce):
test_xcealloc.c— match/mismatch on each compared field, thefrom/instidexclusion, 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.ckeepssim_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-verifyround-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.cmust stay green after migrating to the public API.
Design Decisions
Decision 1: Co-locate equality with construction
- Context: where to put the four functions.
- Alternatives: a single new
xce_equality.{c,h}module; or one function per struct's existing owner module. - Selected: co-locate —
allocfamily inxcealloc.c,job_preason_eqinpreason.c. - Rationale: matches the existing precedent (
dup_job_alloclives besidemake_*inxcealloc.c;add_job_preason/make_pendreasoninpreason.c); avoids a new module for four small functions. - Trade-offs: none material.
Decision 2: Naming (alloc_eq, not xce_alloc_eq)
- Context: public API names.
- Alternatives:
xce_-prefixed names;_equalssuffix. - Selected:
alloc_eq/host_alloc_eq/job_alloc_eq/job_preason_eq. - Rationale: follows the existing unprefixed lowercase convention (
make_alloc,dup_job_alloc,erase_job_preason), with the_eqsuffix as the standard C equality idiom. Thejob_alloc/job_preasonstems match the struct names. - Trade-offs:
alloc_eqis slightly generic, but it is unambiguous in a header (xcealloc.h) that already owns the unprefixedallocfamily.
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_eqcompares onlymain_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_eqand keep the main-reason-only comparison as a simulator-local static helper (selected); (b) publish main-reason-onlyjob_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 iffmain_reasonmatches and both detail lists match in order, field-for-field. The simulator'ssim_decision_eqPEND branch keeps a smallstaticmain-reason-only helper (documented with the bug-1845 rationale) instead of callingjob_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
staticand 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
- 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_equses the same convention.
Decision 5: Promote the NULL-safe string comparison to a shared str_eq in utils.{c,h}
- Context: with
job_preason_eqnow a full match, bothxcealloc.c(host/resname/reserve strings) andpreason.c(host/resource strings) need NULL-safe string comparison. Keeping itstaticin one file forces the other to duplicate it. - Alternatives: (a) shared
str_eqinutils.{c,h}(selected); (b) duplicate astatichelper in each of the two files. - Selected: add
int str_eq(const char* a, const char* b)toutils.{c,h}, beside the existingfloat_equalhelper. - Rationale: one consumer each would have justified a
static; two consumers and a third on the horizon (controller) justify a shared helper.utils.halready 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.