Skip to content

Add resumable channel sessions - #6887

Draft
SteffenDE wants to merge 11 commits into
mainfrom
sd-resumable-transport
Draft

SteffenDE wants to merge 11 commits into
mainfrom
sd-resumable-transport

Conversation

@SteffenDE

@SteffenDE SteffenDE commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Works upon #6885 and generalizes it into a Phoenix.Transports.Session, such that websockets are resumable if configured with

  socket "/live", Phoenix.LiveView.Socket,
    websocket: [connect_info: [session: @session_options], resumable: true],
    longpoll: [connect_info: [session: @session_options]]

(and enabled on the client)

Longpoll is resumable by default.

Long-poll request failures currently close the socket and force channels to rejoin,
even when the server session is still alive.
This change retries interrupted requests so channels can stay joined
and outgoing pushes can be buffered during recovery.

The server retains message batches until acknowledged and deduplicates
retried pushes using sequence numbers. The client exposes `socket.onSuspend`
and `socket.onResume` callbacks for recovery status; push timeouts still apply.
If the session expires, or an older server is encountered during a rolling deploy,
the client falls back to the existing reconnect behavior.
Long polling acknowledged batches of messages, which ties the bookkeeping
to how long polling groups messages. Both directions now number individual
messages, so another transport can resume a session in the future.

The long-poll server becomes Phoenix.Transports.Session. It keeps the messages
the client did not acknowledge, sends all of them with the number of the last
one, and the client drops the ones it already received. Incoming messages must
arrive in order, as a gap means that messages were lost.

A poll after a failed one resumes the session, which invokes the new optional
Phoenix.Socket.resume/3 callback with the params and connect_info of the
new request. Returning :error stops the socket, so the client connects again.
With the new `:resumable` WebSocket option, clients that opt in with the
`resumable: true` option of the phoenix.js Socket resume their socket over
a new WebSocket after the connection failed, without rejoining channels.

The socket runs in a Phoenix.Transports.Session, which long polling uses
as well. The WebSocket process attaches to it as a pipe. Both sides number
their messages like long polling, keep them until the other side confirmed
them, and send the missed ones again after resuming. The client resumes
with the token of the session and the number of the last message it
received, which invokes Phoenix.Socket.resume/3.

The new ResumableWebSocket transport resumes the session with backoff,
reports interruptions with onSuspend and onResume, and resumes instead of
reconnecting when the heartbeat times out. When the session is gone, or the
server does not support resuming, it closes so the socket connects again.
A socket that started over a WebSocket can now continue with long polling,
and the other way around, as both transports resume the same session with
the same message numbers.

On the server, a resume detaches the WebSocket of the session, so a client
can switch to long polling. Polls the client gave up on when it resumed over
a WebSocket expire instead of taking its messages. Long polling sends binary
messages in base64 with a "base64:" prefix, which a session can have when it
started over a WebSocket.

On the client, ResumableWebSocket falls back to long polling when it cannot
resume the session over a WebSocket within the longPollFallbackMs option of
the socket, for example when a proxy stops allowing WebSockets. It hands the
session to a LongPoll, which resumes it with the token and the number of the
last message received, and sends the messages the server did not confirm.
The socket keeps its connection and channels stay joined.
The comment described a Phoenix 1.8 session receiving a resume, which
cannot happen, as clients only resume sessions that numbered their messages.
The timeout fires when the session is slow to check the resume. Polling
anyway is safe, as the session handles the resume before the flush that
follows, so a rejected resume never delivers messages.
The pipe and the session closed WebSockets with bare RFC 6455 codes, which
hid why they close. Both now use a map of named codes, and the client uses
a shared CLOSE_SESSION_GONE constant for the code that makes the socket
connect again when a session is gone.
WebSock.terminate/2 is optional, and the pipe has nothing to clean up, as
the session monitors it.
The code they cover already has TODOs for Phoenix 1.10, so the tests point
to them as well.
Transports found the session by the pid in the token alone. After a session
stopped, another session can get its pid, so an old token could resume it.
A token is valid for 2 weeks, and on nodes without distribution, pids look
the same after a restart.

Clients that resume now send the random topic of the token with :resume and
:pipe_attach. A session rejects a topic that is not its own but keeps running
for its own client. The WebSocket pipe also requires the endpoint id of the
token for pids of its own node, like long polling does.

Long polling keeps the subscribe of Phoenix 1.8 for its other requests, so
newer nodes and Phoenix 1.8 sessions keep working together during a deploy.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant