Harden lab status refresh

This commit is contained in:
codex 2026-06-29 15:19:10 -03:00
parent 381ecb4ea0
commit 8703e6933a
6 changed files with 34 additions and 12 deletions

View File

@ -14,6 +14,15 @@ from .. import settings
_LAB_STATUS_CACHE: dict[str, Any] = {"ts": 0.0, "value": None} _LAB_STATUS_CACHE: dict[str, Any] = {"ts": 0.0, "value": None}
def _status_response(payload: dict[str, Any]) -> Any:
"""Return lab status JSON with browser and proxy caching disabled."""
response = jsonify(payload)
response.headers["Cache-Control"] = "no-store, max-age=0"
response.headers["Pragma"] = "no-cache"
return response
def _vm_query(expr: str) -> float | None: def _vm_query(expr: str) -> float | None:
"""Run one instant VictoriaMetrics query and return the largest value.""" """Run one instant VictoriaMetrics query and return the largest value."""
@ -171,7 +180,7 @@ def register(app) -> None:
now = time.time() now = time.time()
cached = _LAB_STATUS_CACHE.get("value") cached = _LAB_STATUS_CACHE.get("value")
if cached and (now - float(_LAB_STATUS_CACHE.get("ts", 0.0)) < settings.LAB_STATUS_CACHE_SEC): if cached and (now - float(_LAB_STATUS_CACHE.get("ts", 0.0)) < settings.LAB_STATUS_CACHE_SEC):
return jsonify(cached) return _status_response(cached)
t_total = time.perf_counter() t_total = time.perf_counter()
timings_ms: dict[str, int] = {} timings_ms: dict[str, int] = {}
@ -242,4 +251,4 @@ def register(app) -> None:
_LAB_STATUS_CACHE["ts"] = now _LAB_STATUS_CACHE["ts"] = now
_LAB_STATUS_CACHE["value"] = payload _LAB_STATUS_CACHE["value"] = payload
return jsonify(payload) return _status_response(payload)

View File

@ -129,6 +129,8 @@ def test_lab_status_uses_cache_and_probe_fallbacks(monkeypatch) -> None:
payload = response.get_json() payload = response.get_json()
assert response.status_code == 200 assert response.status_code == 200
assert response.headers["Cache-Control"] == "no-store, max-age=0"
assert response.headers["Pragma"] == "no-cache"
assert payload["connected"] is True assert payload["connected"] is True
assert payload["atlas"]["source"] == "grafana" assert payload["atlas"]["source"] == "grafana"
assert payload["dedicated_hosts"]["up"] is True assert payload["dedicated_hosts"]["up"] is True
@ -144,6 +146,7 @@ def test_lab_status_uses_cache_and_probe_fallbacks(monkeypatch) -> None:
second = client.get("/api/lab/status") second = client.get("/api/lab/status")
assert second.get_json() == payload assert second.get_json() == payload
assert second.headers["Cache-Control"] == "no-store, max-age=0"
lab._LAB_STATUS_CACHE["ts"] = 0.0 lab._LAB_STATUS_CACHE["ts"] = 0.0
lab._LAB_STATUS_CACHE["value"] = None lab._LAB_STATUS_CACHE["value"] = None

View File

@ -37,7 +37,8 @@ async function refreshLabStatus() {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 10000); const timeoutId = window.setTimeout(() => controller.abort(), 10000);
try { try {
const resp = await fetch("/api/lab/status", { const resp = await fetch(`/api/lab/status?ts=${Date.now()}`, {
cache: "no-store",
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
signal: controller.signal, signal: controller.signal,
}); });
@ -45,11 +46,13 @@ async function refreshLabStatus() {
labStatus.value = await resp.json(); labStatus.value = await resp.json();
statusError.value = ""; statusError.value = "";
} catch (err) { } catch (err) {
labStatus.value = null; if (!labStatus.value) {
if (err?.name === "AbortError") { labStatus.value = null;
statusError.value = "Live data timed out"; if (err?.name === "AbortError") {
} else { statusError.value = "Live data timed out";
statusError.value = "Live data unavailable"; } else {
statusError.value = "Live data unavailable";
}
} }
} finally { } finally {
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
@ -69,7 +72,7 @@ onUnmounted(() => {
function scheduleNextPoll() { function scheduleNextPoll() {
if (pollTimerId) window.clearTimeout(pollTimerId); if (pollTimerId) window.clearTimeout(pollTimerId);
const delayMs = labStatus.value ? 30000 : 8000; const delayMs = labStatus.value ? 30000 : 4000;
pollTimerId = window.setTimeout(refreshLabStatus, delayMs); pollTimerId = window.setTimeout(refreshLabStatus, delayMs);
} }
</script> </script>

View File

@ -49,7 +49,7 @@ test.beforeEach(async ({ page }) => {
body: JSON.stringify({ enabled: true, reset_url: "https://sso.example.dev/reset" }), body: JSON.stringify({ enabled: true, reset_url: "https://sso.example.dev/reset" }),
}); });
}); });
await page.route("**/api/lab/status", async (route) => { await page.route("**/api/lab/status*", async (route) => {
await route.fulfill({ await route.fulfill({
status: 200, status: 200,
contentType: "application/json", contentType: "application/json",
@ -106,7 +106,7 @@ test("keeps the homepage usable when public status fails", async ({ page }) => {
await page.addInitScript(() => { await page.addInitScript(() => {
window.__LAB_STATUS_FAIL__ = true; window.__LAB_STATUS_FAIL__ = true;
}); });
await page.route("**/api/lab/status", async (route) => { await page.route("**/api/lab/status*", async (route) => {
await route.fulfill({ await route.fulfill({
status: 503, status: 503,
contentType: "application/json", contentType: "application/json",

View File

@ -8,7 +8,7 @@ test.beforeEach(async ({ page }) => {
body: JSON.stringify({ enabled: false }), body: JSON.stringify({ enabled: false }),
}); });
}); });
await page.route("**/api/lab/status", async (route) => { await page.route("**/api/lab/status*", async (route) => {
await route.fulfill({ await route.fulfill({
status: 200, status: 200,
contentType: "application/json", contentType: "application/json",

View File

@ -215,6 +215,13 @@ describe("static shell views and components", () => {
}); });
await flushPromises(); await flushPromises();
expect(app.text()).toContain("true"); expect(app.text()).toContain("true");
expect(global.fetch).toHaveBeenCalledWith(
expect.stringMatching(/^\/api\/lab\/status\?ts=\d+$/),
expect.objectContaining({
cache: "no-store",
headers: { Accept: "application/json" },
}),
);
await app.unmount(); await app.unmount();