AKS Capacity Gate — Stage 5 telemetry counters¶
Closes the final stage of the 5-stage plan for issue #23.
Prior stages: - Stage 1 — capacity_gate module - Stage 2 — per-job workdir verification - Stage 3a — capacity_signals helper - Stage 3b — wire gate into submit_task - Stage 3c — Bicep defaults + submit gate tests - Stage 4 — /api/blast/capacity route + SPA cell
Motivation¶
Operators want to see how often the gate admits / denies / releases without
SSH-ing into a worker pod and grep-ing blast_gate_* log lines. The metric
budget for this project is intentionally tiny — no Prometheus, no Application
Insights custom metric — so Stage 5 adds the cheapest thing that solves the
problem: per-cluster integer counters kept in memory on each worker and
surfaced through the already-deployed /api/blast/capacity route. The SPA
CapacityGateCell then renders a single line ("admit 12 · deny 3 · lost 1")
under the existing watermark bars.
The design relies on the Container App's single-revision invariant
(minReplicas: 1, maxReplicas: 1) — there is exactly one worker process, so
the in-process dict is the source of truth, no cross-process aggregation is
needed. Counters reset to zero on revision swap, which matches the
operator-visible window (a deploy is a natural "since when?" boundary). The
charter explicitly forbids introducing a managed Redis or a metrics
side-channel; this design honours that constraint.
User-facing change¶
GET /api/blast/capacityresponse gains acountersblock:
"counters": {
"admit_total": 12,
"deny_total": 3,
"release_total": 11,
"reserve_lost_total": 1,
"deny_by_reason": { "cpu_watermark": 2, "slot_cap_reached": 1 },
"last_event_at": "2026-05-31T15:51:00+00:00"
}
- SPA Capacity Gate cell shows a small counter strip below the watermark
bars:
admit 12 · deny 3(andlost 1when reserve-lost has ever fired). The strip is hidden when the backend returns nocountersfield, which preserves backward-compat with any cached client.
API / IaC diff summary¶
api/services/blast/capacity_gate.py:- Added
bump_admit / bump_deny / bump_release / bump_reserve_lostandgate_counters_snapshot(module-level dict guarded bythreading.Lock). - Added
_reset_counters_for_tests(test-only helper, not exported in__all__). __all__updated to include the five new public helpers.api/tasks/blast/submit_task.py:- Added one-line
capacity_gate.bump_*call after every existingblast_gate_*LOGGER.info site (admit / deny / release / reserve_lost). - No behaviour change on the gate-disabled (default) path — counters are only bumped when the gate branch is entered.
api/routes/blast/capacity.py:- Response payload gains
"counters": capacity_gate.gate_counters_snapshot(cluster_name). web/src/api/blast.ts:- Added
CapacityGateCountersinterface andcounters?: CapacityGateCountersonCapacityGateSnapshot. web/src/components/cards/ClusterBento/CapacityGateCell.tsx:- Render a small counter strip below the watermark bars when
countersis present.
No Bicep changes — counters live in memory; no infra surface to provision.
Validation evidence¶
uv run pytest -q api/tests/test_blast_capacity_gate.py api/tests/test_blast_capacity_signals.py api/tests/test_blast_submit_capacity_gate.py api/tests/test_blast_capacity_route.py api/tests/test_blast_capacity_gate_counters.py→ 67 passed (was 56 at end of Stage 4; +8 telemetry tests + 4 submit-task counter integration tests).- New
api/tests/test_blast_capacity_gate_counters.py: 8 unit tests covering zero defaults, per-cluster isolation, deny-by-reason grouping, defensive-copy semantics, empty-cluster fallback, and thread-safety under 200 concurrentbump_admitcalls. - New cases in
api/tests/test_blast_submit_capacity_gate.py: 4 integration tests covering admit+release, retryable deny, hard reject, and reserve-lost race — each asserts the matching counter increments by exactly 1. - Updated
api/tests/test_blast_capacity_route.py: added_reset_countersautouse fixture, asserted zero-defaults on the default-disabled test, and addedtest_capacity_snapshot_surfaces_telemetry_countersthat pre-bumps every counter and asserts the route echoes them. cd web && npm test -- --run src/components/cards/ClusterBento/CapacityGateCell.test.ts→ 7 passed.
Charter compliance¶
- §12a Rule 4 (default-OFF gates): counter bumps are inside the
BLAST_GATE_ENABLED=truebranch — when the gate is off the existing submit-lock path is byte-identical to pre-Stage 5. - §12a Rule 5 (no
Depends(require_caller)on SSE): counters are surfaced via the existingrequire_caller-protected/api/blast/capacityroute. No SSE event stream is touched. - §13 in-process state: counters reset on revision swap; this is the documented contract, not a bug. No Redis, no Cosmos, no metrics SDK added.