since 3 days I am trying to resolve an issue that I can’t wrap my head around.
I am not experienced with Elixir and really want to learn the language. Currently I am trying to build a small chat app for learning purposes.
To learn the language in more detail I decided to not use Phoenix. So I have set up my app with Bandit & Plug.
I want to set up server sent events to notify chatrooms about new messages. I thought it would be a great idea to bundle all connections to a chatroom in a dedicated process for the chatroom.
I tried building a GenServer that does that, but I can’t send chunked data from there, as my GenServer is not the owner of the connection. I’ll get an error with something like data cant be sent from a process that is not the stream owner.
I had it working once, when I set up a receive do loop directly in my router, but that does not feel right, as I can’t really monitor or coordinate connections like that.
Lastly I have tried just updating the “owner” field in the connection, which also did not work.
Looking into websocket implementations also did not help with this and I could not find any digestible information regarding server sent events. I have seen someone in the forum pointed to the send_chunked docs of Plug, but I can’t find how that’s supposed to help me with connection ownership.
So bandit does not allow for transferring ownership of the Plug.Conn and I am out of ideas on how to broadcast messages…
I’d suggest using Registry or Phoenix.PubSub to broadcast messages between the multiple connection holding processes. The connection holding processes have vastly different lifecycles, so trying to consolidate that would cause more harm/complexity than anything else.
This would also mimic the architecture of phoenix channels, which uses n + 1 processes per websocket connection. One for the websocket connection handling and 1 process per channel the user joined. Each of those channel processes then communicates with Phoenix.PubSub with other connections having joined the same channel name. There’s never a central process for a channel.
You can run receive do loop in your controller and stream to the client. There are 2 concerns:
communicate with the user
monitor and coordinate connections
1 need to be in the per connection process and 2 should be in a central GenServer. Now, how does your per connection process find your GenServer? This is where Registry come into play. And how does your central Genserver know what are the per connection processes to forward the messages? This is where PubSub come into play. Elixir give you a lot of tools for this kind of inter-process communcation.
Yea I have tried implementing a Registry for this yesterday wich yielded the same result of issues with the connection stream owner.
I don’t have controllers and I don’t really know how to build one? Wouldn’t that also just result in trying to transfer ownership of the connection to my controller?
I have tried building a Registry implementation yesterday which unfortunately also did not work. There I tried to put all the connections in one “bucket” as it’s done in the OTP guide from the Elixir documentation.
I had people recommending Phoenix.PubSub too, but I don’t want to use any packages as that would just result in not learning anything…
You can also ignore the name “controller” – any of your plugs running the connection (where you’d call send_chunked / chunk) would do.
I’d suggest decoupling the idea of “one chatroom” from “one process”. You’d want the many processes holding those http connections to subscribe on the (duplicate type) registry with a key of the chatroom name. Then broadcasting can pull all the registered pids for that key from the registry and send a message to them: Registry — Elixir v1.18.2
The http connection holding processes can then loop using receive forwarding messages they receive from other processes and sent them to their clients.
So what I am getting from this is that I should include receive do in my route handler and listen for messages. This was the implementation that originally worked for me but it did not feel like it was the correct way. I’ll try that again. Thanks!