feat: the System tab reads a stored sample instead of inspecting per load (4295)
CI and images / lint (push) Failing after 3s
CI and images / extension-version (push) Successful in 4s
CI and images / frontend-build (push) Successful in 31s
CI and images / backend-lint-and-test (push) Successful in 35s
CI and images / integration (push) Successful in 2m44s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped

Operator: "there is a repull every time this page loads is there a reason
this info isn't being tracked in the background and stored in some way?"

There was a reason and it had expired, and underneath it there was plain
waste.

The expired one: /api/system/workers was deliberately uncached because an
operator dragging the stepper must not be shown a pre-change value. That
stopped being true at 1353d34, when the UI began patching its row from the
write's reply instead of refetching.

The waste: size_worker_lanes already inspected the broker on a timer to
decide pool sizes — computing the pool, active, reserved and queue depth
the page shows, using them, and discarding them. The browser then asked
the broker for the same numbers four times a minute, per open tab.

So one inspect now feeds three things: the sizing decision, a stored
sample (worker_lane_sample, alembic 0107), and the celery roster. No
request path touches the broker at all — the roster refresh comes off
/api/system/health too, where it had been rate-limited to 20s and so made
worker liveness a function of whether anyone had a browser open.

Consequences, stated rather than hidden:

- The live figures are up to one sweep old. measured_at travels with each
  lane and the page says how old, because a stale number presented as
  current is how someone watches a queue "not move" that is moving.
- The sweep is the roster's only writer now, so its period and the
  staleness thresholds are in a relationship. 60s against a 90s stale
  threshold left one missed tick between normal and all-yellow — the
  shape of lesson #4355 — so the period is 30s, named once in
  worker_lanes, and system_health asserts its headroom at import with a
  test stating the same thing in prose.
- An idle lane therefore also gives a worker back twice as fast. That is
  the direction asked for: "idle instances quiet down when not running".

Also bounds the inspect in push_lane_cap, which was an await with no
deadline (rule 156) — harmless while it ran on a request, less so now
that it runs in a background task where a hang would be silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-23 18:52:08 -04:00
co-authored by Claude Opus 5
parent 7f1693a40d
commit 45bb7044f7
17 changed files with 871 additions and 133 deletions
+85 -47
View File
@@ -31,7 +31,7 @@ from __future__ import annotations
import asyncio
import logging
from sqlalchemy import func, select
from sqlalchemy import func
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
@@ -40,10 +40,11 @@ from .worker_lanes import LANES, lane_for_node
log = logging.getLogger(__name__)
# How stale the roster may be before a health request refreshes it. Comfortably
# under the staleness thresholds that decide a service is missing, so the
# verdict is never limited by how often anyone looked.
REFRESH_TTL_SECONDS = 20.0
# There is no refresh TTL any more. It existed because the HEALTH REQUEST
# refreshed the roster, rate-limited to 20s so that a page open in two tabs
# did not inspect twice as often. `size_worker_lanes` owns the refresh now, on
# `SWEEP_PERIOD_SECONDS`, so the cadence is a schedule rather than a side
# effect of someone looking.
# celery inspect is a broker round trip and this sits on a request path, so it
# gets a deadline (rule 156). A broker that has stopped answering must make the
@@ -140,10 +141,13 @@ def _inspect_celery_sync() -> dict[tuple[str, ...], dict]:
return grouped
async def touch_service(
session: AsyncSession, *, key: str, kind: str, display_name: str, details: dict
) -> None:
"""Record that a part checked in just now.
def touch_service_stmt(*, key: str, kind: str, display_name: str, details: dict):
"""The upsert that records a check-in, as a statement.
Built here rather than inline so the async caller (an agent lease, over
the API) and the sync one (the sizing sweep, in a celery task) run the
SAME write. Two spellings of one upsert is the kind of duplication that
stays correct right up until one of them gains a column.
Upsert rather than read-modify-write: several web processes and several
agents can be doing this at once, and the last writer is simply the most
@@ -154,7 +158,7 @@ async def touch_service(
stmt = pg_insert(ServiceSeen).values(
key=key, kind=kind, display_name=display_name, details=details,
)
stmt = stmt.on_conflict_do_update(
return stmt.on_conflict_do_update(
index_elements=[ServiceSeen.key],
set_={
"kind": stmt.excluded.kind,
@@ -163,7 +167,75 @@ async def touch_service(
"last_seen_at": func.now(),
},
)
await session.execute(stmt)
async def touch_service(
session: AsyncSession, *, key: str, kind: str, display_name: str, details: dict
) -> None:
"""Record that a part checked in just now."""
await session.execute(touch_service_stmt(
key=key, kind=kind, display_name=display_name, details=details,
))
def _roster_rows(grouped: dict[tuple[str, ...], dict]) -> list[dict]:
"""The `touch_service` arguments for everything that answered.
Split from the write so the async and sync refreshes below share the
mapping as well as the statement — what a roster row IS should not depend
on which kind of session is writing it.
"""
return [
{
"key": "celery:" + ",".join(queues),
"kind": "celery",
"display_name": role_display_name(queues),
"details": {
"queues": list(queues),
"hostnames": entry["hostnames"],
"replicas": len(entry["hostnames"]),
"active": entry["active"],
},
}
for queues, entry in grouped.items()
]
def refresh_celery_roster_sync(session) -> None:
"""The roster refresh, from the sizing sweep's sync session.
## Why the sweep owns this now
It used to run on the request path, rate-limited to once every 20s by the
newest celery row. So the roster only advanced while somebody had a
browser open — the liveness of the workers was a function of whether
anyone was looking at them, which is the observer-effect version of the
bug this roster exists to prevent.
Operator, 2026-09-23: *"there is a repull every time this page loads — is
there a reason this info isn't being tracked in the background and stored
in some way?"*
Now a timer writes it and the page only reads. The cadence is
`SWEEP_PERIOD_SECONDS`, and `api/system_health` asserts it leaves headroom
under the staleness thresholds — because a sweep period and a stale
threshold chosen in different files and never compared is exactly how the
idle GPU agent came to read as stopped (lesson #4355).
Never raises. A failure means the roster does not advance, and the rows
going stale is then a TRUE report about a broker nobody can reach.
"""
try:
grouped = _inspect_celery_sync()
except Exception:
log.warning(
"service roster: celery inspect failed; roster not refreshed",
exc_info=True,
)
return
for row in _roster_rows(grouped):
session.execute(touch_service_stmt(**row))
session.commit()
async def refresh_celery_roster(session: AsyncSession) -> None:
@@ -186,39 +258,5 @@ async def refresh_celery_roster(session: AsyncSession) -> None:
log.warning("service roster: celery inspect failed; roster not refreshed", exc_info=True)
return
for queues, entry in grouped.items():
await touch_service(
session,
key="celery:" + ",".join(queues),
kind="celery",
display_name=role_display_name(queues),
details={
"queues": list(queues),
"hostnames": entry["hostnames"],
"replicas": len(entry["hostnames"]),
"active": entry["active"],
},
)
async def refresh_if_stale(session: AsyncSession) -> None:
"""Refresh the celery roster if nobody has for REFRESH_TTL_SECONDS.
Rate-limited by the data rather than by a lock: the gate is the newest
last_seen_at across the celery rows, which every web process can see. Two
processes racing through the gate costs one redundant inspect and writes
the same values twice, so the benign outcome needs no coordination to
prevent.
"""
newest = (
await session.execute(
select(func.max(ServiceSeen.last_seen_at)).where(ServiceSeen.kind == "celery")
)
).scalar_one_or_none()
if newest is not None:
age = (await session.execute(select(func.now()))).scalar_one() - newest
if age.total_seconds() < REFRESH_TTL_SECONDS:
return
await refresh_celery_roster(session)
for row in _roster_rows(grouped):
await touch_service(session, **row)