mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
## Summary A REQ handler runs as a spawned task and can pause in its access checks. Before this change it took its subscription ID only when it registered. So: - `REQ x`, then `CLOSE x` while the REQ was still in its access checks: the relay sent `CLOSED x`, and then the REQ registered `x` anyway and sent events and `EOSE` under an ID the client had closed. - `REQ x` (A), then a newer `REQ x` (B): if A finished its checks after B registered, A replaced B, or A's refusal retired B and sent a `CLOSED` the client could not tell from B failing. Now the read loop admits each REQ and sync REQ, in arrival order, before it spawns the handler: the newest request owns the ID from the moment it arrives. A `CLOSE` or a newer same-ID request fences an older one that is still pending. A fenced request does not register, and does not send `NOTICE`, `CLOSED`, `EVENT` or `EOSE` for the ID. This builds on the owner token and lifecycle lock that #8522 added. That PR fenced output after registration; this one moves the token to admission, so the same fence also covers the time before registration. The pubsub retain no longer waits on a full subscriber command queue. Callers hold a connection's lifecycle lock while they retain. A retain past a full queue now leaves a coalesced wake-up, and the subscriber subscribes every retained topic it is missing. A retain that fits in the queue leaves no wake-up, so first retains do not each make the subscriber rescan the map. ## Behavior - `close::admit` runs in the read loop before spawn. It records the request's token in the connection's subscription map, with a flag that says whether the request has claimed yet. It also applies the subscription limit, which moves there from the handlers. - Claims (`claim_admitted_live`, `claim_admitted_search`, the sync claim) register only while the request still owns its token. - Refusals after admission go through `close_if_owner`. The two REQ refusals that also send a `NOTICE` send it in the same critical section. - Refusals before admission (rate limit, admission quota) still retire the old holder, as before. - The old holder stays registered until the newer request claims or is refused, as before. - A channel-access eviction that removes an old holder while a newer same-ID request is still pending retires only the old holder. It sends no `CLOSED` for the ID, because the client already replaced that holder, and the newer request still claims. Client-visible changes to the subscription limit: - The limit is checked on arrival, before scope, auth and filter checks. On a full connection, a REQ for a new ID that would also fail those checks now gets `CLOSED "error: too many subscriptions"` instead of the other refusal. - Requests still in their access checks count toward the limit. ## Scope Live fan-out does not check the token. A same-ID request that has arrived but not yet claimed does not stop live events to the old holder. That path is unchanged here. If a handler panics before it claims, its token stays in the map until a `CLOSE`, a same-ID REQ, or the disconnect removes it. Until then it counts toward the subscription limit. ## Testing - `close_fences_a_req_whose_handler_has_not_run` (REQ and sync REQ): `REQ x` then `CLOSE x` before the handler runs gives only `CLOSED x` and no registration. - `only_the_newest_same_id_req_answers`: of two same-ID REQs, only the newer one answers. - `superseded_or_closed_requests_cannot_claim`: an older request cannot claim after a newer one, and a closed request cannot claim or close. - `revoke_of_a_replaced_holder_spares_the_pending_replacement`: a revoke of the old holder sends no `CLOSED`, keeps the pending request's token, and the pending request then claims its own channel. - `queue_full_retain_does_not_wait_and_schedules_reconciliation` (pubsub). - `live_subscriber_recovers_a_dropped_subscribe_hint` (pubsub, requires Redis): it runs the real subscriber on a live link and drops a topic's subscribe hint with a full queue. The topic is still delivered without a reconnect and without `Confirm`. The same holds after the topic is released, unsubscribed and retained again. - Mutations: with no owner check in `close_owned`, both connection tests fail. With no owner check in `claim_admitted_live`, the claim test fails. With no `replaced` check in the eviction, the revoke test fails. With no reconcile arm in the subscriber, or no `notify_one` in `retain_topic`, the Redis subscriber test fails. - CI: the four new relay tests and the full-queue pubsub test are added to `just test-unit` and `scripts/run-tests.sh`. The Redis subscriber test runs in a new Redis-backed backend-integration step. - `cargo test -p buzz-relay --lib` (with `--skip config::tests`, which hangs locally when run in one process), `cargo test -p buzz-pubsub -- --include-ignored` against a local Redis, `cargo clippy -p buzz-relay -p buzz-pubsub --all-targets -- -D warnings`, `cargo fmt --all -- --check`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>