Current section

Files

Jump to
oban lib oban sonar.ex
Raw

lib/oban/sonar.ex

defmodule Oban.Sonar do
@moduledoc false
use GenServer
alias Oban.Notifier
alias __MODULE__, as: State
defstruct [
:conf,
:timer,
interval: :timer.seconds(15),
nodes: %{},
stale_mult: 2,
status: :unknown
]
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
{name, opts} = Keyword.pop(opts, :name)
conf = Keyword.fetch!(opts, :conf)
if conf.testing != :disabled do
:ignore
else
GenServer.start_link(__MODULE__, struct!(State, opts), name: name)
end
end
@impl GenServer
def init(state) do
Process.flag(:trap_exit, true)
{:ok, state, {:continue, :start}}
end
@impl GenServer
def terminate(_reason, state) do
if is_reference(state.timer), do: Process.cancel_timer(state.timer)
:ok
end
@impl GenServer
def handle_continue(:start, state) do
:ok = Notifier.listen(state.conf.name, :sonar)
:ok = Notifier.notify(state.conf, :sonar, %{node: state.conf.node, ping: :ping})
{:noreply, schedule_ping(state)}
end
@impl GenServer
def handle_call(:get_status, _from, state) do
{:reply, state.status, state}
end
def handle_call(:prune_nodes, _from, state) do
state = prune_stale_nodes(state)
{:reply, state.nodes, state}
end
@impl GenServer
def handle_info(:ping, state) do
:ok = Notifier.notify(state.conf, :sonar, %{node: state.conf.node, ping: true})
state =
state
|> prune_stale_nodes()
|> update_status()
|> schedule_ping()
{:noreply, state}
end
def handle_info({:notification, :sonar, %{"node" => node} = payload}, state) do
time = Map.get(payload, "time", System.system_time(:millisecond))
state =
state
|> Map.update!(:nodes, &Map.put(&1, node, time))
|> update_status()
{:noreply, state}
end
# Helpers
defp schedule_ping(state) do
timer = Process.send_after(self(), :ping, state.interval)
%{state | timer: timer}
end
defp update_status(state) do
node = state.conf.node
status =
case Map.keys(state.nodes) do
[] -> :isolated
[^node] -> :solitary
[_ | _] -> :clustered
end
if status != state.status do
:telemetry.execute([:oban, :notifier, :switch], %{}, %{conf: state.conf, status: status})
end
%{state | status: status}
end
defp prune_stale_nodes(state) do
stale = System.system_time(:millisecond) - state.interval * state.stale_mult
nodes = Map.reject(state.nodes, fn {_, recorded} -> recorded < stale end)
%{state | nodes: nodes}
end
end