Skip to content

Redesign admin overview around queue pressure and operator actions - #543

Draft
hardbyte wants to merge 68 commits into
mainfrom
brian/admin-operations-redesign
Draft

hardbyte wants to merge 68 commits into
mainfrom
brian/admin-operations-redesign

Conversation

@hardbyte

@hardbyte hardbyte commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

The admin overview uses per-minute queue pressure and fleet history to show whether waiting work is draining, falling behind, or has no worker. Queue drilldowns keep state counts, rolling-hour completions, worker capacity, pause/drain actions, and mobile navigation distinct from measured history.

The maintenance leader records queue and fleet samples once a minute in additive migration v058, with configurable retention (48 hours by default). This branch preserves the migration chain #637 (v053) → #631 (v054) → #635 (v055) → #538 (v056) → #602 (v057) before appending history; existing main v051 claim functions and v052 job output remain unchanged. Merge those prerequisites first.

History corrections:

  • Attribute completed, retried, failed, and parked striped jobs to their originating logical dispatcher while preserving the physical queue identity used by storage and handlers.
  • Consume a departing worker’s final published counter delta once. Missing participants or lost counters leave the interval unknown, including when no workers remain; stale snapshots do not inflate live capacity or get counted twice.
  • Preserve unknown callback completion rates on receiver-only queues, so the overview uses its hourly fallback instead of treating missing evidence as zero.

Counter history measures published snapshots; a process can lose its unpublished tail. Leader changes, missing coverage, and callback outcome intervals remain explicit gaps. Current job-state time series are not presented as queue-depth history.

Validation:

  • Required root and separate Python workspace format, all-target/all-feature Clippy, and builds pass.
  • All four history integration tests pass on PostgreSQL 17 and 18, including striped complete/retry/fail/park and a real v057 database running the current worker before v058. The exported 57→58 bundle applies twice while preserving the storage relation and completed job.
  • 14 history unit tests, 16 migration unit tests, and the explicit v054 runtime-grant replay test on both PostgreSQL versions pass.
  • All 56 frontend tests, typecheck, production build, and operations skill validation pass. The departing-worker, striped-outcome, and receiver-only regressions were observed failing before their fixes.
  • full-ci is enabled for this updated head; remote matrix results are pending.

Enqueue lane-order fixture

The PG18 rest shard exposed an inherited #538 fixture assumption: the v056 folded enqueue correctly skipped a provisioning gate on already prepared lanes, so the test waited after its enqueue had committed. The canonical fix is included through #602. It gates the second reservation lock and requires each intended backend PID to wait on the exact expected key; the 20-second bound, result ordering and accounting assertions stay unchanged.

Original-fixture RED and deliberately reversed-order negative controls were preserved on PostgreSQL 17 and 18. The corrected seven cases pass on both majors at this stacked head, with required fmt/Clippy/build. Queue history remains v058 and product behavior is unchanged. The original failed CI job remains recorded; updated-head full CI is pending.

This head also includes the reviewed transaction-wide prune timeout handling from #642 through #635. Lock contention after child locks now rolls back the whole maintenance transaction and returns Blocked; rollup stops at the first blocked slot. No other #642 ancestry was imported. Both Rust workspaces passed required format, all-target/all-feature Clippy, and builds; all seven lane-order cases and both post-child-lock rollback controls passed on PostgreSQL 17 and 18. The existing v058 migration and lane fixture are preserved. Fresh exact-head full CI is pending.

reserve_enqueue_seq read the lane's sequence name only after calling
ensure_lane_sequences, which on every reservation computed two sequence
names, ran two to_regclass lookups and two no-op UPDATEs of the lane
heads. It now reads the name first and provisions only a lane row that
has none. A multi-row range was reserved with nextval followed by setval,
two sequence WAL records; it is now one setval under the lane lock that
every writer of the sequence already holds, and a one-row reservation is
a bare nextval.

The definition moves into awa.refresh_enqueue_functions, called by the
installer and by the v052 tail for every discovered substrate. Signature,
lock key and return value are unchanged, so N-1 binaries and
insert_job_compat call it as before. v050 and v051 are reserved for
parallel branches.
The reserve path probed EXISTS on queue_enqueue_heads for a cached lane
before reserving, but advance_enqueue_head already reads the head row and
repairs a stale cache entry when it is missing, so a cached lane now goes
straight to the reservation. The cancel paths that call ensure_lane
without reserving keep the probe.

The enqueue cursor read fetched the ring authority row on every call.
Once a store has read ledger authority it reads the ledger cursor
directly: the flip to ledger is one-way. prepare_schema and reset clear
the observation.
A ready-only enqueue made ten round trips: BEGIN, job ids, the lane
probe, the reservation, the ring authority and cursor reads, the ready
and segment inserts, NOTIFY and COMMIT. v052 adds
{schema}.enqueue_ready_rows, which for each lane in the caller's lock
order provisions a missing head with the same lane lock and inserts as
ensure_lane_inserts, reserves through reserve_enqueue_seq, then reads
ring_cursor('queue'), appends the ready rows before their segments
(NULL job ids take the job id sequence) and notifies.

enqueue_params_copy and enqueue_params_batch call it in autocommit when
every row is ready, none takes a uniqueness claim and the batch is below
the COPY threshold: one round trip and one transaction. Inside a caller
transaction (uniqueness claims, deferred rows, enqueue_runtime_rows) it
replaces the per-lane ensure and reserve, the cursor reads and both
inserts. Batches of 2,048 rows or more keep COPY. The store probes once
for an executable function and keeps the multi-statement path on a
schema without it.
The one-write reservation read the sequence through a dynamic statement
built per call. pg_sequence_last_value and setval take the sequence as a
regclass, so the read and the write are static; sequence_next_value
still covers a sequence that has not been called yet. Server time per
20-row reservation on PostgreSQL 17: 107 us on main, 55 us with the
dynamic read, 30 us now; WAL per reservation stays at one sequence
record.
…equences

Counts ensure_lane_sequences calls inside a reservation with
transaction-level function statistics: none for a named lane, one for a
lane row whose sequence name is NULL. Fails against the pre-v052
reservation, which provisioned on every call.
…d version

The newer-schema guard test deleted the newest version row and expected
CURRENT_VERSION - 1. Versions are not contiguous (v008 is skipped and
v050/v051 are reserved), so it now expects the highest listed version
below CURRENT_VERSION.
The enqueue cursor read skipped the authority row once the store had seen
ledger, an observation that could come from a caller's transaction that
flipped the authority and rolled back. The cursor is now one statement
that branches on the authority row inline: the same single round trip,
no cache, and only the table privileges the old path used.

The enqueue_ready_rows probe was also permanent. A missing function is
now probed again after a minute, so a runtime started before v052
picks it up without a restart, and a call that fails with
undefined_function or insufficient_privilege forgets the observation so
the next enqueue re-probes and falls back. The probe also requires
EXECUTE on ring_cursor, which the function calls and which is revoked
from PUBLIC.
…nqueue-fold

# Conflicts:
#	CHANGELOG.md
#	awa-model/src/migrations.rs
…den the reserve helper

v052 sits above v050 in the runner, which only applies versions above the
highest recorded, so the branch now carries #527's v050 and cannot land
first. The upgrade guide no longer implies binary-first is safe for
health-gated workers: /readyz waits for the schema to reach the binary's
version. It also describes the current probe (re-probed after a minute,
EXECUTE on ring_cursor required, a lost function failing one call), and
the changelog distinguishes enqueue_params_batch from the COPY entry
point's 2,048-row threshold.

reserve_enqueue_seq rejects a NULL count, which returned a start without
advancing the sequence. The lane-cache rollback test also runs with the
function disabled, since only the multi-statement path fills that cache.
The least-privilege test covers a runtime denied ring_cursor EXECUTE and
EXECUTE revoked after use.
Rewrites the v052 changelog entry from the user's side while resolving the changelog conflict.
# Conflicts:
#	CHANGELOG.md
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • 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

hardbyte and others added 11 commits October 24, 2026 12:26
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
…by leaders, report lock skips

- Analyse only the known ring parents, not every partitioned table in the
  schema, so a substrate sharing a schema with application tables never
  analyses them.
- A leader that resolved canonical at start but holds the queue-storage
  runtime as standby refreshes the queue-storage schema once routing flips to
  it, checked on every pass.
- Take the parent's SHARE UPDATE EXCLUSIVE lock before ANALYZE (SKIP_LOCKED),
  so a conflicting lock on the parent fails the call with lock_not_available
  (recorded as skipped_lock) instead of skipping every partition and reporting
  analyzed. Correct the documentation of what SKIP_LOCKED covers.
- Link the pull request from the migration header.
- Tests: refresh on election before the first interval, both rejection guards
  of the helper from the runtime role, and a locked parent.
…t the batch function

insert_many and insert_many_copy used awa.insert_jobs_compat_batch
unconditionally, so a binary running ahead of migration v053 failed every
batch insert. They now look for the function in the catalog and use the
per-row path until it exists.

The per-row statement asks the catalog as it inserts, the process keeps the
answer, and a call that finds a cached function gone fails once and resets it.
insert_many_skip_unique_conflicts needs the function and reports a schema
without it. Tests run a binary against a schema without the function, through
the migration appearing and the function disappearing again, and mix producers
on the per-row and batch functions on one lane.
…scovery preflight

The fail-closed test rewound the schema by the newest migration and expected
the rerun to hit the substrate-ownership preflight. A migration that touches
no custom substrate, such as the batch insert function, carries no preflight,
so the test now picks the newest migration that does.
A lane's enqueue and claim sequences were created by whichever role first
touched the lane, through SECURITY INVOKER helpers, so only that role and
the roles its default privileges named could use them. Another runtime
role then failed with "permission denied for sequence" on enqueue, claim
or promotion.

Each queue-storage schema now has two SECURITY DEFINER functions owned by
the schema owner, provision_lane and provision_lane_sequences. The lane
helpers create a missing sequence through them, and each new sequence gets
the privileges of the head table it backs. ensure_lane_inserts calls
provision_lane when it exists and is executable, inside a savepoint, and
otherwise writes the lane rows as before.

v053 refreshes the helpers and repairs existing lane sequences one at a
time, reassigning and granting only what the migrating role may and
reporting the rest. awa.lane_sequence_problems and the new
`awa storage lane-sequences` command list what remains with the statements
to run; awa.repair_lane_sequences and `--repair` rerun the repair.
…vision_lane

Queue storage takes any queue name, including an empty one, and any smallint priority, so the provisioning functions only reject a null argument or a shard outside 0-63.
…may analyse

- Revoke EXECUTE on awa.analyze_queue_storage_relation from PUBLIC. The
  SHARE UPDATE EXCLUSIVE lock it takes lasts until the caller's transaction
  ends, so any role could hold it open and block ring prune. The migration
  grants EXECUTE to the roles that already hold INSERT, UPDATE and DELETE on
  the awa head tables, and the function requires the calling session user to
  hold those privileges on the target schema.
- Give the function its own lock_timeout.
- Accept only the two head tables and the thirteen ring parents inside the
  function, not any partitioned table of the schema.
- Probe for the function once per pass, so a binary ahead of the schema makes
  one catalog query instead of fifteen failing calls and server ERROR lines.
  Pass-level outcomes use a new "pass" metric scope.
- Correct the header, which claimed the function takes no lock.
- Tests: a role without EXECUTE and a role without head-table privileges are
  denied, the migration grants EXECUTE to existing grantees, non-ring tables
  are rejected, and a missing helper costs one probe and no calls.
…ded lock wait

v053 reassigned lane sequences inside the migration transaction. ALTER
SEQUENCE ... OWNER takes the sequence's ACCESS EXCLUSIVE lock until commit,
with no lock_timeout, so an open enqueue on one lane could stall the whole
migration and every repaired lane waited for the upgrade to commit.

v053 now only refreshes the helpers. After it commits, awa migrate repairs
each lane sequence with its own statement through
awa.repair_lane_sequence(schema, sequence), which has lock_timeout = 300ms;
on 55P03 the sequence is left unchanged and reported as busy. `awa storage
lane-sequences --repair` uses the same path, and the set-returning
awa.repair_lane_sequences(schema) is gone.

Reassigning needs the current owner's privileges, which the schema owner
lacks for sequences the migration role or an app role created. A migration
role connected with role set to the owner therefore could not reassign its
own sequences. The repair now retries such a sequence as the login role,
lane_sequence_problems reports repair_roles (the login roles that can repair
each sequence), and leftovers are reported with the next step to take. The
docs state which role to run the repair as.
Consume a departing worker's final published counters once and preserve
unknown coverage when a participant disappears. Attribute striped job
outcomes to the originating dispatcher's counter handle while keeping
physical job identities intact. Keep receiver-only callback rates unknown
in the overview so its hourly fallback still applies.

Validation: required Rust formatting/Clippy/workspace build;14 history
unit tests; striped complete/retry/fail/park regression RED then GREEN on
PostgreSQL17/18; existing history cases on both majors;55 frontend tests,
frontend typecheck/build and operations skill validation.
@hardbyte hardbyte added the full-ci Run the full CI matrix (Python build+test, E2E) on this PR label Nov 6, 2026 — with ChatGPT Codex Connector

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run the full CI matrix (Python build+test, E2E) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants