Skip to main content

Module socket

Module socket 

Source
Expand description

Slack Socket Mode connection lifecycle: apps.connections.open, the hello/events_api/disconnect-warning state machine, per-event ACK, the make-before-break reconnect-on-warning dance, and the 5-minute dead-connection timer — plus the outbound chat.postMessage sender.

§Two independent tasks, not one interleaved loop

run_bridge joins the inbound socket loop (run_socket_loop) and the outbound send loop (run_outbound_loop) via tokio::join!, exactly like telegram/poller.rs::run_poll_loop joins its inbound/outbound loops — see that file’s doc comment for the full review finding that motivated the split (an interleaved tokio::select! let an outbound backoff-sleep block inbound polling for as long as the backoff lasted). The same coupling would be worse for Slack: the inbound loop must ACK every events_api envelope within a hard 3-second deadline (spec §2.2), so anything that could delay servicing inbound frames — including an outbound rate-limit wait — must live on a fully separate task. The two loops share no state; outbound doesn’t touch the WebSocket at all, it only calls rest::post_message.

§Make-before-break reconnect-on-warning (spec §2.3)

This is entirely internal to run_socket_loop’s per-connection state (run_session), not a second top-level task — the dance is about managing one logical inbound connection’s lifecycle, which is exactly what run_socket_loop already owns.

run_session holds at most two live socket halves at once:

  • primary: WsHalf — the currently-active connection. All new inbound processing (routing, ACKs for anything just arriving) happens here.
  • old: Option<WsHalf> — set only during the ~10s window between a disconnect: warning and Slack force-closing that connection. Frames still arriving here (Slack can route events to it right up until the close) are drained and ACKed exactly like primary’s, just not promoted to primary status.

When a warning arrives on primary, we don’t tear the connection down and reconnect — we kick off a second, independent connect (fetch a fresh URL via apps.connections.open, open a new WS, wait for its hello) while continuing to service primary unmodified. That connect runs as a pinned, boxed future (cutover_connect) polled as one more tokio::select! branch each loop iteration — the same technique discord/gateway.rs uses for its heartbeat-interval sleep (hb_sleep): a Pin<Box<dyn Future>> defaulting to std::future::pending() when there’s nothing in flight, swapped in only when there’s real async work to track. This is what makes the second connect genuinely concurrent with continuing to drain primary within a single task, no extra tokio::spawn required.

Once that future resolves successfully (new socket reached hello), the next loop iteration promotes it: old = Some(mem::replace(primary, new)). From that point on, primary’s branch handles all new frames as usual, and a second tokio::select! branch (guarded by old.is_some(), built from the next_frame helper below so it evaluates to std::future::pending() — i.e. never fires — when old is None) keeps draining and ACKing old until it errors or closes, at which point it’s dropped. A duplicate warning arriving while a cutover is already in flight is logged and ignored (cutover_pending guard) rather than launching a second concurrent connect.

An unexpected close on primary with no prior warning (network blip, Slack silently dying, etc.) is the plain backoff path: run_session returns Err, and the outer run_socket_loop retry loop reconnects after an exponentially-increasing delay (capped at 60s, matching Discord’s RECONNECT_DELAY_SECS/MAX_RECONNECT_DELAY_SECS constants), always fetching a fresh URL first (never retrying a stale one).

Structs§

WsHalf 🔒

Enums§

FrameOutcome 🔒

Constants§

CHANNEL_MIN_SPACING 🔒
Minimum spacing between two outbound sends to the same channel (spec §2.6 — “~1 msg/s/channel”, enforced with a bit of headroom).
DEAD_CONN_TIMEOUT_SECS 🔒
Proactive dead-connection heuristic (spec §2.3): if no frame of any kind has arrived in this long, force a reconnect even though nothing signaled a close.
HELLO_TIMEOUT_SECS 🔒
Timeout waiting for the first frame (must be hello) after connecting.
MAX_RECONNECT_DELAY_SECS 🔒
RECONNECT_DELAY_SECS 🔒

Functions§

ack_envelope 🔒
Sends {"envelope_id": "..."} back on the socket the envelope arrived on. Kept allocation-light and infallible-as-possible per spec §2.2’s note that the ACK path should stay cheap and unconditional.
connect_and_run 🔒
handle_frame 🔒
Handles one WS frame: ACKs events_api/slash_commands envelopes immediately (before any parsing/routing — spec §2.2’s hard 3-second requirement), then routes events_api payloads to the reactive bus.
handle_outbound 🔒
next_backoff_secs 🔒
Next backoff duration after a repeated failure, doubling and capped — same family used for reconnects (spec §2.6: “on repeated 429s apply the same exponential backoff used for reconnects”). Pure/testable.
next_frame 🔒
Await a frame on old if present, else never resolve. Lets the cutover socket’s read half participate in the same tokio::select! as primary’s without needing a separate task — mirrors how discord/gateway.rs boxes an optional sleep future for its heartbeat.
open_and_await_hello 🔒
Fetches a fresh Socket Mode URL and connects, waiting for the mandatory first hello frame (spec §2.1, §4.1). Used both for the initial connect and for the cutover half of the make-before-break dance.
resolve_channel_id 🔒
Resolves an OutboundMsg.channel_id (empty → bridge default) into the channel id to send to. Pure/testable.
route_event 🔒
run_bridge
Orchestrates the two independent tasks described in the module doc comment. Called once from SlackBridge::init_global’s spawned task.
run_outbound_loop 🔒
run_session 🔒
Runs one connection’s session loop, holding at most two live socket halves (primary + optionally old, see module doc comment).
run_socket_loop 🔒
Outer retry-forever loop: fetch a fresh WS URL, run a session until it ends (cleanly or with an error), back off, repeat. Every attempt — initial connect included — fetches a new URL via apps.connections.open (spec §2.1: the URL is single-use, there is no Discord-style constant Gateway URL to fall back to).
should_route 🔒
True when a SlackEvent should be routed to the reactive bus: right event type, right channel, not self-authored. Pure/testable without a real socket.
unix_secs 🔒

Type Aliases§

WsRead 🔒
WsStream 🔒
WsWrite 🔒