mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
perf(relay): serve relay-membership checks from the read replica (#4124)
## Summary Route `Db::is_relay_member` — the membership check that runs on every authenticated HTTP request and WS AUTH — through the standard `route_read` machinery on the bounded arm, instead of adding a bespoke cache (replaces #3844). - `crates/buzz-db/src/relay_members.rs`: add `is_relay_member_on(&mut PgConnection, ...)` executor seam; the pool version delegates to it. - `crates/buzz-db/src/lib.rs`: `Db::is_relay_member` now routes via `route_read("relay_membership", RoutePredicate::Bounded)` — replica only on a proved fresh session, writer on any route rejection, writer re-run on replica query error. Exactly the shape of every other routed read. This is the one permission read served from the replica, by explicit product decision (Tyler accepted ≤1s bounded staleness on reads we choose): the fleet-wide fence guarantee (`BUZZ_REPLICA_READ_MAX_AGE_MS`, deploy target 1s) is an order of magnitude tighter than the 10s TTL proposed in #3844 and needs no invalidation machinery. Staleness is symmetric for admits and revokes. `BUZZ_REPLICA_READ_MAX_AGE_MS` unset = writer-only = kill switch. It is not precedent for routing other permission reads. ## Validation At this exact commit (`git rev-parse HEAD` confirmed in the same shell, rustc 1.95): - `cargo test -p buzz-db` — 94 passed, 0 failed - PG-gated suite single-threaded — **151 passed, 2 failed**; the 2 failures are the per-owner-limit tests broken on main by #3829 (limit 3→5, tests still seed 3) — they fail identically at base `19d57b0d4` in a pristine control checkout; separate trivial fix to follow - New PG-gated test `is_relay_member_is_bounded_routed_and_fails_closed` — divergent writer/replica fixtures prove: budget unset ⇒ writer; budget set + fresh proof ⇒ replica; over-budget entry ⇒ writer - clippy `-D warnings` + fmt clean; pre-push hooks green (desktop check/test, rust tests, tauri checks) - **Live-local pass** (TESTING.md, release binary, `BUZZ_REQUIRE_RELAY_MEMBERSHIP=true`, fresh DB): - writer-only (no `READ_DATABASE_URL`): member accepted, outsider 403 `relay_membership_required`; metrics `route_decision{path="relay_membership",decision="writer",reason="disabled"}` - replica configured + `BUZZ_REPLICA_READ_MAX_AGE_MS=1000`: member accepted / outsider denied via `decision="replica",reason="fresh"`; admit visible to the routed check within ~1.2s; revoke enforced within ~1.2s - reader outage mid-flight (TCP proxy killed): member send still succeeds in <200ms via `decision="writer",reason="reader_acquire_timeout"`; outsider still denied — fails closed, no availability loss Reviewed by Wren: 9/10 minimalness, 9/10 elegance, 9.5/10 correctness at this SHA. Supersedes the 10s-cache approach in PR 3844, which should be closed unmerged once this lands. Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
co-authored by
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
parent
9d6726e5b3
commit
ac4fa13b8e
@@ -3999,8 +3999,33 @@ impl Db {
|
||||
}
|
||||
|
||||
/// Returns `true` if `pubkey` (64-char hex) is a member of `community`.
|
||||
///
|
||||
/// Replica-routed on the bounded arm — the one PERMISSION read routed by
|
||||
/// explicit product decision (bounded-stale membership beats the 10s
|
||||
/// cache it replaced). Admits and revokes may lag by at most the budget
|
||||
/// `B`; everything else fails closed to the writer, exactly like
|
||||
/// [`Db::query_events_routed_bounded`]. Not precedent for routing other
|
||||
/// permission reads.
|
||||
pub async fn is_relay_member(&self, community: CommunityId, pubkey: &str) -> Result<bool> {
|
||||
relay_members::is_relay_member(&self.pool, community, pubkey).await
|
||||
let path = "relay_membership";
|
||||
match self.route_read(path, RoutePredicate::Bounded).await {
|
||||
RouteDecision::Replica(mut tx, _entry, reason) => {
|
||||
match relay_members::is_relay_member_on(&mut tx, community, pubkey).await {
|
||||
Ok(is_member) => {
|
||||
Self::record_route(path, "replica", reason);
|
||||
Ok(is_member)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(path, "replica read failed; re-running on writer: {e}");
|
||||
Self::record_route(path, "writer", "replica_error");
|
||||
relay_members::is_relay_member(&self.pool, community, pubkey).await
|
||||
}
|
||||
}
|
||||
}
|
||||
RouteDecision::Writer => {
|
||||
relay_members::is_relay_member(&self.pool, community, pubkey).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the relay member record for `pubkey` in `community`, or `None` if not found.
|
||||
@@ -7414,6 +7439,78 @@ mod tests {
|
||||
drop_scratch_db(&admin, writer, &wname).await;
|
||||
}
|
||||
|
||||
/// Routed relay-membership check: budget unset ⇒ writer; budget set +
|
||||
/// fresh proved entry ⇒ replica (bounded arm); over-budget entry ⇒
|
||||
/// writer. Divergent membership rows prove which pool answered.
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn is_relay_member_is_bounded_routed_and_fails_closed() {
|
||||
let admin = PgPool::connect(&admin_url().await)
|
||||
.await
|
||||
.expect("connect admin");
|
||||
let (writer, wname) = create_scratch_db(&admin, "mem_w").await;
|
||||
let (replica, rname) = create_scratch_db(&admin, "mem_r").await;
|
||||
|
||||
let community = Uuid::new_v4();
|
||||
for pool in [&writer, &replica] {
|
||||
sqlx::query("INSERT INTO communities (id, host) VALUES ($1, $2)")
|
||||
.bind(community)
|
||||
.bind(format!("member-routing-{}.example", community.simple()))
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("insert community");
|
||||
}
|
||||
let cid = CommunityId::from_uuid(community);
|
||||
let writer_only = "aa".repeat(32);
|
||||
let replica_only = "bb".repeat(32);
|
||||
relay_members::add_relay_member(&writer, cid, &writer_only, "member", None)
|
||||
.await
|
||||
.expect("seed writer member");
|
||||
relay_members::add_relay_member(&replica, cid, &replica_only, "member", None)
|
||||
.await
|
||||
.expect("seed replica member");
|
||||
|
||||
let mut db = Db::from_pools(writer.clone(), replica.clone());
|
||||
db.fence().force_open_for_tests(chrono::Utc::now());
|
||||
|
||||
// Budget unset ⇒ bounded arm disabled ⇒ writer.
|
||||
assert!(
|
||||
db.is_relay_member(cid, &writer_only)
|
||||
.await
|
||||
.expect("gate off"),
|
||||
"budget unset must answer from the writer"
|
||||
);
|
||||
assert!(!db.is_relay_member(cid, &replica_only).await.unwrap());
|
||||
|
||||
// Budget set + fresh entry ⇒ replica.
|
||||
db.set_replica_read_max_age_for_tests(Some(std::time::Duration::from_secs(5)));
|
||||
assert!(
|
||||
db.is_relay_member(cid, &replica_only)
|
||||
.await
|
||||
.expect("gate on"),
|
||||
"budget set must answer from the replica"
|
||||
);
|
||||
assert!(!db.is_relay_member(cid, &writer_only).await.unwrap());
|
||||
|
||||
// Entry older than the budget ⇒ fail closed to the writer. Close
|
||||
// first so no prior fresh entry can be the one proved (matches the
|
||||
// count test; today `force_open_for_tests_at` also clears the ring).
|
||||
db.fence().close();
|
||||
db.fence().force_open_for_tests_at(
|
||||
chrono::Utc::now(),
|
||||
std::time::Instant::now() - std::time::Duration::from_secs(10),
|
||||
);
|
||||
assert!(
|
||||
db.is_relay_member(cid, &writer_only)
|
||||
.await
|
||||
.expect("entry too old"),
|
||||
"an over-budget entry must fail closed to the writer"
|
||||
);
|
||||
|
||||
drop_scratch_db(&admin, replica, &rname).await;
|
||||
drop_scratch_db(&admin, writer, &wname).await;
|
||||
}
|
||||
|
||||
/// Community separation across every routed seam, verified on
|
||||
/// REPLICA-SERVED reads.
|
||||
///
|
||||
|
||||
@@ -29,10 +29,22 @@ pub struct RelayMember {
|
||||
|
||||
/// Returns `true` if `pubkey` (64-char hex) is a member of `community`.
|
||||
pub async fn is_relay_member(pool: &PgPool, community: CommunityId, pubkey: &str) -> Result<bool> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
is_relay_member_on(&mut conn, community, pubkey).await
|
||||
}
|
||||
|
||||
/// [`is_relay_member`] on a specific session — the replica-routing path runs
|
||||
/// the lookup on the exact reader connection whose heartbeat observation
|
||||
/// proved fence coverage.
|
||||
pub(crate) async fn is_relay_member_on(
|
||||
conn: &mut sqlx::PgConnection,
|
||||
community: CommunityId,
|
||||
pubkey: &str,
|
||||
) -> Result<bool> {
|
||||
let row = sqlx::query("SELECT 1 FROM relay_members WHERE community_id = $1 AND pubkey = $2")
|
||||
.bind(community.as_uuid())
|
||||
.bind(pubkey)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(conn)
|
||||
.await?;
|
||||
Ok(row.is_some())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user