defmodule Electric.Client.Fetch.Monitor do @moduledoc false # Companion process that registers processes listening for the result of a # given client request. # # Separates the list of subscribers from the actual request process so that # if the request process crashes the list of subscribers is retained and also # so that registering subscribers can happen while the request process is # blocked performing it's actual HTTP request. use GenServer require Logger def name(request_id) do {:via, Registry, {Electric.Client.Registry, {__MODULE__, request_id}}} end def child_spec(request_id) do %{ id: {__MODULE__, request_id}, start: {__MODULE__, :start_link, [request_id]}, restart: :transient, type: :worker } end def start_link(request_id) do GenServer.start_link(__MODULE__, request_id, name: name(request_id)) end def register(monitor_pid, listener_pid) do GenServer.call(monitor_pid, {:register, listener_pid}) end def wait(ref) do receive do {:response, ^ref, response} -> response end end def reply(pid, response) when is_pid(pid) do GenServer.call(pid, {:reply, response}) end @impl true def init(request_id) do state = %{ request_id: request_id, subscribers: [] } {:ok, state} end @impl true def handle_call({:register, listener_pid}, _from, state) do ref = Process.monitor(listener_pid) Logger.debug(fn -> [message: "Registering listener pid #{inspect(listener_pid)}", request_id: state.request_id] end) state = Map.update!(state, :subscribers, &[{listener_pid, ref} | &1]) {:reply, ref, state} end def handle_call({:reply, response}, _from, state) do case response do %{status: status} -> Logger.debug( fn -> "Returning response #{status}" end, request_id: state.request_id ) {:error, %{status: _} = response} -> Logger.warning( fn -> "Request failed: #{inspect(response)}" end, request_id: state.request_id ) {:error, reason} -> Logger.error( fn -> "Request failed: #{inspect(reason)}" end, request_id: state.request_id ) end for {pid, ref} <- state.subscribers do send(pid, {:response, ref, response}) end {:stop, :normal, :ok, state} end @impl true def handle_info({:DOWN, ref, :process, pid, reason}, state) do Logger.debug(fn -> [ message: "#{inspect(pid)} exited with reason #{inspect(reason)}. Removing from subscribers", request_id: state.request_id ] end) state = Map.update!(state, :subscribers, fn subscribers -> Enum.reject(subscribers, &(&1 == {pid, ref})) end) {:noreply, state} end end