AKS auto-stop recognises live OpenAPI BLAST runs¶
Motivation¶
Three linked defects in the AKS idle auto-stop loop:
- Countdown missing before stop. The "time until auto-stop" never showed; the cluster jumped straight to stopped.
- State flapping. After a manual start, the header chip flashed
Starting...→Stopped→Starting...on refresh. - OpenAPI runs invisible. A BLAST job submitted through the OpenAPI
surface (not the dashboard) was never written to the dashboard
jobstateTable, so the idle evaluator saw zero active jobs and stopped the cluster mid-run.
Root cause of (1) and (3) is the same: the evaluator only scanned the
dashboard jobstate Table (_scan_cluster_jobs). Without a live signal,
an active OpenAPI run looked idle → the idle clock had already expired →
no warn window (no countdown) → immediate stop.
User-facing change¶
- A cluster running any
app=blastworkload — including OpenAPI- submitted runs the dashboard never recorded — is no longer auto-stopped. The auto-stop banner showsactive_jobs:Nwith the real live count. - The countdown is restored: live activity re-anchors the idle clock, so the warn window (and visible countdown) appears before a stop.
- The start/stop chip (
Starting.../Stopping...) now persists until ARMpower_stateactually reaches the target, instead of clearing on Celery task success whilepower_stateis still eventually-consistent — removing the flapping.
The live signal is additive only: it can keep a cluster alive or reset the idle clock, but it can never force a stop. If the Kubernetes API is unreachable the probe degrades to "no signal" and the evaluator falls back to the existing jobstate-Table decision, so a K8s outage can never strand a cluster running forever.
API / code diff summary¶
- NEW
api/services/auto_stop_live.py—probe_live_blast_activity(pref)best-effort read-only probe overk8s_check_blast_status(job_id=None). Returns(live_active_jobs, live_latest_activity)orNoneon any failure (exception / non-dict /status == "unknown").completed/failedruns with lingering pods reportactive == 0(do not block a stop) but still seed the activity anchor. api/services/auto_stop_evaluator.py—evaluate_clustergains two optional paramslive_active_jobs/live_latest_activity(defaultNone, backward compatible). Live active jobs are added to the jobstate count; live latest activity extends the idle anchor. The pure evaluator still performs no SDK calls.api/tasks/azure/idle_autostop.py— new_live_blast_signal(pref, power_state)helper (probes only whenpower_state == "Running", degrades to(None, None)); wired into both the per-clusterauto_stop_aksre-evaluation and the beatevaluate_idle_clustersfan-out.api/routes/aks/autostop.py—_compute_statusmirrors the same live probe (Running clusters only, try/except guarded) so the SPA status countdown agrees with the beat driver.web/src/components/cards/ClusterCard/useClusterActions.ts— keep the optimistic transition chip forstarting/stoppinguntil the realpower_statesettles; onlydeletingclears immediately.
No IaC change. No new dependency. The active_jobs:{n} reason string and
active_job_count field (SPA banner contract) are preserved.
Validation¶
uv run pytest -q api/tests/test_auto_stop_live.py— 9 passed (new).uv run pytest -q api/tests/test_auto_stop_evaluator.py— includes 6 new live-signal cases (add-not-max, fallback onNone, anchor reset, stale anchor does not block).uv run pytest -q api/tests/test_auto_stop_task.py— 13 passed, includingtest_auto_stop_aks_live_activity_blocks_idle_stopandtest_live_blast_signal_skips_probe_when_not_running.uv run pytest -q api/tests/test_aks_autostop_route.py— 20 passed.uv run ruff check api— clean.cd web && npm run build— succeeds;npm test -- --run— 536 passed.