Auto-stop livelock: act task skipped its own enqueued stop on cooldown¶
Date: 2026-06-01
Area: AKS idle auto-stop (api/services/auto_stop_evaluator.py, api/tasks/azure/idle_autostop.py)
Motivation¶
A deployed, idle AKS cluster (elb-cluster-02) was never auto-stopped even
though auto-stop was enabled and the idle window had long elapsed. App Insights
showed the per-cluster act task repeating every cooldown window without ever
calling stop_aks:
2026-06-01T07:25:16 auto_stop_aks late-skip cluster=elb-cluster-02 reason=cooldown
2026-06-01T06:55:16 auto_stop_aks late-skip cluster=elb-cluster-02 reason=cooldown
Root cause — a self-inflicted livelock¶
The auto-stop pipeline is two Celery tasks:
evaluate_idle_clusters(beat, every 300 s) — decide. For each enabled preference it runsevaluate_cluster; on astopverdict it stampslast_stop_at = nowas a preflight double-enqueue guard (so an overlapping beat tick seesis_in_cooldownand refuses to enqueue a second stop), then enqueuesauto_stop_aks.auto_stop_aks(per-cluster) — act. It re-runsevaluate_clusterto close the decide-vs-act race, then callsstop_aks.
The act task's re-evaluation saw the last_stop_at stamp the beat had just
written one moment earlier, so is_in_cooldown returned True and the act task
late-skipped with reason=cooldown — skipping the very stop it was enqueued to
perform. The next beat tick stamped again, the next act task skipped again, and
the cluster stayed running forever (the stamp keeps getting refreshed inside its
own 30-minute cooldown window).
User-facing change¶
Idle clusters with auto-stop enabled now actually stop after the configured idle window. No SPA/UI change; the cost-saver simply works as designed.
Code change¶
evaluate_cluster(...)gains anignore_cooldown: bool = Falseparameter. When True, the cooldown gate is skipped. All other gates (enabled, ARMpower_state == "Running", extend, active-jobs, state-repo reachability, history-truncation, idle-window) are unchanged.auto_stop_akscallsevaluate_cluster(..., ignore_cooldown=True). The cooldown concern belongs to the decide pass (beat) and the SPA countdown, not to the act pass — the act task only needs to re-confirm the race-sensitive gates. The beatdecidecall and the SPA status route keep the defaultignore_cooldown=False, so a genuine recent stop still blocks an immediate re-stop.
Validation¶
uv run pytest -q api/tests/test_auto_stop_task.py api/tests/test_auto_stop_evaluator.py— 26 passed.- New
test_ignore_cooldown_bypasses_cooldown_gate(evaluator unit) proves the default path keeps oncooldownwhileignore_cooldown=Trueproceeds tostopfor the same idle, freshly-stamped preference. - New
test_auto_stop_aks_stops_despite_preflight_cooldown_stamp(task, end-to-end with the realevaluate_cluster) reproduces the prod livelock — alast_stop_atstamped 1 minute ago plus an idle cluster — and asserts the act task now callsstop_aksinstead of late-skipping. uv run ruff checkon all four touched files — clean.- Full suite
uv run pytest -q api/tests— 2381 passed (1 unrelated flakytest_terminal_exec.py::test_run_truncates_stdout_above_capsubprocess-timeout test, passes in isolation).