问题
I have an Elixir/Phoenix server app and the clients connect through the build in channels system via websockets. Now I want to detect when an user leaves a channel.
Sidenote: I'm using the javascript client library inside a Google Chrome Extension. For this I extracted the ES6 code from Phoenix, transpiled it to javascript, and tweaked it a little so it runs standalone.
Now when I just close the popup, the server immediately triggers the terminate/2
function with reason = {:shutdown, :closed}
. There is no kind of close-callback involved on the extension side, so this is great!
But when the client simply looses network connection (I connected a second computer and just pulled out the network plug) then terminate/2
will not trigger.
Why and how do I fix this?
I played around with the timeout
option of transport :websocket, Phoenix.Transports.WebSocket
but this did not work out.
Update:
With the new awesome Phoenix 1.2 Presence
stuff, this should not be needed anymore.
回答1:
The proper way to do this is to not trap exits in your channel, and instead have another process monitor you. When you go down, it can invoke a callback. Below is a snippet to get you started:
# lib/my_app.ex
children = [
...
worker(ChannelWatcher, [:rooms])
]
# web/channels/room_channel.ex
def join("rooms:", <> id, params, socket) do
uid = socket.assigns.user_id]
:ok = ChannelWatcher.monitor(:rooms, self(), {__MODULE__, :leave, [id, uid]})
{:ok, socket}
end
def leave(room_id, user_id) do
# handle user leaving
end
# lib/my_app/channel_watcher.ex
defmodule ChannelWatcher do
use GenServer
## Client API
def monitor(server_name, pid, mfa) do
GenServer.call(server_name, {:monitor, pid, mfa})
end
def demonitor(server_name, pid) do
GenServer.call(server_name, {:demonitor, pid})
end
## Server API
def start_link(name) do
GenServer.start_link(__MODULE__, [], name: name)
end
def init(_) do
Process.flag(:trap_exit, true)
{:ok, %{channels: HashDict.new()}}
end
def handle_call({:monitor, pid, mfa}, _from, state) do
Process.link(pid)
{:reply, :ok, put_channel(state, pid, mfa)}
end
def handle_call({:demonitor, pid}, _from, state) do
case HashDict.fetch(state.channels, pid) do
:error -> {:reply, :ok, state}
{:ok, _mfa} ->
Process.unlink(pid)
{:reply, :ok, drop_channel(state, pid)}
end
end
def handle_info({:EXIT, pid, _reason}, state) do
case HashDict.fetch(state.channels, pid) do
:error -> {:noreply, state}
{:ok, {mod, func, args}} ->
Task.start_link(fn -> apply(mod, func, args) end)
{:noreply, drop_channel(state, pid)}
end
end
defp drop_channel(state, pid) do
%{state | channels: HashDict.delete(state.channels, pid)}
end
defp put_channel(state, pid, mfa) do
%{state | channels: HashDict.put(state.channels, pid, mfa)}
end
end
来源:https://stackoverflow.com/questions/33934029/how-to-detect-if-a-user-left-a-phoenix-channel-due-to-a-network-disconnect