Как определить, покинул ли пользователь канал Phoenix из-за отключения сети?

У меня есть серверное приложение Elixir / Phoenix, и клиенты подключаются через систему встроенных каналов через веб-сокеты. Теперь я хочу определить, когда пользователь покидает канал.

Примечание: я использую клиентскую библиотеку javascript внутри расширения Google Chrome. Для этого я извлек код ES6 из Phoenix, преобразовал его в javascript и немного изменил, чтобы он работал автономно.

Теперь, когда я просто закрываю всплывающее окно, сервер немедленно запускает функцию terminate/2 с помощью reason = {:shutdown, :closed}. На стороне расширения нет никакого обратного вызова, так что это здорово!

Но когда клиент просто теряет сетевое соединение (я подключил второй компьютер и просто вынул сетевой штекер), terminate/2 не сработает.

Почему и как это исправить?

Я поигрался с timeout опцией transport :websocket, Phoenix.Transports.WebSocket, но это не сработало.

Обновление: с новыми замечательными Presence возможностями Phoenix 1.2 в этом больше нет необходимости.


person Philip Claren    schedule 26.11.2015    source источник
comment
Я только что заметил, что сервер не всегда распознает, когда всплывающее окно было закрыто. Поэтому я надеюсь, что решение моего вопроса также решит это.   -  person Philip Claren    schedule 26.11.2015
comment
Обратите внимание, что функция «Присутствие» не будет работать, если последний пользователь, подписавшийся на канал, теряет соединение. См. stackoverflow.com/ questions / 53986369 / Возможно, это неправильное решение.   -  person xji    schedule 01.01.2019
comment
См. Также elixirforum.com/t/ Очевидно, даже с Presence вам все равно понадобится внешний процесс для мониторинга канала, поэтому ответ здесь по-прежнему очень актуален.   -  person xji    schedule 03.01.2019


Ответы (1)


Правильный способ сделать это - не перехватывать выходы в вашем канале, а вместо этого поручить другому процессу следить за вами. Когда вы спускаетесь, он может вызвать обратный вызов. Ниже приведен фрагмент, с которого можно начать:

# 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

В более новых версиях Elixir / Phoenix HashDict изменил имя на Map. Правильный пример для новых кодовых баз:

# 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: Map.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 Map.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 Map.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: Map.delete(state.channels, pid)}
  end

  defp put_channel(state, pid, mfa) do
    %{state | channels: Map.put(state.channels, pid, mfa)}
  end
end
person Chris McCord    schedule 26.11.2015
comment
Я заставил его работать, и у меня есть два дополнительных вопроса: 1. Почему этого нет в ядре (это слишком хорошо)? 2. Время между отключением сети и срабатыванием выхода составляет примерно 90 секунд. Это как-нибудь настраивается? (Я подумал о том, чтобы установить тайм-аут транспорта, скажем, 20 секунд и пинговать сервер каждые 10 секунд ... но, конечно, сжигаются дополнительные ресурсы) - person Philip Claren; 26.11.2015
comment
Это отличное решение. Единственная проблема заключается в том, что я выполняю некоторые действия с БД в функции leave, и эта проблема stackoverflow.com/questions/38335635/ происходит в тестах. - person xji; 06.02.2019
comment
Хорошо, что проблема наконец-то решена с помощью Elixir v1.8.0 и DBConnection v2.0.4 twitter.com/plataformatec/status / 1091300824251285504 - person xji; 10.02.2019