Skip to content

Propose that an idle queue is swept only at a real deadline, caught up on its next request, and parked after 24 h, and that each wake-up attempt is billed - #150

Open
sorenbs wants to merge 1 commit into
slatefrom
idle-queues-design
Open

sorenbs wants to merge 1 commit into
slatefrom
idle-queues-design

Conversation

@sorenbs

@sorenbs sorenbs commented Nov 3, 2026

Copy link
Copy Markdown
Member

Documentation only. This proposes how queues that nobody uses stop costing sweeps and wake-ups, and records the decision to bill each wake-up attempt. Nothing is implemented here.

Why

While a queue has anything pending, it stays on its shard's roster. It is swept at its next deadline, or an hour out when no deadline applies (nq_roster_target). Nothing tells a queue in use from a forgotten one:

  • A backlog nobody claims is swept about 96 times under the default 4-day retention.
  • A drained queue is swept hourly for up to a day while its deletion debt waits for the collector's daily pass.
  • An unanswered wake-up subscription is notified once a minute until its backlog expires. The backoff stops growing at 60 s, so that is about 1,440 notifications a day. Each one is also a state write and an outbound HTTPS request.

On 2026-11-02, a queue cell with 25 ms store latency served about 14 cold touches a second. A million queues touched once an hour needed 32 cells. A million forgotten queues swept once an hour are the same rate of cold work, and they earn nothing.

What changes

New: docs/QUEUES-IDLE.md, revision 9 of the queues design, with decisions D23-D28:

  • D23: a queue is on the roster only for a real deadline. Deletion debt moves to its own index, which the collector reads.
  • D24: the roster is keyed by deadline, so a tick reads only the queues that are due.
  • D25: an operation on an overdue queue first runs one sweep step, and never more than one.
  • D26: a queue with no request for 24 h is parked and swept at most once a day until nothing is pending. Its expired data is retired up to 24 h late.
  • D27: a wake-up subscription goes dormant after 24 h without a useful claim. The next useful claim or send makes it active again.
  • D28: each attempted wake-up notification is one billed queue operation at D14's rate, whatever the receiver answers. One that the project budget refuses is not billed.

Amended:

  • QUEUES-DESIGN.md: §6.8, §8.1, §9.2 and §10 point to the new doc. §14 adds D23-D28, and D14 counts wake-up attempts.
  • OBSERVABILITY-BILLING.md §5: a row for wake-up attempts.
  • docs/README.md: an entry for the new doc.

Findings in the current code (not fixed here)

  • queue_roster reads only the first 4,096 roster rows, ordered by hash, on every tick. A shard with more than 4,096 queues on its roster never sweeps, wakes or collects the debt of the rest.
  • capture_debt reads the state row of every queue on the roster page every 30 s.
  • sweep_due_queues awaits each notification in turn, so a receiver that takes its full 2 s delays every queue after it.
  • The per-project notification budget in §9.2 (10 a second) is not in the code.
  • §6.8 says probe_debt counts the roster's earliest deadline. The code doesn't yet, so a closed shard's queue timers wait until something else opens it.

Decisions

The owner approved the direction on 2026-11-03: catch-up on access instead of hourly sweeps, parking after 24 h, and stopping wake-ups after 24 h. The owner also decided D28. The details of D23-D27 are for review.

Breaking changes

None now. Once implemented, wake-up dormancy, the billing of wake-up attempts, parkedAt, and the late retirement of a parked queue's data are each client-visible. Each becomes an edge record when it lands.

🤖 Generated with Claude Code

…p on its next request, and parked after 24 h, and that each wake-up attempt is billed

A queue with anything pending stays on its shard's roster and is swept at
its next deadline, or an hour out when none applies. A forgotten queue with
a backlog is swept about 96 times under the default retention. A drained one
is swept for up to a day while its debt waits for the collector's daily
pass. An unanswered wake-up subscription is notified once a minute until its
backlog expires. On 2026-11-02 a queue cell served about 14 cold touches a
second, so a million forgotten queues swept once an hour would need about as
much capacity as a million in use.

docs/QUEUES-IDLE.md is revision 9 of the queues design, proposed and not
implemented:
- sweeps only at real deadlines, with deletion debt in its own index;
- a roster keyed by deadline;
- one sweep step on an overdue queue's next request;
- parking after 24 h without a request, swept at most once a day;
- wake-ups dormant after 24 h without a useful claim;
- each attempted wake-up notification billed as a queue operation.

The doc also records four findings in the current code:
- the sweeper reads only the first 4,096 roster rows of a shard, so queues
  past them are never swept;
- the collector re-reads every roster queue's state row every 30 s;
- notifications are sent one at a time on the sweeper's tick;
- the per-project notification budget is not built.

QUEUES-DESIGN.md §6.8, §8.1, §9.2, §10 and §14 point to it, and §14 adds
decisions D23-D28. D14 counts wake-up attempts. OBSERVABILITY-BILLING.md §5
adds the wake-up row, and docs/README.md maps the new document.

The owner approved the direction and decided the billing of wake-up
attempts on 2026-11-03.

Documentation only: no code, tests, ledgers or receipts change.
Edge change: none now; dormancy, the wake-up billing, parkedAt and late
retirement of a parked queue's data each become a record when they land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sorenbs added a commit that referenced this pull request Nov 4, 2026
…ge after the last, so a shard with more rostered queues than one page holds sweeps every one of them

Why: #150, finding 1 (the tuning plan's P3 addition). The
sweeper read the roster's first page (4,096 rows in hash order) every
tick and swept the due queues on it: a queue past the first page was
never swept while the queues ahead of it stayed rostered, so its expired
leases never came back, its retries never fell due by time alone, its
failures never expired and its wake-ups never fired. At 100k queues on
one shard (habanero's queue cell) that is 96% of them.

Change: src/shard/queue_maintenance.rs: roster_page(after) reads at most
ROSTER_PAGE rows after the queue `after` (from the start for None) and
returns where the next page starts, None once a page reaches the
roster's end; the sweeper loop keeps that cursor between ticks, so every
rostered queue is examined within ceil(rows / ROSTER_PAGE) + 1 ticks.
queue_roster (the collector's first page) reads it through
roster_page(None), unchanged. roster_hash and roster_deadline name the
row's parts.

Red (the sweeper reading the first page every tick):
  shard::native_queue_tests::roster::the_sweeper_reaches_every_rostered_queue_past_its_first_page
    assertion `left == right` failed: 40 of the 40 due queues past the first page were never swept
      left: 40
     right: 0
  The test puts 4,096 roster rows of queues due in a thousand years
  ahead (in hash order) of 40 due queues, and asserts each due queue's
  roster row deleted and state row off the roster (roster_at None), and
  every undue row kept exactly.
Green: that test, and every native queue and transaction test (82), on
slate d16f1b0.

Controls: none.
Ledgers: test code only beside queue_maintenance.rs (its mutation owner
exists); no owners.json row.
Formal receipts: none staled.
Edge change: none (a queue past the first page is now swept as one on it
always was).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sorenbs added a commit that referenced this pull request Nov 4, 2026
…ir wake-ups 16 at once, so a shard's sweeps share commit groups instead of waiting one durable write each

Why: #150, finding 3, and a throughput limit found on the
way (the tuning plan's P3 addition). The sweeper awaited each sweep's
durable reply before submitting the next: one sweep per commit window,
about 10 a second at the production WAL window, below the 28 a second
100k queues need (each acked queue carries collection debt and comes
back on the roster an hour later), and the cold warm-up could not group
them because they never shared a group. Each wake-up notification was
awaited the same way, so one receiver that took its whole timeout held
back every other queue's notification.

Change: src/shard/queue_maintenance.rs: sweep_due_queues submits the
page's due queues SWEEPS_AT_ONCE (64, as many as a group warms at once)
at a time (buffer_unordered), collects the wake-ups they ask for
(wake_of; a refused sweep is retried at the queue's next roster turn, as
before) and posts them NOTICES_AT_ONCE (16) at a time
(for_each_concurrent). Each sweep is the same NqOp::Sweep as before.

Red (SWEEPS_AT_ONCE 1, one sweep at a time):
  shard::native_queue_tests::roster::the_sweeper_sweeps_a_ticks_due_queues_together
    assertion `left == right` failed: the due queues' sweeps run together
      left: 1
     right: 64
  80 due queues, every state-row load held open (test_support
  hold_native_state_reads): the most loads in flight at once, then every
  queue swept (each roster row deleted).
Green: both roster tests, and every native queue and transaction test
(83), on slate d16f1b0.

Controls: none.
Ledgers: mutation-owner-sizes.json re-measured for this stack (nq_warm
23, queue_maintenance 158 from 125, nq_claim 67, nq_stage 124, and the
drift slate's own commits left in eight other owners); no owners.json
row (futures_util combinators, no spawn).
Formal receipts: none staled.
Edge change: none (sweeps and wake-ups as before, sooner).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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