Skip to content

Use the PubSub.Sender behavior for fastlaning - #6869

Merged
SteffenDE merged 3 commits into
v1.8from
sd-pubsub-sender-v1.8
Oct 7, 2026
Merged

SteffenDE merged 3 commits into
v1.8from
sd-pubsub-sender-v1.8

Conversation

@SteffenDE

@SteffenDE SteffenDE commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Adds PubSub :sender support to 1.8.

Channels now subscribe with a Phoenix.PubSub.Sender, new in phoenix_pubsub 2.4, instead of {:fastlane, ...} metadata. Fastlaning moves out of the Phoenix.Channel.Server.dispatch/3 dispatcher and into Phoenix.Channel.Server.send/4. PubSub's default dispatching calls send/4 once for each subscription.

Applications see no difference at runtime:

  • Broadcasts are still encoded once per serializer and sent straight to the transports.
  • Intercepted events still go through handle_out/3.
  • Every broadcast still names Phoenix.Channel.Server as the dispatcher.

This release prepares for 1.9, where channels stop passing a dispatcher. Without it, a rolling deploy from 1.8 to 1.9 crashes channels on the 1.8 nodes.

Why this is needed

Fastlaning depends on the dispatcher named when broadcasting

Today init_join/3 subscribes with metadata: {:fastlane, transport_pid, serializer, intercepts}. Only Phoenix.Channel.Server.dispatch/3 knows how to read that value, so every channel broadcast has to pass Phoenix.Channel.Server as the dispatcher.

Sometimes another dispatcher handles the broadcast, for example with a plain Phoenix.PubSub.broadcast/3 or a default dispatcher configured on the PubSub. That dispatcher ignores the subscription value and sends the raw %Phoenix.Socket.Broadcast{} to the channel process.

The channel process assumes it only receives broadcasts for intercepted events:

  • handle_info(%Broadcast{}) always calls handle_out/3.
  • There's no default handle_out/3.

So every event that isn't intercepted crashes the channel. It raises UndefinedFunctionError, or FunctionClauseError if the channel intercepts other events. The client rejoins, and the next broadcast crashes the channel again.

phoenix_pubsub 2.4 deprecates custom dispatchers in favour of per-subscription senders, and in 1.9 we want channel broadcasts to use the default dispatching.

The dispatcher runs on the receiving node

A cluster broadcast carries the dispatcher as a module name: {:forward_to_local, topic, message, dispatcher}. The receiving node runs its own dispatcher.dispatch/3 against its own registry entries.

Suppose 1.9 nodes broadcast with the default dispatcher while 1.8 nodes still have {:fastlane, ...} subscriptions. The 1.8 nodes then hit exactly the crash described above:

1.9 node                                        1.8.15 node
PubSub.broadcast(p, "room:1", %Broadcast{})
  {:forward_to_local, "room:1", msg,
   Phoenix.PubSub} --------------------------->  PG2Worker.handle_info/2
                                                   PubSub.local_broadcast(.., Phoenix.PubSub)
                                                   Phoenix.PubSub.dispatch/3
                                                     {:fastlane, ...} is not understood
                                                     send(channel_pid, %Broadcast{})
                                                 Channel.Server.handle_info(%Broadcast{})
                                                   MyChannel.handle_out("new_msg", ...)
                                                   ** (UndefinedFunctionError) / (FunctionClauseError)

No change on the 1.9 side can fix this, because the code that fails is 1.8's. The 1.8 nodes have to subscribe in a way the default dispatching understands before 1.9 is rolled out. That's what this PR does.

What changes

  • phoenix_pubsub ~> 2.4 becomes a requirement.
  • init_join/3 subscribes with sender: {Phoenix.Channel.Server, {transport_pid, serializer, intercepts}}.
  • Phoenix.Channel.Server.send/4 implements the Phoenix.PubSub.Sender behaviour with the same logic as the old dispatch/3:
    • intercepted events go to the channel process;
    • everything else is encoded once per serializer and sent to the transport.
  • Phoenix.Channel.Server.dispatch/3 stays but hands off to Phoenix.PubSub.dispatch/3:
    • It must still exist, because nodes on 1.8.15 and earlier name Phoenix.Channel.Server in their broadcasts.
    • Handing off is enough, because subscriptions now carry their own sender.
  • All broadcasts keep passing Phoenix.Channel.Server, both cluster-wide and local:
    • Cluster broadcasts must pass it, so that 1.8.15 receivers keep using their own fastlane dispatcher.
    • Local broadcasts and Presence would work without it. Keeping it means this patch release behaves exactly as before. It also keeps channel broadcasts out of a user-configured default PubSub dispatcher in a patch release.

Rolling deploy compatibility

From → to Dispatcher in message Path on the receiver Result
1.8.15 → this PR Phoenix.Channel.Server dispatch/3 → PubSub.dispatch/3 → send/4 fastlaned
this PR → 1.8.15 Phoenix.Channel.Server 1.8.15's own fastlane dispatch/3 fastlaned
this PR → 1.9 Phoenix.Channel.Server 1.9's dispatch/3 → PubSub.dispatch/3 → send/4 fastlaned
1.9 → this PR Phoenix.PubSub PubSub.dispatch/3 → send/4 fastlaned
1.9 → 1.8.15 Phoenix.PubSub raw %Broadcast{} → handle_out/3 channel crash

The last row is why the 1.9 changelog asks users to deploy 1.8.16 first. The upgrade path:

1.8.<=15  ->  1.8.16 (this PR)  ->  1.9 (stops passing the dispatcher, keeps dispatch/3)  ->  1.10 (removes dispatch/3)

Side effect

A %Phoenix.Socket.Broadcast{} published with Phoenix.PubSub.broadcast/3 and no dispatcher now reaches channels correctly, through fastlane or handle_out/3. Before, it crashed every channel on the topic that didn't intercept the event.

Notes for users / changelog

  • Update both packages: mix deps.update phoenix phoenix_pubsub. Updating only phoenix keeps the locked pubsub version, and Hex may stay on an earlier 1.8 release.
  • phoenix_pubsub 2.4 raises on subscribe(..., metadata: [atom | _]), because that shape now means a sender.
  • Hot code upgrades: channels that are running during an in-place upgrade keep their {:fastlane, ...} subscriptions, which the new code doesn't read. Rolling restarts aren't affected.
  • The {:fastlane, ...} subscription value was internal. Custom dispatchers that matched on it won't see it any more.

Channels now subscribe with a sender instead of fastlane metadata.
Since nodes running Phoenix < 1.8.16 still expect channel broadcasts
to use Phoenix.Channel.Server as dispatcher, we keep broadcasting with
it and delegate its dispatch/3 to the default dispatching.
@SteffenDE
SteffenDE merged commit 8c68844 into v1.8 Oct 7, 2026
16 checks passed
@SteffenDE
SteffenDE deleted the sd-pubsub-sender-v1.8 branch October 7, 2026 10:26
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