Files
buzz/crates
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc ab490cff5a fix(pubsub): replace PSUBSCRIBE patterns with exact per-community subscriptions
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>
2026-07-31 15:15:06 -04:00
..
2026-07-27 14:18:24 -04:00