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 adisconnect: warningand 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 likeprimary’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§
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_commandsenvelopes immediately (before any parsing/routing — spec §2.2’s hard 3-second requirement), then routesevents_apipayloads 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
oldif present, else never resolve. Lets the cutover socket’s read half participate in the sametokio::select!asprimary’s without needing a separate task — mirrors howdiscord/gateway.rsboxes an optional sleep future for its heartbeat. - open_
and_ 🔒await_ hello - Fetches a fresh Socket Mode URL and connects, waiting for the mandatory
first
helloframe (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+ optionallyold, 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
SlackEventshould be routed to the reactive bus: right event type, right channel, not self-authored. Pure/testable without a real socket. - unix_
secs 🔒