Skip to content

feat(gateway): Keep per-status request membership records so a queue's requests can be listed by status - #895

Draft
roychying wants to merge 7 commits into
mainfrom
chenghan.ying/sqv2-gateway-request-status
Draft

roychying wants to merge 7 commits into
mainfrom
chenghan.ying/sqv2-gateway-request-status

Conversation

@roychying

@roychying roychying commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Why?

The SQv2 dashboard needs queue depth: how many requests each queue holds at each status right now, and how long the oldest has been waiting. Nothing answers "requests of queue Q at status S" today. request_summary only supports reads by request ID, a (queue, status) index would break the key-value storage contract, and scanning a queue's summaries reads its whole history.

This replaces #891, which kept these records in the orchestrator. The gateway is the better home, as discussed on #891:

  • Summary status is finer than orchestrator request.state: it shows validating, batching, speculating and landing, where the orchestrator stays batched from batching through landing.
  • The summary has received_at_ms, so age since submit is available.
  • No extra writes on the orchestrator's pipeline path.

What?

  • Entity: QueueRequestStatus{Queue, Status, RequestID, ReceivedAtMs, EnteredAtMs}, plus InQueueRequestStatuses(), the one list of in-queue statuses (every public status except accepting and the terminal statuses); IsRequestStatusInQueue derives from it.

  • Store contract: QueueRequestStatusStore with List(status), Put, Delete. Same advisory, per-record pattern as QueueBatchStateStore: single-key idempotent writes, no multi-row transactions.

  • MySQL: gateway queue_request_status table, PK (queue, status, request_id), so listing by status is a primary-key prefix scan. Put is INSERT … ON DUPLICATE KEY UPDATE entered_at_ms = entered_at_ms, so re-filing keeps the original entry time.

  • Opt-in: mysql.NewStorage(db, scope, WithQueueRequestStatusRecords()). Without it, record writes are no-ops and List returns an error, so a bump can't write to a table the deployment doesn't have yet. The reference gateway server enables it.

  • Materializer: on every status-log delivery, after the summary update, PersistLog keeps the record in step with the summary:

    • files the record for the summary's current status. That's a no-op when it exists, and it also repairs a delivery that failed after the summary update;
    • re-reads the summary after that put and deletes the record it just filed if the status has moved on;
    • when the delivery moved the summary, deletes the previous status's record after filing the new one, so a request in the queue always has at least one record;
    • on a terminal summary, deletes the request's record under every in-queue status. Every terminal redelivery retries this sweep, and it also clears records stranded by earlier failed moves.

    accepting and terminal statuses have no record. Event logs never move the summary, so they skip the record.

  • Records are advisory, and stale rows happen in normal operation. Land and Cancel persist logs outside the log consumer's per-request ordering, and a delivery whose lease expires can overlap its redelivery. In each case a late Put can re-file a status another writer already moved the summary away from. The re-read after each put removes those, because every writer updates the summary before deleting the old record. The terminal sweep covers finished requests. A record stranded by a failed move while the request is still in the queue remains until the request finishes. So readers must confirm each listed request against its summary, count it only under the summary's status, and delete rows whose summary is finished or at a different status. That's one summary read per listed row per reader per sample. request_summary stays the only source of truth.

  • Docs: gateway schema README (new section, including the stale-row rules and reader cleanup), the store contract and entity docs, storage README key-value contract, AGENTS.md and the service-scoped RFC (fourteen store contracts, five gateway accessors).

Test Plan

  • Unit: status vocabulary (each status is in-queue, terminal, accepting or unknown); MySQL store list/put/delete including queue mismatch and no-op cases; the storage option off and on.
  • Unit (materializer): first record on acceptance, put-then-delete ordering on a status change, removal on a terminal status, same-status re-log, re-filing on a losing redelivery, no record while accepting, put and delete failures returned for retry.
  • Existing gateway controller and log-consumer tests updated for the new accessor.
  • Contract test TestStorage_QueueRequestStatusRecordLifecycle against real MySQL (runs in CI): bucket listing, re-put keeps the first entered_at_ms, moves, queue isolation.
  • make lint (queue-shard linter: all 33 tables shardable), check-gazelle, check-tidy, check-mocks.

Issue

@roychying
roychying force-pushed the chenghan.ying/sqv2-gateway-request-status branch from ccb6d37 to 08996ad Compare October 20, 2026 18:39
@roychying
roychying changed the base branch from chenghan.ying/sqv2-dashboard-metrics to main October 20, 2026 18:39

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant