mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Two subscribers rode wildcard patterns: `buzz:*:cache-invalidate` and `buzz:*:conn-control`. ElastiCache Serverless restricts `PSUBSCRIBE`, so both fail loudly there. Replace each with exact `SUBSCRIBE`/`UNSUBSCRIBE` on the per-community channels this pod actually needs. Standalone Redis and cluster-mode behave identically — there is no separate serverless mode, and no `SSUBSCRIBE`. Reconciliation is level-triggered. A `desired` closure is re-read inside the subscriber task immediately before each command, never snapshotted, so a queued unsubscribe cannot be applied against interest that has since been renewed. Establishment is recorded only after Redis acks the `SUBSCRIBE`: `sink.subscribe().await` returning `Ok` *is* the acknowledgement, which is what lets a cross-crate gate read acknowledged state without adding a oneshot ack path to fire-and-forget retain. Convergence is driven by a 1s tick, with wakes as a latency shortcut only — a lost or coalesced wake costs latency, never convergence, and the tick is the restore path that fires with no external event. On disconnect the whole established set is cleared, and the next connect rebuilds from `desired` rather than from a remembered active set. The two families have different interest owners. Connection control follows live sockets: a pod needs a community's disconnect commands only while it holds a socket a remote ban could close. Cache invalidation follows *cache residency*, not connection lifetime, because a cached authorization decision outlives the socket that populated it; scoping it to connections would leave a pod serving authz for a community it no longer listens to. Residency lapses on a timer, so no call path owes an unsubscribe. It cannot be driven off a moka eviction listener: moka's notifications are lazy and do not fire at TTL on an idle cache, so an event-driven release would never run on the pod that most needs it. `CacheResidency::admit` deliberately both records residency and returns the gate. Split into a separate gate and a separate record, only an already-admitted community would ever be recorded — so on a fresh pod nothing enters the desired set, nothing is subscribed, nothing is established, and all three authorization caches read through to Postgres for the life of the process with no error and no log. Folding them makes that ordering unrepresentable. The gate covers cache *inserts only*, never reads. Gating reads would discard a live, still-invalidatable cache on every reconnect and turn a pub/sub blip into a DB stampede. A refused insert means the pod reads through — degraded, not stale — and bumps `buzz_authz_cache_insert_skipped_total`. `AUTHZ_CACHE_TTL` is now one constant shared by the three moka caches and by residency, because residency must outlast every entry it protects. Both subscribers moved from an early spawn to alongside their consumer loops: each needs a desired-set closure read off `AppState`, which did not exist at the old spawn point. Tests: five Redis-backed reconciliation tests, asserting on `PUBSUB NUMSUB` rather than `PUBLISH`'s return. `PUBLISH` counts pattern matches, so any unrelated `PSUBSCRIBE buzz:*` client on a shared server inflates it — and exact-subscriber count is precisely the property this change is about, since it cannot be satisfied by a leftover wildcard. 100 exact subscriptions on one connection route every payload to its own community (no cross-wiring under the wrong tenant label); a withdrawn subset unsubscribes while the rest keep routing; a newly desired community converges on the tick with no wake at all; and a severed connection rebuilds the entire desired set. The sever kills only its own client by id — `CLIENT KILL TYPE pubsub` would take down every other pub/sub client on the server, including concurrent tests. Relay-side tests assert the bootstrap closes: the refused first `admit` must still make the community desired, which is the property whose absence is the deadlock above. Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>