AKS start: kill the "UI stays Starting until refresh" lag¶
Motivation¶
After clicking Start on a stopped AKS cluster, the cluster card could keep
showing Starting… even after the cluster had actually settled
(provisioning_state="Succeeded"), until the user manually refreshed the page.
The state would only update on the next manual reload, making Start feel
unresponsive.
Root cause¶
Two combining issues, both downstream of the monitor snapshot cache:
-
The monitor cache is per-process, so the lifecycle task cannot invalidate it.
/api/monitor/akscaches the cluster list (30 s TTL + stale-while-revalidate) inside theapisidecar. Thestart_aksCelery task runs in theworkersidecar — a separate process — so it has no way to drop theapiprocess's cachedprovisioning_state="Starting"snapshot when the start LRO completes. The SPA therefore kept seeingStartinguntil the cache TTL/stale window expired and a poll happened to trigger a background ARM re-query. -
The 10 s "fast-poll" during a transition never fired. Both transition poll effects in
useClusterActions.tslisted the wholequeryobject in their dependency arrays.queryis a fresh reference every render, so thesetIntervalwas torn down and recreated on every parent render and the 5 s / 10 s ticks rarely elapsed. The SPA fell back to the 30 s base poll, compounding issue (1).
A manual refresh "fixed" it only because it forced an immediate fetch that eventually rode the stale-while-revalidate path to a fresh ARM read.
User-facing change¶
- While a cluster start/stop transition is in flight, every poll now asks the
backend for a cache-bypassed (
fresh=true) ARM read, so theprovisioning_statelabel settles toSucceededthe moment ARM flips — no manual refresh needed. Once the transition chip clears, normal 30 s caching resumes. - The 5 s task-status poll and the 10 s fast-poll now actually run for the full duration of a transition.
API / code change summary¶
api/services/monitor_cache.py:cached_snapshot(...)gains a keyword-onlyforce: bool = False. WhenTrueit bypasses both the fresh-hit and stale-hit early returns and refreshes synchronously (still storing the result for subsequent normal reads), and never queues a background refresh.api/routes/monitor/aks.py:GET /api/monitor/aksgains afresh: boolquery param mapped tocached_snapshot(force=fresh)on both the RG-scoped and subscription-wide paths. Documented as the cross-process-safe way to read authoritative ARM state during a transition.web/src/api/monitoring.ts:monitoringApi.aks(sub, rg?, { fresh })adds the optionalfreshflag (sets?fresh=true).web/src/components/cards/ClusterCard/useClusterActions.ts: exported a purehasActiveClusterTransitions(sub, rg)reader (reuses the persisted-transition store); changed both transition poll effects to depend on the stablequery.refetchinstead of the per-renderqueryobject.web/src/components/cards/ClusterCard/ClusterCard.tsx: the AKS cluster-list query'squeryFnnow readshasActiveClusterTransitions(...)at call time and passes{ fresh }, so polls during a transition bypass the cache.
No IaC change. No new dependency.
Validation¶
uv run pytest -q api/tests/test_monitor_cache.py api/tests/test_monitor_aks_fresh.py→ 19 passed (2 newforce-bypass unit tests + 2 new routefreshtests).uv run pytest -q api/tests→ 2818 passed, 3 skipped (the lonetest_terminal_exec::test_run_truncates_stdout_above_capfailure is a pre-existing parallel-timing flake — passes in isolation — and is unrelated to this change).uv run ruff check api/services/monitor_cache.py api/routes/monitor/aks.py …→ All checks passed.cd web && npx tsc --noEmit→ clean;npx eslinton changed files → clean;npx vitest run transitionTargetReached.test.ts aksStatus.test.ts→ 11 passed;npm run build→ green.