Skip to content

Admin UI: is every queue keeping up? (queue history, health API, limited-by) - #545

Merged
hardbyte merged 12 commits into
brian/admin-operations-redesignfrom
ccr-37ca53f6-xza211
Oct 26, 2026
Merged

hardbyte merged 12 commits into
brian/admin-operations-redesignfrom
ccr-37ca53f6-xza211

Conversation

@hardbyte

@hardbyte hardbyte commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

The overview now answers one question first: is every queue keeping up? Stacked on #543.

Normal operations

Normal operations

Incident

Incident

Queue detail and mobile during the incident

Queue detail during the incident

Incident on mobile

Recovery

Recovery

The screenshots come from the synthetic Playwright fixture (e2e/operations-fixture.ts), which drives all three phases. queue_health_scenario_test.rs runs 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 to awa.fleet_history.

  • Counter deltas are 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.
  • Completed, failed and retried are also unknown for a minute in which a callback could have resolved one of the queue's jobs, since callbacks resolve in whichever process receives them.
  • The default retention is 48h. Set it with ClientBuilder::queue_history_retention or Python's queue_history_retention_hours; 0 disables sampling.

Assessment on the server. awa-ui/src/assessment.rs turns that history into a verdict for each queue, plus a "limited by" diagnosis: paused, no workers, rate limit, worker capacity, or no visible limit.

  • Verdicts: stalled, behind, slow, paused, draining, steady, idle, or unknown.
  • The UI and the new endpoints both read this one assessment:
    • GET /api/health/queues?fail_on=critical|warning returns 503 at the threshold, so it works as a probe or alert target.
    • GET /api/stats/queue-history?minutes=N returns the series, plus fleet events (version rollouts, leader changes, lost instances).

UI.

  • The overview leads with the verdict, the total backlog and its trend, and the measured finishing and arrival rates. Fleet changes show as markers on the sparkline.
  • The queue list shows only what needs attention or is recovering. The queues and queue-detail pages reuse the same assessment.
  • The queues table fits at 1440px. Its controls are icon buttons pinned to the right edge, Capacity appears where there is room, and DLQ-off and config-mismatch flags sit beside the queue name.
  • Gaps in history render as gaps, not zeros.
  • The README images are replaced with the incident and recovery views.

Related open PRs

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:
    • All new tests pass:
      • worker counters in snapshots;
      • the leader writing history, with a prune round trip;
      • the health and history endpoints;
      • the normal → incident → recovery → cleared DB scenario;
      • assessment and history unit tests, including off-minute intervals and callback-resolved outcomes.
    • migration_test (69/69), schema_discovery_owner_test, awa-model and awa-cli pass.
    • Unrelated to this change: chaos_suite_test::test_finalize_retries_after_terminated_finalization_backends fails 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.
  • Frontend:
    • vitest: 54 tests;
    • synthetic Playwright suite: 10 tests, including the three-phase walkthrough that produced these screenshots;
    • DB-backed Playwright suite: 81 tests, on a fresh database against awa serve.
  • Python: the bindings build; test_start_config.py and test_descriptors.py pass (31 tests, including the new queue_history_retention_hours validation test).

🤖 Generated with Claude Code

https://claude.ai/code/session_016oDvumkeByijpFvqb85Qbj

claude added 10 commits October 25, 2026 20:29
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
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7c8a42c4-7716-42b3-bf13-be0df35f9863

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T23:20:25.742907Z 2eb1fa7 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread awa-worker/src/executor.rs Resolved
Comment thread awa-worker/src/maintenance.rs Resolved
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
@hardbyte
hardbyte merged commit 29181af into brian/admin-operations-redesign Oct 26, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants