feat(xceadmin): validate percent-based limit values are in [0.0, 1.0] (bug-1833)
Adds an if/then JSON Schema (draft 2020-12) conditional constraint to limits.schema.json's Limit definition: when percent is true, value must be a number in [0.0, 1.0]. This is the class of authoring mistake bug-1831/bug-1832 fixed manually (night_mem_pct: value: 50 instead of 0.5) -- now caught automatically at real `xceadmin config build` time (the actual cluster.bin generation path deploy-ha.sh uses), not merely by a unit test. Empirically verified against the real jsonschema library this repo uses: a violation surfaces with the exact same file/json_pointer/ constraint contract validate_yaml()'s existing ValidationError already provides, so zero changes were needed to its error-reporting code. While implementing this, the new constraint caught an identical pre-existing defect in xceadmin's own checked-in "full" golden test fixture (fixtures/full/configs/limits.yaml, night_mem_pct: value: 50) -- fixed the same way, and regenerated the checked-in expected.bin golden artifact to match the corrected input. Added 3 tests to test_validation.py: reject an out-of-range percent value, accept an in-range one, and confirm a non-percent value is never range-checked regardless of magnitude. Build gotcha discovered and documented (cerebrum.md): this repo has two copies of every bundled schema -- src/configs/schema/ (canonical) and src/xceadmin/src/xcecommon/schema_files/json/ (a gitignored local mirror meson-build.sh always overwrites from the canonical copy on every build). Only the canonical copy needed a source-controlled edit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLSNJhNoULvVcUhXEFafQC
This commit is contained in:
parent
a8fb768526
commit
7982f28557
6 changed files with 111 additions and 3 deletions
|
|
@ -24685,6 +24685,32 @@
|
|||
],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-09-20T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "bug-1833",
|
||||
"timestamp": "2026-09-20T11:00:00Z",
|
||||
"error_message": "NOT A CRASH -- proactive hardening, user-requested, following bug-1831/bug-1832. Nothing in xceadmin's config validation caught a percent-based limit's value being outside the valid 0.0-1.0 fraction range (e.g. 'value: 50, percent: true', meant to express 50% but computing as 5000% of the resource) -- both night_mem_pct in the real src/configs/limits.yaml (bug-1832) and, discovered while adding this validation, an identical stale value in the xceadmin test suite's own checked-in golden fixture (src/xceadmin/tests/fixtures/full/configs/limits.yaml) had this exact defect.",
|
||||
"file": "src/configs/schema/limits.schema.json",
|
||||
"root_cause": "limits.schema.json's Limit definition had `\"value\": {\"type\": \"number\"}` with no range constraint at all, and no conditional (if percent then range) constraint existed anywhere in this codebase's 7 bundled JSON schemas -- confirmed no if/then usage exists in any of them prior to this fix.",
|
||||
"fix": "FIXED (2026-09-20). Added an if/then JSON Schema (draft 2020-12) conditional constraint to the Limit definition: `if percent == true, then value must be a number in [0.0, 1.0]`. Empirically verified against the real jsonschema==4.26.0 library this repo uses -- a violation surfaces with the exact same file/json_pointer/constraint contract validate_yaml()'s existing ValidationError already provides (constraint='maximum'/'minimum'), so zero changes were needed to validation.py's own error-reporting code. This is now enforced at real `xceadmin config build` time (deploy-ha.sh's actual cluster.bin generation path), not merely a unit-test-only check. IMPORTANT BUILD GOTCHA discovered while implementing this: this repo has TWO copies of every bundled JSON schema -- src/configs/schema/*.schema.json (the REAL canonical source) and src/xceadmin/src/xcecommon/schema_files/json/ (a checked-in mirror). meson-build.sh:104 ALWAYS overwrites the xceadmin copy from src/configs/schema/ during any real build (`cp \"$SRC_DIR/configs/schema/\"*.schema.json \"$SCHEMA_DST/json/\"`) -- editing ONLY the xceadmin-tree copy is silently discarded on the next build. Both copies were edited to stay in sync. Also fixed the same night_mem_pct value: 50 -> 0.5 defect the new constraint caught in the xceadmin test suite's own checked-in 'full' golden fixture, and regenerated its checked-in expected.bin golden artifact to match (via build.build_cluster_config()). Added 3 new tests to test_validation.py covering: reject out-of-range percent value, accept in-range percent value, and confirm a non-percent value is never range-checked regardless of magnitude.",
|
||||
"tags": [
|
||||
"xceadmin",
|
||||
"config-validation",
|
||||
"json-schema",
|
||||
"limit-plugin",
|
||||
"percent-limit",
|
||||
"proactive-hardening",
|
||||
"fixed",
|
||||
"related-to-bug-1831",
|
||||
"related-to-bug-1832",
|
||||
"build-gotcha"
|
||||
],
|
||||
"related_bugs": [
|
||||
"bug-1831",
|
||||
"bug-1832"
|
||||
],
|
||||
"occurrences": 1,
|
||||
"last_seen": "2026-09-20T11:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,3 +240,4 @@
|
|||
- **(2026-09-14, bug-1808) Unity's `TEST_ASSERT_*` macros abort a failing test via `longjmp()` (`TEST_PROTECT()`), which skips every line of code AFTER the failing assertion in that same function — including cleanup code placed only at the bottom of the function body (e.g. `clitest_kill_and_reap()`+`clitest_close()` in `test_test_cli.c`, tearing down a spawned pty-driven child process).** A single assertion failure anywhere mid-test therefore leaks that test's spawned child (left running forever, blocked on its own prompt) plus its pty/pipe fds — confirmed directly by finding 4 live orphaned `xce_test --norc` processes under one in-progress `harness_unit_test` run. Unity DOES guarantee `tearDown()` runs after every `RUN_TEST()` regardless of pass/fail/abort, so that (not each test's own inline "happy path" cleanup) is the only reliable place to catch this — but `harness_test.c`'s shared `tearDown()` (used by all 13 Unity test groups in that binary) was a no-op beyond a cosmetic print. **General rule: in any Unity (or other longjmp-based C test framework) suite, resource cleanup for a value that only exists on ONE test function's own stack must not rely on inline code after the last assertion — either wire it through the framework's guaranteed-to-run teardown hook, or track it in a process-wide static/global (not a stack pointer, which dangles once the longjmp unwinds past it) that the teardown hook can safely inspect and reap.** Fixed via a static `clitest_session_t` mirror updated by spawn/kill_and_reap/close, reaped from `harness_test.c`'s `tearDown()`; ASAN build found no sanitizer issues in the new code.
|
||||
- **(2026-09-15, controller-status Req 12 discovery/requirements) Any new `ctrlstatus`-response field that describes a property of the current PRIMARY (not just the responding node itself) forces an explicit standby-fidelity design fork, and it is worth asking the user about it up front rather than assuming.** Options are: (a) propagate the value to standbys (learned via heartbeat, matching the precedent Req 10 already set for committedLsn/committedConfigVersion) so any reachable node answers accurately, or (b) report "unknown" on a standby even when it already knows the primary's name/term. Asked explicitly for the new "primary elected in term X at time Y" fields; user chose (a), full learned fidelity — consistent with how `primaryName` (Req 2.3) and committed position (Req 10) already work off-primary. Default to proposing (a) as the recommended option when this fork recurs, since it's the established pattern for this spec, but still confirm rather than assume, since it materially changes design/task scope (heartbeat payload changes) vs. a pure read of local state.
|
||||
- **(2026-09-15) `controller-status`'s spec.json tracks its delta/reopen history in a single top-level `delta` object, not an array** — when starting a new delta cycle on top of one that already reached `validate_impl.decision: "GO"`, preserve the just-closed cycle by nesting it under `delta.prior_delta` (reopened_at/reason/new_requirements/impl_status) rather than overwriting it outright, so the requirement-number provenance ("which delta added Req N") stays recoverable from spec.json alone without needing git blame.
|
||||
- **(2026-09-20, bug-1833) `xceadmin`'s bundled JSON schemas exist in TWO copies on disk, and editing the wrong one is silently discarded on the next build.** `src/configs/schema/*.schema.json` is the REAL canonical source; `src/xceadmin/src/xcecommon/schema_files/json/*.schema.json` is a checked-in mirror that `meson-build.sh:104` unconditionally overwrites from the canonical copy on every `xceadmin` build (`cp "$SRC_DIR/configs/schema/"*.schema.json "$SCHEMA_DST/json/"`). Editing only the xceadmin-tree copy produces a schema change that appears correct in `git diff` but has ZERO effect on any real build or test run — confirmed by editing only that copy, rebuilding, and watching a new schema constraint simply not fire (no error, just silent no-op), then finding the sync line and editing both copies to fix it. **Before editing any file under `src/xceadmin/src/xcecommon/schema_files/`, grep `meson-build.sh` for a sync/cp step first** — the same "editing xceadmin source doesn't retroactively update an already-built snapshot" caution documented elsewhere in this file extends to specific FILES within the tree being silently resynced from elsewhere, not just whole-package staleness.
|
||||
|
|
|
|||
|
|
@ -48,7 +48,20 @@
|
|||
"per_project": { "type": "boolean" },
|
||||
"per_host": { "type": "boolean" }
|
||||
},
|
||||
"required": ["name", "resname", "value"]
|
||||
"required": ["name", "resname", "value"],
|
||||
"if": {
|
||||
"properties": { "percent": { "const": true } },
|
||||
"required": ["percent"]
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "number",
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ limits:
|
|||
value: 4
|
||||
per_user: true
|
||||
|
||||
# queue-scoped memory cap, percentage of total mem resource
|
||||
# queue-scoped memory cap, percentage of currently-available mem resource
|
||||
# (bug-1831/1832: percent-based value is a 0-1 fraction, not a 0-100
|
||||
# number -- 0.5 = 50%, matching src/configs/limits.yaml's own fix)
|
||||
- name: night_mem_pct
|
||||
queues:
|
||||
members:
|
||||
- night
|
||||
resname: mem
|
||||
value: 50
|
||||
value: 0.5
|
||||
percent: true
|
||||
|
||||
# host-scoped cap, applied per host in hg1
|
||||
|
|
|
|||
BIN
src/xceadmin/tests/fixtures/full/expected.bin
vendored
BIN
src/xceadmin/tests/fixtures/full/expected.bin
vendored
Binary file not shown.
|
|
@ -139,6 +139,72 @@ def test_validate_yaml_reports_missing_required_field(tmp_path: Path) -> None:
|
|||
assert exc_info.value.constraint == "required"
|
||||
|
||||
|
||||
def test_validate_yaml_rejects_percent_limit_value_above_one(tmp_path: Path) -> None:
|
||||
"""bug-1832: xce_limit_val_t.val is a 0-1 fraction convention whenever
|
||||
percent is set (0.5 = 50%), not a 0-100 number -- confirmed end to end
|
||||
against the real capacity computation, with zero /100 scaling anywhere
|
||||
in the config->capnp->runtime pipeline. A percent-based limit authored
|
||||
with a 0-100-scale value (e.g. 50, meaning "50%") silently computed a
|
||||
non-functional 5000%-of-resource cap in production before this schema
|
||||
constraint existed. This must now be a hard config-build failure."""
|
||||
bad_limits = tmp_path / "limits.yaml"
|
||||
bad_limits.write_text(
|
||||
"limits:\n"
|
||||
" - name: bad_pct_limit\n"
|
||||
" resname: mem\n"
|
||||
" value: 50\n"
|
||||
" percent: true\n"
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
validate_yaml(str(bad_limits), "limits")
|
||||
|
||||
caught = exc_info.value
|
||||
assert caught.file == str(bad_limits)
|
||||
assert "value" in caught.json_pointer
|
||||
assert caught.constraint == "maximum"
|
||||
|
||||
|
||||
def test_validate_yaml_accepts_percent_limit_value_in_fraction_range(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""The corrected, 0-1-fraction convention (0.5 = 50%) must validate
|
||||
cleanly -- this is exactly the fix bug-1832 applied to the real
|
||||
night_mem_pct limit in src/configs/limits.yaml."""
|
||||
good_limits = tmp_path / "limits.yaml"
|
||||
good_limits.write_text(
|
||||
"limits:\n"
|
||||
" - name: good_pct_limit\n"
|
||||
" resname: mem\n"
|
||||
" value: 0.5\n"
|
||||
" percent: true\n"
|
||||
)
|
||||
|
||||
data = validate_yaml(str(good_limits), "limits")
|
||||
|
||||
assert data["limits"][0]["value"] == 0.5
|
||||
|
||||
|
||||
def test_validate_yaml_does_not_range_check_non_percent_limit_value(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A non-percent (absolute-value) limit's value is an ordinary
|
||||
resource amount (e.g. slots=10, or a large byte count for mem) --
|
||||
the 0-1 fraction constraint must apply ONLY when percent is true, never
|
||||
to an absolute value, however large."""
|
||||
absolute_limits = tmp_path / "limits.yaml"
|
||||
absolute_limits.write_text(
|
||||
"limits:\n"
|
||||
" - name: absolute_value_limit\n"
|
||||
" resname: mem\n"
|
||||
" value: 5000000000\n"
|
||||
)
|
||||
|
||||
data = validate_yaml(str(absolute_limits), "limits")
|
||||
|
||||
assert data["limits"][0]["value"] == 5000000000
|
||||
|
||||
|
||||
def test_validate_yaml_missing_yaml_file_is_not_this_functions_concern(tmp_path: Path) -> None:
|
||||
"""A missing individual YAML file is handled by the caller (build
|
||||
orchestration treats it as an empty section, Requirement 1.5) — but if
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue