Repository navigation
Admin UI: is every queue keeping up? (queue history, health API, limited-by) - #545
Conversation
A single snapshot cannot say whether a backlog is growing, and retained created_at cannot stand in for an arrival rate: on the queue-storage substrate compacted terminal rows fall back to finalized_at once their ready entry rotates. So `awa serve` now samples queue depth itself. Server (awa-ui): - QueueHistory keeps an in-memory window (default 60m at 15s) of per-queue available/running/lag, fed by the cached queue_overviews read and by a background sampler that holds only a weak reference and stops when the router is dropped. Served bucketed per minute at /api/stats/queue-history with nulls for gaps. - RouterConfig / router_with_config expose the setting; awa serve adds --queue-history-minutes (0 disables) and --queue-history-interval. Frontend: - Each queue is classified as not being worked, falling behind, long wait, paused, draining (with time to clear), steady or idle from its snapshot plus a least-squares fit over the last 5 minutes. - The overview leads with one verdict, the total waiting count and its last hour, then lists only queues needing attention. Failures are grouped by kind, queue and error; workers show only problems. - One dense QueueFlowTable serves Overview and Queues, switching to cards by container width rather than viewport. The Queues page gets back capacity/rate limit, DLQ policy and descriptor flags, and keeps search, sort and filter in the URL. - Built from the existing Card/Badge/Heading primitives; the page-specific CSS block is gone. Tests: Rust unit and DB-backed sampler tests, vitest coverage for the trend and verdict logic, synthetic browser scenarios rewritten around a stalled / falling-behind / draining / paused story, and the DB-backed Playwright specs updated. Removes the committed screenshots of the previous design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
…r-side The in-memory sampler in `awa serve` could not survive restarts, differed between UI replicas, and could only see depth. Replace it with history the maintenance leader writes, plus measured rates from the workers. Workers (awa-worker): - Per-queue cumulative counters (claimed, completed, failed, retried, rate-limited polls, claim wait since run_at) published in the existing runtime snapshot as an optional `counters` field. Older readers ignore it; older writers leave it absent. Schema v051 (additive, no existing table touched): - awa.queue_history: one row per queue per minute with depth by state, oldest wait, paused, live workers, capacity, and counter deltas (NULL when unknown rather than guessed). - awa.fleet_history: one row per minute with live/healthy/stale instances, leader and running versions. Maintenance leader: - Samples once a minute in a task beside the maintenance loop (so a slow depth read cannot delay rescue or promotion), differences counters per instance with restart and leader-change handling, and prunes past `queue_history_retention` (default 48h; Rust builder and Python `queue_history_retention_hours`; zero stops sampling). awa-ui: - assessment.rs classifies every queue (stalled, behind, slow, paused, draining, steady, idle), says what limits it (no workers, rate limit, worker capacity, spare capacity), and derives arrival and completion rates. One source of truth for the UI and GET /api/health/queues (`?fail_on=critical|warning` answers 503 for plain HTTP alerting). - /api/stats/queue-history serves per-minute series and fleet events (rollouts, leader changes, lost instances) from the new tables. - The in-memory sampler and its CLI flags are gone. Frontend: consumes the server report; shows measured rates, a "Limited by" column, fleet-change markers on the backlog chart and a recent fleet changes list; queue detail shows its own rates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
… v053 - Renumber the history migration to v053: open PRs #515 and #538 claim v051 and v052, and the runner applies only versions above the current one, so this must land after both (noted at the migration list). - Add a "Recovering" verdict: when nothing needs attention but a queue is draining a backlog that takes two minutes or more, say so and when it will be clear, instead of a bare "Keeping up". - Keep draining queues in the overview's focus list so recovery is visible, titled "Recovering (n)" when nothing else needs attention. - Trim copy that narrated the screen: the eyebrow over the verdict, the history coverage line (now shown only when trends are missing), card descriptions, and the Queues footnote (its definition moved to the Waiting column's tooltip). - Tests: a unit test walking one queue through normal, incident, recovery and clear; a database-backed test of the same phases through /api/health/queues with real job rows; synthetic browser scenarios for each phase that also capture review screenshots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
Renumbering to v053 left a gap the runner cannot see past: it applies only versions above the current one, so a database that applied v053 would silently skip v051/v052 when the open PRs claiming them (#515, #538) land, and "one version behind" stopped meaning v052. Keeping v051 turns the collision into a merge conflict in migrations.rs for whichever PR lands second, which forces a deliberate renumber. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
The README showed the pre-0.6 top-nav Jobs and queue detail pages. Use the overview during an incident (hero) and during recovery, in dark mode at 2x like the images they replace. A new opt-in spec regenerates them from the synthetic fleet: AWA_README_IMAGES_DIR=../../docs/images npm run test:operations -- readme-images Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
Every pending bundle must refuse to migrate a database whose registered queue-storage substrate another role could inject code into. v051 does not touch queue-storage schemas, so like v024, v025, v031 and v035 it carries only the discovery bootstrap (and its preflight) ahead of its own step, in built-in runs and exported bundles alike. schema_discovery_owner_test caught the gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
"Spare capacity" read as a cause next to a stalled queue; "No visible limit" says what the diagnosis actually found. The focus list already includes draining queues, so its title counts them too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
…mmit) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2eb1fa7ab4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The table had grown wider than its card at common desktop widths, so the Pause and Drain controls were cut off, and the controls header's sr-only label escaped the scroll container and widened the page. The wrapper is now the label's containing block, the controls are icon buttons pinned to the right edge, and Capacity appears only where there is room. DLQ-off and config-mismatch flags move beside the queue name so they survive that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
Two cases stored misleading per-minute counts: - The first interval after taking leadership, or after a delayed sample, can cover seconds or well over a minute. Intervals more than 15s off a minute now record unknown counts rather than a misstated rate. - Callbacks complete, fail or retry parked jobs from whichever process receives them, which no runtime counts. Runtimes now count parked attempts, and a minute in which a callback could have resolved one of a queue's jobs records its outcome counts as unknown. Claims stay measured. The leader history test also waits for a fleet row with a live instance, since the first sample can land before the runtime's own snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj
The overview now answers one question first: is every queue keeping up? Stacked on #543.
Normal operations
Incident
Queue detail and mobile during the incident
Recovery
The screenshots come from the synthetic Playwright fixture (
e2e/operations-fixture.ts), which drives all three phases.queue_health_scenario_test.rsruns the same story against a real database.What changed
Workers. Each worker runtime reports per-queue outcome counters in its existing snapshot: claimed, completed, failed, retried, rate-limited, claim wait and parked-for-callback. The field is optional and additive, so N−1 binaries ignore it.
History (migration v051). The maintenance leader writes one row per queue per minute to
awa.queue_history, and one fleet row toawa.fleet_history.NULL(unknown), never zero, when a restart, a new runtime, an N−1 runtime without counters, or a sample more than 15s off the minute makes them unknowable.ClientBuilder::queue_history_retentionor Python'squeue_history_retention_hours;0disables sampling.Assessment on the server.
awa-ui/src/assessment.rsturns that history into a verdict for each queue, plus a "limited by" diagnosis: paused, no workers, rate limit, worker capacity, or no visible limit.GET /api/health/queues?fail_on=critical|warningreturns 503 at the threshold, so it works as a probe or alert target.GET /api/stats/queue-history?minutes=Nreturns the series, plus fleet events (version rollouts, leader changes, lost instances).UI.
Related open PRs
JobExecutor::newtest constructors (one extra argument).Testing
CI on this stacked PR runs only lint, build, the browser suite and the wheel smoke test: the PR-check selector skips the Rust and Python test shards for a non-main base. Everything below was run locally on Postgres 16.
cargo test --workspace:migration_test(69/69),schema_discovery_owner_test,awa-modelandawa-clipass.chaos_suite_test::test_finalize_retries_after_terminated_finalization_backendsfails on main too when it runs after the adapter/benchmark/cel tests on a shared DB (it leaves the storage canonical), and passes on a fresh DB.awa serve.test_start_config.pyandtest_descriptors.pypass (31 tests, including the newqueue_history_retention_hoursvalidation test).🤖 Generated with Claude Code
https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj