Current section
Files
Jump to
Current section
Files
lib/rabbit/connection/server.ex
defmodule Rabbit.Connection.Server do
@moduledoc false
use GenServer
import Rabbit.Utilities
require Logger
@opts_schema %{
module: [type: :module, required: false],
uri: [type: :binary, required: false],
username: [type: :binary, default: "guest", required: false],
password: [type: :binary, default: "guest", required: false],
virtual_host: [type: :binary, default: "/", required: false],
host: [type: :binary, default: "localhost", required: false],
port: [type: :integer, default: 5672, required: false],
channel_max: [type: :integer, default: 0, required: false],
frame_max: [type: :integer, default: 0, required: false],
heartbeat: [type: :integer, default: 10, required: false],
connection_timeout: [type: :integer, default: 50_000, required: false],
ssl_options: [type: [:binary, :atom], default: :none, required: false],
client_properties: [type: :list, default: [], required: false],
socket_options: [type: :list, default: [], required: false],
retry_backoff: [type: :integer, default: 1_000, required: true],
retry_max_delay: [type: :integer, default: 5_000, required: true]
}
@connect_opts [
:username,
:password,
:virtual_host,
:host,
:port,
:channel_max,
:frame_max,
:heartbeat,
:connection_timeout,
:ssl_options,
:client_properties,
:socket_options
]
################################
# Public API
################################
@doc false
def start_link(opts \\ [], server_opts \\ []) do
GenServer.start_link(__MODULE__, opts, server_opts)
end
################################
# GenServer Callbacks
################################
@doc false
@impl GenServer
def init(opts) do
with {:ok, opts} <- connection_init(opts) do
opts = KeywordValidator.validate!(opts, @opts_schema)
state = init_state(opts)
{:ok, state, {:continue, :connect}}
end
end
@doc false
@impl GenServer
def handle_continue(:connect, state) do
case connect(state) do
{:ok, state} -> {:noreply, state}
{:error, state} -> {:noreply, state, {:continue, :retry_delay}}
end
end
def handle_continue(:cleanup, %{connection: nil} = state) do
{:noreply, state, {:continue, :retry_delay}}
end
def handle_continue(:cleanup, state) do
{:noreply, state}
end
def handle_continue(:retry_delay, state) do
state = retry_delay(state)
{:noreply, state}
end
@doc false
@impl GenServer
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
state = cleanup(state, pid, reason)
{:noreply, state, {:continue, :cleanup}}
end
def handle_info(:reconnect, state) do
{:noreply, state, {:continue, :connect}}
end
@doc false
@impl GenServer
def handle_call(:state, _from, state) do
{:reply, state, state}
end
def handle_call(:fetch, _from, state) do
result = fetch(state)
{:reply, result, state}
end
def handle_call(:alive?, _from, state) do
result = alive?(state)
{:reply, result, state}
end
def handle_call({:subscribe, subscriber}, _from, state) do
state = subscribe(state, subscriber)
{:reply, :ok, state}
end
@impl GenServer
def terminate(_reason, state) do
disconnect(state)
end
################################
# Private API
################################
defp connection_init(opts) do
module = Keyword.get(opts, :module)
if callback_exported?(module, :init, 1) do
module.init(opts)
else
{:ok, opts}
end
end
defp init_state(opts) do
%{
name: process_name(self()),
connection_opts: Keyword.get(opts, :uri) || Keyword.take(opts, @connect_opts),
connection: nil,
monitors: %{},
subscribers: MapSet.new(),
retry_attempts: 0,
retry_backoff: Keyword.get(opts, :retry_backoff),
retry_max_delay: Keyword.get(opts, :retry_max_delay)
}
end
defp connect(%{connection: nil} = state) do
case AMQP.Connection.open(state.connection_opts) do
{:ok, connection} ->
state = %{state | connection: connection}
state = monitor(state, connection.pid, :connection)
publish_connected(state, connection)
{:ok, state}
error ->
log_error(state, error)
{:error, state}
end
end
defp connect(state) do
{:ok, state}
end
defp disconnect(%{connection: nil} = state) do
state
end
defp disconnect(%{connection: connection} = state) do
if Process.alive?(connection.pid), do: AMQP.Connection.close(connection)
publish_disconnected(state, :stopped)
%{state | connection: nil}
end
defp fetch(%{connection: nil}), do: {:error, :not_connected}
defp fetch(state), do: {:ok, state.connection}
defp alive?(%{connection: nil}), do: false
defp alive?(_state), do: true
defp monitor(state, pid, type) do
if Map.has_key?(state.monitors, pid) do
state
else
Process.monitor(pid)
%{state | monitors: Map.put(state.monitors, pid, type)}
end
end
defp cleanup(state, pid, reason) do
state =
case Map.get(state.monitors, pid) do
:connection ->
log_error(state, reason)
publish_disconnected(state, reason)
%{state | retry_attempts: 0, connection: nil}
:subscriber ->
%{state | subscribers: MapSet.delete(state.subscribers, pid)}
_ ->
state
end
%{state | monitors: Map.delete(state.monitors, pid)}
end
defp retry_delay(state) do
delay = calculate_reconnect_delay(state)
Process.send_after(self(), :reconnect, delay)
%{state | retry_attempts: state.retry_attempts + 1}
end
defp calculate_reconnect_delay(state) do
delay = state.retry_backoff * state.retry_attempts
if delay > state.retry_max_delay do
state.retry_max_delay
else
delay
end
end
defp subscribe(%{connection: nil} = state, subscriber) do
state = monitor(state, subscriber, :subscriber)
%{state | subscribers: MapSet.put(state.subscribers, subscriber)}
end
defp subscribe(state, subscriber) do
state = monitor(state, subscriber, :subscriber)
publish_connected([subscriber], state.connection)
%{state | subscribers: MapSet.put(state.subscribers, subscriber)}
end
defp publish_connected(state_or_subscribers, connection) do
publish(state_or_subscribers, {:connected, connection})
end
defp publish_disconnected(state_or_subscribers, reason) do
publish(state_or_subscribers, {:disconnected, reason})
end
defp publish(%{subscribers: subscribers}, message) do
publish(subscribers, message)
end
defp publish(subscribers, message) do
for pid <- subscribers do
if Process.alive?(pid), do: send(pid, message)
end
:ok
end
defp log_error(state, error) do
Logger.error("""
[Rabbit.Connection] #{inspect(state.name)}: connection error.
Detail: #{inspect(error)}
""")
end
end