feat(relay): huddle-audio-unavailable guardrail under horizontal scaling (§5b)

Plan §5b, decided by Tyler: rather than sticky-route huddles or ship a
silent split-room, a horizontally-scaled deployment surfaces a clear,
client-handleable unavailable signal on huddle join.

- config: huddle_audio_available bool, env BUZZ_HUDDLE_AUDIO_AVAILABLE.
  Defaults true so single-pod (N=1) deployments keep today's huddle
  behavior unchanged. Operators running multiple relay pods set it false.
- audio handler: after auth + membership pass and BEFORE get_or_create joins
  a room, if huddle_audio_available is false we send
  {type:error, code:huddle_audio_unavailable, message:...} and return — no
  silent room join whose frames never cross pods.

Why a config flag and not pod-count self-detection: the relay can't reliably
count its own pods; an explicit operator flag is the honest model and keeps
the §4 fork-B (any-pod-any-connection) generic routing free of huddle
stickiness. The real fix is the out-of-relay media/SFU service (Tyler's
long-term target), out of scope for this rewrite.

Tests: default-true (N=1 compat) and env-false-disables, both green. Full
buzz-relay --lib green at --test-threads=1 (374). Note for this lane: there
is a pre-existing parallel-run env-var race (global_presence_pubsub test
calls Config::from_env without the config tests' ENV_MUTEX guard) — not a
regression from this change; flagged to fix in the wiring lane.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d
2026-06-26 16:27:06 -04:00
parent b966eff547
commit 4de6715ac6
2 changed files with 69 additions and 0 deletions

View File

@@ -183,6 +183,35 @@ async fn handle_audio_connection(socket: WebSocket, state: Arc<AppState>, channe
return;
}
// Huddle audio guardrail (plan §5b). Audio frames are relayed only within
// a single pod; under horizontal scaling (any-pod-any-connection) two peers
// in one huddle can land on different pods and never hear each other. A
// multi-pod deployment sets `huddle_audio_available = false`, and we surface
// a clear, client-handleable "unavailable" signal here — BEFORE joining a
// room — rather than shipping a silent split-room. Single-pod deployments
// leave the flag at its `true` default and keep today's behavior. The fix
// is an out-of-relay media/SFU service (Tyler's long-term target), not
// sticky-routing huddles into this rewrite.
if !state.config.huddle_audio_available {
debug!(
channel_id = %channel_id,
pubkey = %pubkey_hex,
"huddle audio unavailable under horizontal scaling — rejecting join"
);
let _ = ws_send
.send(WsMessage::Text(
serde_json::json!({
"type": "error",
"code": "huddle_audio_unavailable",
"message": "huddle audio unavailable in this deployment"
})
.to_string()
.into(),
))
.await;
return;
}
let room = state.audio_rooms.get_or_create(channel_id);
// Re-check archived status after obtaining the room. This closes the

View File

@@ -80,6 +80,23 @@ pub struct Config {
/// are permitted regardless of auth method (API token, NIP-42).
pub require_relay_membership: bool,
/// Whether this deployment can serve huddle (voice) audio.
///
/// Huddle audio frames are relayed peer-to-peer *within a single pod*
/// (`AudioRoomManager` is an in-process map; only huddle lifecycle events
/// cross pods via Redis). Under horizontal scaling (any-pod-any-connection,
/// plan §4 fork B) two peers in the same huddle can land on different pods
/// and never hear each other. Rather than sticky-route huddles or ship a
/// silent split-room (plan §5b, decided by Tyler), a horizontally-scaled
/// deployment sets this `false` and the relay surfaces a clear, client-
/// handleable "huddle audio unavailable" signal on join.
///
/// Defaults to `true` so single-pod deployments (the N=1 case) keep today's
/// behavior unchanged. Operators running multiple relay pods MUST set
/// `BUZZ_HUDDLE_AUDIO_AVAILABLE=false` until the out-of-relay media/SFU
/// service lands.
pub huddle_audio_available: bool,
/// Optional hex-encoded pubkey of the relay owner.
/// When set, this pubkey is automatically bootstrapped into `relay_members`
/// with the `owner` role on first startup.
@@ -190,6 +207,12 @@ impl Config {
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
// Defaults true → single-pod (N=1) keeps today's huddle behavior. A
// horizontally-scaled deployment sets this false; see the field doc.
let huddle_audio_available = std::env::var("BUZZ_HUDDLE_AUDIO_AVAILABLE")
.map(|v| !(v == "false" || v == "0"))
.unwrap_or(true);
let allow_nip_oa_auth = std::env::var("BUZZ_ALLOW_NIP_OA_AUTH")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
@@ -390,6 +413,7 @@ impl Config {
metrics_port,
pubkey_allowlist_enabled,
require_relay_membership,
huddle_audio_available,
relay_owner_pubkey,
allow_nip_oa_auth,
media,
@@ -440,6 +464,22 @@ mod tests {
!config.allow_nip_oa_auth,
"allow_nip_oa_auth should default to false"
);
assert!(
config.huddle_audio_available,
"huddle_audio_available should default to true so single-pod (N=1) keeps today's huddle behavior"
);
}
#[test]
fn huddle_audio_available_can_be_disabled_for_horizontal_scaling() {
let _guard = ENV_MUTEX.lock().unwrap();
std::env::set_var("BUZZ_HUDDLE_AUDIO_AVAILABLE", "false");
let config = Config::from_env().expect("config");
std::env::remove_var("BUZZ_HUDDLE_AUDIO_AVAILABLE");
assert!(
!config.huddle_audio_available,
"BUZZ_HUDDLE_AUDIO_AVAILABLE=false must disable huddle audio (multi-pod deployments)"
);
}
#[test]