Elegant Reconnection with Phoenix WebSockets
October 2, 2017
The problem – getting the client state up to date after it reconnects.
Let’s quickly look at the problem and the solution outlined in the book.
In Chapter 10: Using Channels -> Handling Disconnects, the section begins with:
Any stateful conversation between a client and server must handle data that gets out of sync. This problem can happen with unexpected disconnects, or a broadcast that isn’t received while a client is away. We need to handle both cases. Let’s find out how.
The book then goes on to create a system that uploads
annotation entities, and their database
id, and keeps track of the last one received. If the connection goes away and returns, upon reconnection, the client sends the
id of the last received
annotation with its re-connection attempt.
In this way, the server is able to know to only upload
annotation entities with
ids greater than the
id sent in the re-connection attempt.
This is a perfectly workable solution but suffers a few shortcomings.
- That special way has to be rewritten for every single client that uses the WebSocket.
A More Generic Solution
Thinking about the problem in a generic way, we can see that it boils down to this:
- The server is in a certain state at the time it sends its message to the client.
- During a disconnect, the server’s state will continue to be updated without the client receiving the updates.
- During a reconnect, the server needs to know the old state and send the interim states back to the client to get it up to speed.
With that in mind, we can borrow some ideas from the “Memento” pattern.
Here’s the solution:
- When the server sends a message to the client, it includes with the message, an opaque object (the memento) which contains the minimum information relevant to the current state of the server.
- The memento is stored with the client unmodified and unexamined.
- When the client disconnects and reconnects, it sends the memento to the server with the reconnection attempt.
- The server interprets the memento and applies the new server state to the client.
In this scenario, the only contract between the client and server to implement this is the address of the memento (perhaps a special json key?). That small contract is the only piece of information to be shared among client implementations.
Revisiting Programming Phoenix
Modifying the Programming Phoenix example, we can modify the server to send up a map under the key
__memento__ key every time it receives a message that contains it. When calling
channel.join(), the saved memento is always sent down (again, under the
Now we modify the server code so that every time it sends up a new
annotation entity, it also sends a
__memento__ object that includes the
id of the last
annotation it sends up. It knows it will receive that in the future should a reconnection attempt occur.
- All clients can implement this small piece of code to handle reconnection.
- To support future features, the server can modify the structure of the memento without incurring a change to the client code.