I need to perform some actions when the user leaves a channel (in most cases where they close the tab voluntarily, but there may also be a connection loss/timeout etc.)
According to posts like https://elixirforum.com/t/phoenix-presence-run-some-code-when-user-leaves-the-channel/17739 and How to detect if a user left a Phoenix channel due to a network disconnect?, intercepting the "presence_diff" event from Presence seems to be a foolproof way to go, as it should also covers the cases where the connection terminates abnormally.
Strangely, the presence_diff event seems to only be triggered when I track the user via Presence.track, but not when the user leaves.
Meanwhile, adding a terminate(reason, socket) callback in my channel correctly catches the leave event.
I wonder what could be wrong in my configuration. Or did I not understand the use of Presence correctly?
Example code:
def join("participant:" <> participant_id, _payload, socket) do
if socket.assigns.participant_id == participant_id do
send(self(), :after_participant_join)
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
def handle_info(:after_participant_join, socket) do
experiment_id = socket.assigns.experiment_id
Presence.track(socket, experiment_id, %{
# keys to track
})
# Broadcast something
# broadcast(socket, ...)
{:noreply, socket}
end
intercept(["presence_diff"])
def handle_out("presence_diff", payload, socket) do
# Only gets triggered at Presence.track, but not when the connection is closed.
IO.puts("presence_diff triggered, payload is #{inspect(payload)}")
leaves = payload.leaves
for {experiment_id, meta} <- leaves do
IO.puts("Leave information: #{meta}")
# Do stuffs
end
end
# This works, however.
def terminate(reason, socket) do
IO.puts("terminated. #{inspect(reason)}")
# Do stuffs.
end
OK I think I know what happened: Each "participant:" <> participant_id topic is, as its name suggests, only subscribed to by one participant. Therefore, when that participant quits, the process also dies and nobody is able to act on the presence_diff message.
A separate process is still needed. One can call MyApp.Endpoint.subscribe from that process to subscribe to the "participant:" <> participant_id topic and act on the presence_diff messages.
Or one can set up an external monitor. See How to detect if a user left a Phoenix channel due to a network disconnect?
Related
My use case is to have multiple users connected to a lobby/waiting room, but only two of them will be picked from the lobby to start a conversation, after which they should be removed from the lobby. How can I implement that? Note that in this system the users aren't registered and don't have usernames. They should directly come in from the webpage.
Apparently the track and untrack functions also have variants that accept pid as an argument. However I'm not sure how I can retrieve the pids of the processes in the first place, when the conversation needs to be started.
Is the function self() the correct one to use in this case? i.e. maybe I can write
def handle_info(:after_lobby_join, socket) do
Presence.track(socket, "lobby", %{
pid: self()
})
{:noreply, socket}
end
def handle_info(:start, socket) do
pid1 = hd(Presence.list(socket)["lobby"][:metas])[:pid]
# Start the conversation by sending messages individually to pid1 and pid2
...
untrack(pid1, "my_app:lobby", "lobby")
{:ok, socket}
end
Or am I overcomplicating the issue/not understanding Presence correctly?
There's also a phx_ref field but I don't seem to be able to use it for this purpose.
Also, apparently I would only want to send "start_conversation" messages to the two users being picked but not the others in the lobby. I see that the function push sends messages to a designated socket. But if I am keeping track of the pid, can the corresponding sockets be identified from the pid?
I solved my initial question using individual user channels eventually. I just let the frontend generate random user IDs.
A solution without using user channels is proposed in Elixir forum. It doesn't use Presence though.
def handle_info({:new_user, socket}, [{us1, ref1}, {us2, ref2}]) do
# we had 2 users, a new one joined, so that's 3
user_sockets = [socket, us1, us2]
# we can create a room for them now
create_new_room(user_sockets)
# and clean the state
Enum.each([ref1, ref2], fn ref ->
Process.demonitor(ref)
end)
{:noreply, []}
end
def handle_info({:new_user, socket}, waiting_user_sockets) do
# otherwise just add the new users to the waiting users list
ref = Process.monitor(socket)
{:noreply, [{socket, ref} | waiting_user_sockets]}
end
def handle_info({:DOWN, ref, :process, _object, _reason}, waiting_user_sockets) do
# remove the disconnected socket
end
I’ve just started using Ruby and am writing a piece to consume some messages from a RabbitMQ queue. I’m using Bunny to do so.
So I’ve created my queues and binded them to an exchange.
However I’m now unsure how I handle subscribing to them both and allowing the ruby app to continue running (want the messages to keep coming through i.e. not blocked or at least not for a long time) until I actually exit it with ctrl+c.
I’ve tried using :block => true however as I have 2 different queues I’m subscribing to, using this means it remains consuming from only one.
So this is how I’m consuming messages:
def consumer
begin
puts ' [*] Waiting for messages. To exit press CTRL+C'
#oneQueue.subscribe(:manual_ack => true) do |delivery_info, properties, payload|
puts('Got One Queue')
puts "Received #{payload}, message properties are #{properties.inspect}"
end
#twoQueue.subscribe(:manual_ack => true) do |delivery_info, properties, payload|
puts('Got Two Queue')
puts "Received #{payload}, message properties are #{properties.inspect}"
end
rescue Interrupt => _
#TODO - close connections here
exit(0)
end
end
Any help would be appreciated.
Thanks!
You can't use block: true when you have two subscriptions as only the first one will block; it'll never get to the second subscription.
One thing you can do is set up both subscriptions without blocking (which will automatically spawn two threads to process messages), and then block your main thread with a wait loop (add just before your rescue):
loop { sleep 5 }
I'm following along the book from PragProg "Programming Phoenix" and I'm currently on the chapters about Phoenix's channels.
At some point there's an example about setting up a simple channel with one topic and handling in/out messages between the client and server. No fancy stuff, everything worked as advertised.
Then I started investigating Phoenix.Channel's API and found "broadcast_from" function.
Doing a bit of research it was clear to me that using "broadcast_from" (from the channel) would have sent the message to all connected clients but the one whose message I was currently handling.
My current code is
defmodule Rumbl.VideoChannel do
use Rumbl.Web, :channel
def join("videos:" <> video_id, _params, socket) do
:timer.send_interval(5000, :ping)
{:ok, %{status: "successful join"}, assign(socket, :video_id, String.to_integer(video_id))}
end
def handle_info(:ping, socket) do
count = socket.assigns[:count] || 1
#push socket, "ping", %{count: count}
broadcast_from! socket, "test", %{id: 1, status: :critical}
{:noreply, assign(socket, :count, count + 1)}
end
end
I expected that, upon client's connection, the client would not receive
the "test" messages. And that was, indeed, the outcome. Until I opened another browser window and connected to the channel. At that point
both windows started receiving the "test" messages. It also happened if the second window was opened from another device (such as an iPhone).
Is that the normal behaviour or is it me misusing/misunderstanding the documentation?
Thanks in advance for your support.
I think, it's the normal behaviour. From the broadcast_from docu:
The channel that owns the socket will not receive the published message.
It's a bit confusing. As I understand the docu, when you open another window, join creates another socket and therefore another pid from which you receive the test message in your first window. With one window you will see no test message. With two windows you will see the test messages each from the other pid of the two sockets.
I have read through the zguide but haven't found the kind of pattern I'm looking for:
There is one central server (with known endpoint) and many clients (which may come and go).
Clients keep sending hearbeats to the server, but they don't want the server to reply.
Server receives heartbeats, but it does not reply to clients.
Hearbeats sent when clients and server are disconnected should somehow be dropped to prevent a heartbeat flood when they go back online.
The closet I can think of is the DEALER-ROUTER pattern, but since this is meant to be used as an async REQ-REP pattern (no?), I'm not sure what would happen if the server just keep silent on incoming "requests." Also, the DEALER socket would block rather then start dropping heartbeats when the send High Water Mark is reached, which would still result in a heartbeat flood.
The PUSH/PULL pattern should give you what you need.
# Client example
import zmq
class Client(object):
def __init__(self, client_id):
self.client_id = client_id
ctx = zmq.Context.instance()
self.socket = ctx.socket(zmq.PUSH)
self.socket.connect("tcp://localhost:12345")
def send_heartbeat(self):
self.socket.send(str(self.client_id))
# Server example
import zmq
class Server(object):
def __init__(self):
ctx = zmq.Context.instance()
self.socket = ctx.socket(zmq.PULL)
self.socket.bind("tcp://*:12345") # close quote
def receive_heartbeat(self):
return self.socket.recv() # returns the client_id of the message's sender
This PUSH/PULL pattern works with multiple clients as you wish. The server should keep an administration of the received messages (i.e. a dictionary like {client_id : last_received} which is updated with datetime.utcnow() on each received message. And implement some housekeeping function to periodically check the administration for clients with old timestamps.
I have a small application, serving connections(like a chat). It catches the connection, grabs login from it, then listens to the data and broadcasts it to each connection, except sender.
The problem is i'm not a very advanced tester and do not know, how this can be tested.
# Handle each connection
def serve(io)
io.puts("LOGIN\n")
# Listen for identifier
user = io.gets.chomp
...
# Add connection to the list
#mutex.synchronize { #chatters[user] = io }
# Get and broadcast input until connection returns nil
loop do
incoming = io.gets
broadcast(incoming, io)
end
end
#Send message out to everyone, but sender
def broadcast(message="", sender)
# Mutex for safety - GServer uses threads
#mutex.synchronize do
#chatters.each do |chatter|
socket = chatter[1]
# Do not send to sender
if sock != sender
sock.print(message)
end
end
end
end
If you just want to do unit testing, you could use RSpec mocks (or some other mocking framework) to stub your methods and ensure that the logic works the way you expect. If you actually want to drive an integration test, that's a lot more work, and will require that you create a separate reader and writer for the socket so that you can actually test each piece of the conversation independently for expected behavior.
Someone else has apparently blogged about a similar issue to yours. Perhaps that example will help you.
If your question is more about the test cases you should write, instead of about how to test sockets, then you may want to rewrite your question so that answers will be more on-target.