(Re)making the argument for using Phoenix Channels

Looking for help in getting my facts straight in a technical dispute I’m currently facing.

What I am hearing is that the Phoenix Channels implementation is basically free to silently drop push messages from server to client, given a fairly short window of client unavailability (5 seconds). I presume that’s based on a reading of https://hexdocs.pm/phoenix/channels.html#resending-client-messages, which as I understand it concerns client-to-server push messages.

Before I try (again) to make my case, I’m looking for confirmation that I understand the following things correctly or correction if I’ve got it wrong:

  • When a Phoenix server instance sends a message to a client, it will either successfully deliver the message or the socket will have disconnected. (In the disconnect case, it is not possible to determine which messages were transmitted and which were not. That is OK in our proposed design. We are prepared to re-establish state when reconnecting.)

  • The Phoenix server instance will not selectively transmit messages as long as it remains connected. (In other words, it would be a Bad Thing™ for us if a momentary delay in network availability could cause messages 1, 3, 5, and 6 to be sent while 2 and 4 would get silently dropped, unless the entire connection were terminated shortly thereafter.)

The counter to the above statements are being used as sufficient cause to discredit any other argument in favor of using Phoenix Channels.

I should note that in our application space, there is no need for client-to-server push messages, so if the timeout case can be provably constrained to client-to-server messages, that would suffice.

Thanks for any advice you can offer!

1 Like

Your assumptions are correct, except this line is backwards:

The channels client (ie phoenix.js browser clients) will timeout at 10 seconds by default if they don’t receive an acknowledgment from a client push. It’s possible the message eventually arrived or not, but the client will invoke your timeout callback and the message will not be re-pushed by the client. The server has no such timeout when pushing a message to the client. Your first bullet is correct in that a message will either arrive or the client will have disconnected. Likewise, message order per channel process is preserved during the connection lifecycle. So the only time you “miss” messages on the client is when the connection drops. You handle this on the app code side by tracking a last_seen_id or similar on the client/server. On join, the client passes a last_seen_id of the last thing it saw (such as a chat room message ID), which lets the server know how to catch the client up with side effects that have happened since that id (such as new messages in that chat room). On the client, anytime an event is received for, say, "new_message", the client bumps its last seen ID and all will recovery gracefully across discconect/reconnect. The code might look something like this:

let socket = new Socket(...)

let lastSeenMsg = {}
let roomParams = () => lastSeenMsg.id ? {last_seen_id: lastSeenMsg.id} : {}
let roomChannel = socket.channel("rooms:123", roomParams)

roomChannel.on("new_message", msg => {
  lastSeenMsg = msg
  renderNewMessage(msg)
})

roomChannel.join() .receive("ok", ({messages}) => {
  lastSeenMsg = messages[messages.length - 1]
  renderMessages(messages)
})
# room_channel.ex
def join("rooms:" <> id, params, socket) do
  ...
  messages = fetch_messages_since(params["last_seen_id"])
  {:ok, %{messages: messages}, socket}
end

Hope that helps!

14 Likes

Thank you, Chris! Just the clarification I was looking for (and hoping for).

3 Likes