diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3d5ce0d968..9f2723544a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -336,6 +336,12 @@ recovers performs the ordinary admission checks. The worker shares graceful shutdown cancellation and has no migration or rollout flag; all relay pods must run this code for the bound to hold deployment-wide. +Targets are ordered deterministically within each scan, so repeated deadline +hits can leave the same tail targets unqueried and fail-close them after grace. +This path does not rotate scan order or jitter authorization-driven closes; if +writer saturation lasts beyond grace, many sockets on a pod can close together +and create a reconnect herd. + ### Step 5: Cleanup On disconnect (any cause): diff --git a/crates/buzz-relay/src/state.rs b/crates/buzz-relay/src/state.rs index a8879a71df..9d4307c0b4 100644 --- a/crates/buzz-relay/src/state.rs +++ b/crates/buzz-relay/src/state.rs @@ -52,6 +52,8 @@ pub(crate) enum CommunityDisconnectReason { AuthorizationDenied, /// The authenticated pubkey lost access to the community (e.g. a ban). AccessRevoked, + /// The relay could not re-establish the socket's access decision. + AuthorizationUnavailable, } impl CommunityDisconnectReason { @@ -69,6 +71,12 @@ impl CommunityDisconnectReason { code: axum::extract::ws::close_code::POLICY, reason: WsUtf8Bytes::from_static("authorization denied"), })), + Self::AuthorizationUnavailable => { + WsMessage::Close(Some(axum::extract::ws::CloseFrame { + code: axum::extract::ws::close_code::POLICY, + reason: WsUtf8Bytes::from_static("authorization unavailable"), + })) + } Self::AccessRevoked => WsMessage::Close(Some(axum::extract::ws::CloseFrame { code: axum::extract::ws::close_code::POLICY, reason: WsUtf8Bytes::from_static("access revoked"), @@ -6786,44 +6794,3 @@ pub(crate) struct LiveAuthorizationSession { /// Most recent successful authoritative access check. pub last_checked_at: TokioInstant, } - -/// Why a community-bound socket is being asked to stop. -/// -/// Only deletion is externally attributed today. Ordinary lifecycle exits keep -/// using cancellation alone and therefore retain the existing bare-close -/// behavior. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum CommunityDisconnectReason { - CommunityDeleted, - /// NIP-FI: the connection's proven pubkey was added to the deny set. - AuthorizationDenied, - /// The authenticated pubkey lost access to the community (e.g. a ban). - AccessRevoked, - /// The relay could not re-establish the socket's access decision. - AuthorizationUnavailable, -} - -impl CommunityDisconnectReason { - pub(crate) fn close_message(self) -> WsMessage { - match self { - Self::CommunityDeleted => WsMessage::Close(Some(axum::extract::ws::CloseFrame { - code: axum::extract::ws::close_code::POLICY, - reason: WsUtf8Bytes::from_static("community deleted"), - })), - Self::AuthorizationDenied => WsMessage::Close(Some(axum::extract::ws::CloseFrame { - code: axum::extract::ws::close_code::POLICY, - reason: WsUtf8Bytes::from_static("authorization denied"), - })), - Self::AccessRevoked => WsMessage::Close(Some(axum::extract::ws::CloseFrame { - code: axum::extract::ws::close_code::POLICY, - reason: WsUtf8Bytes::from_static("access revoked"), - })), - Self::AuthorizationUnavailable => { - WsMessage::Close(Some(axum::extract::ws::CloseFrame { - code: axum::extract::ws::close_code::POLICY, - reason: WsUtf8Bytes::from_static("authorization unavailable"), - })) - } - } - } -}