defmodule Gnat.ConnectionSupervisor do
use GenServer
require Logger
@moduledoc """
A process that can supervise a named connection for you
If you would like to supervise a Gnat connection and have it automatically re-connect in case of failure you can use this module in your supervision tree.
It takes a map with the following data:
```
gnat_supervisor_settings = %{
name: :gnat, # (required) the registered named you want to give the Gnat connection
backoff_period: 4_000, # number of milliseconds to wait between consecutive reconnect attempts (default: 2_000)
connection_settings: [
%{host: '10.0.0.100', port: 4222},
%{host: '10.0.0.101', port: 4222},
]
}
```
The connection settings can specify all of the same values that you pass to `Gnat.start_link/1`. Each time a connection is attempted we will use one of the provided connection settings to open the connection. This is a simplistic way of load balancing your connections across a cluster of nats nodes and allowing failover to other nodes in the cluster if one goes down.
To use this in your supervision tree add an entry like this:
```
import Supervisor.Spec
worker(Gnat.ConnectionSupervisor, [gnat_supervisor_settings, [name: :my_connection_supervisor]])
```
The second argument is used as GenServer options so you can give the supervisor a registered name as well if you like. Now in the rest of your code you can call things like:
```
:ok = Gnat.pub(:gnat, "subject", "message")
```
And it will use your supervised connection. If the connection is down when you call that function (or dies during that function) it will raise an error.
"""
@spec start_link(map(), keyword()) :: GenServer.on_start
def start_link(settings, options \\ []) do
GenServer.start_link(__MODULE__, settings, options)
end
@impl GenServer
def init(options) do
state = %{
backoff_period: Map.get(options, :backoff_period, 2000),
connection_settings: Map.fetch!(options, :connection_settings),
name: Map.fetch!(options, :name),
gnat: nil,
}
Process.flag(:trap_exit, true)
send self(), :attempt_connection
{:ok, state}
end
@impl GenServer
def handle_info(:attempt_connection, state) do
connection_config = random_connection_config(state)
Logger.debug "connecting to #{inspect connection_config}"
case Gnat.start_link(connection_config, name: state.name) do
{:ok, gnat} -> {:noreply, %{state | gnat: gnat}}
{:error, err} ->
Logger.error "failed to connect #{inspect err}"
{:noreply, %{state | gnat: nil}} # we will get an :EXIT message and handle it there
end
end
def handle_info({:EXIT, _pid, reason}, %{gnat: nil}=state) do
Logger.error "failed to connect #{inspect reason}"
Process.send_after(self(), :attempt_connection, state.backoff_period)
{:noreply, state}
end
def handle_info({:EXIT, _pid, reason}, state) do
Logger.error "connection failed #{inspect reason}"
send self(), :attempt_connection
{:noreply, state}
end
def handle_info(msg, state) do
Logger.error "#{__MODULE__} received unexpected message #{inspect msg}"
{:noreply, state}
end
defp random_connection_config(%{connection_settings: connection_settings}) do
connection_settings |> Enum.random()
end
end