Current section

Files

Jump to
eventstore lib event_store notifications postgrex_notifications.ex
Raw

lib/event_store/notifications/postgrex_notifications.ex

defmodule EventStore.Notifications.PostgrexNotifications do
@moduledoc """
This is a copy of the new Postgrex.Notifications module that contains the timeout mechanism.
We copied here so we don't need to wait for the next stable postgrex release to have this feature
without using the postgrex master branch.
"""
use Connection
require Logger
alias Postgrex.Protocol
@timeout 5000
defstruct idle_timeout: 5000,
protocol: nil,
parameters: nil,
listeners: Map.new(),
listener_channels: Map.new()
## PUBLIC API ##
@type server :: GenServer.server()
@doc """
Start the notification connection process and connect to postgres.
The option that this function accepts are exactly the same accepted by
`Postgrex.start_link/1`. Note `:sync_connect` defaults to `true`.
"""
@spec start_link(Keyword.t()) :: {:ok, pid} | {:error, Postgrex.Error.t() | term}
def start_link(opts) do
{server_opts, opts} = Keyword.split(opts, [:name])
opts = Keyword.put_new(opts, :sync_connect, true)
connection_opts = Postgrex.Utils.default_opts(opts)
Connection.start_link(__MODULE__, connection_opts, server_opts)
end
@doc """
Listens to an asynchronous notification channel using the `LISTEN` command.
A message `{:notification, connection_pid, ref, channel, payload}` will be
sent to the calling process when a notification is received.
## Options
* `:timeout` - Call timeout (default: `#{@timeout}`)
"""
@spec listen(server, String.t(), Keyword.t()) :: {:ok, reference}
def listen(pid, channel, opts \\ []) do
message = {:listen, channel}
timeout = opts[:timeout] || @timeout
Connection.call(pid, message, timeout)
end
@doc """
Listens to an asynchronous notification channel `channel`. See `listen/2`.
"""
@spec listen!(server, String.t(), Keyword.t()) :: reference
def listen!(pid, channel, opts \\ []) do
{:ok, ref} = listen(pid, channel, opts)
ref
end
@doc """
Stops listening on the given channel by passing the reference returned from
`listen/2`.
## Options
* `:timeout` - Call timeout (default: `#{@timeout}`)
"""
@spec unlisten(server, reference, Keyword.t()) :: :ok
def unlisten(pid, ref, opts \\ []) do
message = {:unlisten, ref}
timeout = opts[:timeout] || @timeout
case Connection.call(pid, message, timeout) do
:ok -> :ok
{:error, %ArgumentError{} = err} -> raise err
end
end
@doc """
Stops listening on the given channel by passing the reference returned from
`listen/2`.
"""
@spec unlisten!(server, reference, Keyword.t()) :: :ok
def unlisten!(pid, ref, opts \\ []) do
unlisten(pid, ref, opts)
end
## CALLBACKS ##
def init(opts) do
if opts[:sync_connect] do
case connect(:init, opts) do
{:ok, _, _} = ok -> ok
{:stop, reason, _} -> {:stop, reason}
end
else
{:connect, :init, opts}
end
end
def connect(_, opts) do
case Protocol.connect([types: nil] ++ opts) do
{:ok, protocol} ->
idle_timeout = Keyword.get(opts, :idle_timeout, 5000)
{:ok, %__MODULE__{idle_timeout: idle_timeout, protocol: protocol}, idle_timeout}
{:error, reason} ->
{:stop, reason, opts}
end
end
def handle_call({:listen, channel}, {pid, _} = from, s) do
ref = Process.monitor(pid)
s = put_in(s.listeners[ref], {channel, pid})
s = update_in(s.listener_channels[channel], &((&1 || Map.new()) |> Map.put(ref, pid)))
# If this is the first listener for the given channel,
# we need to actually issue the LISTEN query.
if Map.size(s.listener_channels[channel]) == 1 do
listener_query("LISTEN \"#{channel}\"", {:ok, ref}, from, s)
else
{:reply, {:ok, ref}, s, s.idle_timeout}
end
end
def handle_call({:unlisten, ref}, from, s) do
case Map.fetch(s.listeners, ref) do
:error ->
{:reply, {:error, %ArgumentError{}}, s, s.idle_timeout}
{:ok, {channel, _pid}} ->
Process.demonitor(ref, [:flush])
s = remove_monitored_listener(s, ref, channel)
if Map.size(s.listener_channels[channel]) == 0 do
s = update_in(s.listener_channels, &Map.delete(&1, channel))
listener_query("UNLISTEN \"#{channel}\"", :ok, from, s)
else
{:reply, :ok, s, s.idle_timeout}
end
end
end
def handle_info({:DOWN, ref, :process, _, _}, s) do
case Map.fetch(s.listeners, ref) do
:error ->
{:noreply, s, s.idle_timeout}
{:ok, {channel, _pid}} ->
s = remove_monitored_listener(s, ref, channel)
if Map.size(s.listener_channels[channel]) == 0 do
s = update_in(s.listener_channels, &Map.delete(&1, channel))
listener_query("UNLISTEN \"#{channel}\"", :ok, nil, s)
else
{:noreply, s, s.idle_timeout}
end
end
end
def handle_info(:timeout, %{protocol: protocol} = state) do
case Protocol.ping(protocol) do
{:ok, protocol} ->
{:noreply, %{state | protocol: protocol}, state.idle_timeout}
{error, reason, protocol} when error in [:error, :disconnect] ->
{:stop, reason, %{state | protocol: protocol}}
end
end
def handle_info(msg, s) do
%{protocol: protocol, listener_channels: channels, listeners: listeners} = s
opts = [notify: &notify_listeners(channels, listeners, &1, &2)]
case Protocol.handle_info(msg, opts, protocol) do
{:ok, protocol} ->
{:noreply, %{s | protocol: protocol}, s.idle_timeout}
{error, reason, protocol} when error in [:error, :disconnect] ->
{:stop, reason, %{s | protocol: protocol}}
end
end
defp listener_query(statement, result, from, s) do
%{protocol: protocol, listener_channels: channels, listeners: listeners} = s
opts = [notify: &notify_listeners(channels, listeners, &1, &2)]
case Protocol.handle_listener(statement, opts, protocol) do
{:ok, %Postgrex.Result{}, protocol} ->
if from, do: Connection.reply(from, result)
checkin(protocol, s)
{error, reason, protocol} when error in [:error, :disconnect] ->
{:stop, reason, %{s | protocol: protocol}}
end
end
defp notify_listeners(channels, listeners, channel, payload) do
Enum.each(Map.get(channels, channel) || [], fn {ref, _pid} ->
{_, pid} = Map.fetch!(listeners, ref)
send(pid, {:notification, self(), ref, channel, payload})
end)
end
defp checkin(protocol, s) do
case Protocol.checkin(protocol) do
{:ok, protocol} ->
{:noreply, %{s | protocol: protocol}, s.idle_timeout}
{error, reason, protocol} when error in [:error, :disconnect] ->
{:stop, reason, %{s | protocol: protocol}}
end
end
defp remove_monitored_listener(s, ref, channel) do
s = update_in(s.listeners, &Map.delete(&1, ref))
update_in(s.listener_channels[channel], &Map.delete(&1, ref))
end
end