Files
buzz/crates/buzz-db/src
Luke Tornquist d21f0c0fdb perf(relay): keep 1 s read caches warm when the pool is saturated (#9012)
## What

The relay's 1-second read caches (restriction state, tenant host, open
channels) now date each entry from when the read checked out its pool
connection, not from when the cache miss began. Concurrent misses on the
restriction and tenant-host caches now share one database read. The
open-channels cache already did this.

## Why

Each of these caches serves an entry for 1 s after "the read that
produced it began". Today that start time is taken before waiting for a
pool connection. Once pool waits reach about a second (for example, many
clients reconnecting at once), every entry is already expired by the
time it lands. The cache hit rate falls toward zero exactly when the
pool is saturated, so every request does its own read. Those reads queue
for the same connections, which stretches the waits further. Requests
that time out in the pool then fail closed (`internal error checking
restriction state`, `community active check failed; refusing the
socket`), and their clients reconnect and add more load.

In a local synthetic reconnect load test with a 50-connection pool:
- the restriction cache served fewer than one request in five from
cache;
- the tenant-host cache, which has a single key for the whole
deployment, missed more often than it hit;
- the open-channels cache missed hundreds of thousands of times on
expired entries.

## How

- **Stamp at checkout.** `buzz_db::ReadAt<T>` carries a value plus the
instant its connection was checked out. The statement's snapshot is no
older than that instant, so serving the value for 1 s after it keeps the
documented staleness bound, however long the read queued. Three
`*_read_at` methods return it: `moderation_restriction_state_read_at`,
`lookup_community_by_host_read_at` and
`get_listed_open_channel_ids_read_at`. The existing methods now call
these and drop the value's timestamp. Each operation keeps one datastore
span under its existing name, carried by the `*_read_at` method.
- **Share concurrent misses.** `SharedReads<K>` is the per-key gate the
open-channels cache already used, moved into its own module. The
open-channels cache, the restriction cache and the tenant-host cache now
all use it. The first miss reads; the others wait for it, then recheck
the cache. A waiter waits for at most the writer pool's acquire timeout
plus `SHARED_READ_STATEMENT_WAIT` (1 s). That covers the leader's own
wait for a connection, which is what grows in a storm, so waiters keep
sharing its read instead of queueing duplicate reads behind it. A failed
or cancelled leader releases its waiters at once. A leader whose
statement hangs stalls them for at most 1 s past the acquire timeout,
never the statement timeout.

- **Reject hosts no community can have.** `bind_community` rejects a
host longer than `communities.host VARCHAR(255)` can hold
(`buzz_core::tenant::MAX_HOST_LEN`, counted in characters) as unmapped,
next to the empty-host check. An unauthenticated caller's arbitrarily
long host therefore never reaches a resolver, cache, gate or the
database.

No staleness bound changes: every entry is still served for at most 1 s
after its read's snapshot. Invalidation paths are unchanged. A read in
flight at an invalidation can still refill the old row for at most that
bound, as documented today.

## What got simpler

The open-channels gate's hand-rolled `try_lock`/`timeout` block is
replaced by `SharedReads::turn`. One gate implementation now serves
three caches instead of one implementation that only one cache had. The
gate's wait is set once per gate rather than passed at each call site.
Provisioning and host binding share one `MAX_HOST_LEN` instead of a
private constant.

## Tests

- `shared_reads::tests`: one caller reads while the rest wait; a waiter
gives up on a hung read after its bound.
- `concurrent_restriction_and_host_misses_share_the_in_flight_read`:
waiters on both caches use the leader's entry; with an unreachable
database, any waiter that read on its own would fail.
-
`postgres_tests::cache_misses_behind_a_held_pool_share_one_fresh_read`:
with a one-connection pool held for longer than every max age, eight
concurrent misses on each of the three caches cost one database read,
and each entry still lands fresh.
- `tenant::tests::an_overlong_host_fails_closed_before_the_resolver`: a
host over the limit fails as unmapped with no gate entry and no lookup;
a host at the limit, including one in multibyte characters, is still
looked up.
- Mutation-checked: each of the following turns these tests red:
stamping any of the three entries before the pool wait, dropping either
new waiter path, letting waiters give up after the cache's max age,
dropping the host length check, or counting the host length in bytes.

🤖 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>
2026-11-04 09:52:21 -05:00
..