mirror of
https://github.com/block/buzz.git
synced 2026-11-06 17:31:19 +00:00
## What Socket admission (root and audio) and the ephemeral write fence now share one cache of "is this community active?". An entry is served for 1 s after its read checked out a pool connection, and concurrent misses for one community share one read. Archive and deletion drop the entry before they close the community's sockets. ## Why Every new socket runs `is_community_active` before it can authenticate. In a reconnect storm that means thousands of identical reads for one community. A read that times out in the pool refuses the socket (`community active check failed; refusing the socket`), and the client then reconnects and adds more load. On a local cold-start reconnect rig, this refusal count roughly tripled once #9012 freed pool capacity for other reads. It is now one of the top causes of connect failures. ## How - `buzz_db::Db::is_community_active_read_at` returns the answer together with the instant its connection was checked out, following the #9012 `ReadAt` pattern. `is_community_active` now calls it, and the datastore span stays under its existing name. - `community_active::CommunityActiveCache` holds an entry per community and a `SharedReads` gate. An entry is served for `COMMUNITY_ACTIVE_MAX_AGE` (1 s) after its read. Both answers are cached. A cached "inactive" fails safe and lasts at most 1 s after an unarchive. Errors are not cached. - **The archive sweep stays exact.** Admission registers a socket before it checks, so the sweep closes every socket registered before it. A socket that registers after the sweep must see the community as inactive. `apply_community_disconnect` invalidates the entry before the fence and sweep. Invalidation bumps an epoch, and a fill whose read began before the bump is not published. A post-sweep socket therefore reads afresh. It cannot take a pre-archive answer from the cache or from a shared in-flight read. Host invalidations (archive and unarchive writes, local and received) also drop the entry. - **Bound when the notification is missed.** A pod that misses the Redis disconnect admits sockets from a cached "active" for at most 1 s after the archive commits. Lifecycle revalidation then closes them. This matches the 1 s host-routing bound in `docs/community-auth-upgrades.md` ("archive and deletion … cached tenant binding can lag those changes by up to one second"). Deletion keeps its own write fence (`guard_community_write`). - Lifecycle revalidation and persistent ingest still read uncached. ## What got simpler The ephemeral fence's separate `serving_active_cache` is gone. It ran the same SQL predicate through `is_serving_active`, used a 10 s TTL and had no invalidation. The connect path, the WebSocket ephemeral path and HTTP typing now share one cache with one 1 s bound and one invalidation path. That leaves one fewer cache concept, and the ephemeral fence goes from 10 s stale to at most 1 s. ## Tests - `community_active::tests` (unit): - 16 concurrent misses cost one read, and an entry past its max age is re-read. - An invalidation during an in-flight read fences that fill. A waiter that joined the read reads afresh and gets "inactive". - Errors are not cached. - `connection::tests::bootstrap_admits_from_the_cached_community_answer`: with the database unreachable, a cached "active" admits a socket through the real `handle_connection`, and the socket gets its AUTH challenge. - `postgres_tests::cache_misses_behind_a_held_pool_share_one_fresh_read` now also covers this cache. With a one-connection pool held past every max age, eight concurrent admission checks cost one DB read, and the entry lands fresh. - `postgres_tests::archived_community_is_refused_after_invalidation_or_max_age`: after an applied archive, the next check refuses at once. An archive this pod never hears about refuses once the entry passes 1 s. - The ephemeral and typing fence tests now seed the new cache. - Mutation-checked on Blox. Each of these turns a test red: dropping the epoch fence, the max-age filter, the single-flight gate, the connect path's cached read, or the disconnect invalidation; stamping `read_at` before the pool wait; and calling the uncached read on the ephemeral path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>