aprs.me/lib/aprsme/cluster/connection_manager.ex
Graham McIntire a85f74ec3f
refactor: leadership_change handler pattern-matches instead of cond
Cluster.ConnectionManager.handle_info({:leadership_change, ...}) used
a cond on the (became_leader, node, connection_started) triple. Split
into handle_leadership_change/3 with three clauses:

- became_leader=true, us=true, not already started → start APRS-IS
- became_leader=false, us=true, already started → stop APRS-IS
- everything else → no-op

Makes the three state transitions visible in the function heads and
eliminates one more cond from the codebase.
2026-04-23 14:27:49 -05:00

107 lines
3.3 KiB
Elixir

defmodule Aprsme.Cluster.ConnectionManager do
@moduledoc """
Manages the APRS-IS connection based on cluster leadership.
Only starts the connection when this node is elected as leader.
"""
use GenServer
alias Aprsme.Cluster.LeaderElection
alias Aprsme.Is.IsSupervisor
require Logger
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
# Subscribe to leadership changes
:ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "cluster:leadership")
# Check initial leadership state
delay = Application.get_env(:aprsme, :connection_manager_init_delay, 1000)
_ = Process.send_after(self(), :check_initial_state, delay)
{:ok, %{connection_started: false}}
end
@impl true
def handle_info(:check_initial_state, state) do
if leader_check() do
Logger.info("This node is the leader, starting APRS-IS connection")
start_aprs_connection()
{:noreply, %{state | connection_started: true}}
else
Logger.info("This node is not the leader, APRS-IS connection will not be started")
{:noreply, state}
end
end
@impl true
def handle_info({:leadership_change, node, became_leader}, state) do
handle_leadership_change(became_leader, node == node(), state)
end
def handle_info(_msg, state), do: {:noreply, state}
# Became the cluster leader and we weren't already running — start APRS-IS.
defp handle_leadership_change(true, true, %{connection_started: false} = state) do
Logger.info("This node became the leader, starting APRS-IS connection")
start_aprs_connection()
{:noreply, %{state | connection_started: true}}
end
# Lost leadership while we were running — stop APRS-IS.
defp handle_leadership_change(false, true, %{connection_started: true} = state) do
Logger.info("This node lost leadership, stopping APRS-IS connection")
stop_aprs_connection()
{:noreply, %{state | connection_started: false}}
end
# Any other combination (different node, already in the right state) is a no-op.
defp handle_leadership_change(_became_leader, _us?, state), do: {:noreply, state}
@impl true
def terminate(_reason, _state) do
Phoenix.PubSub.unsubscribe(Aprsme.PubSub, "cluster:leadership")
:ok
end
defp leader_check do
LeaderElection.leader?()
catch
:exit, _ -> false
end
defp start_aprs_connection do
# Start the APRS-IS connection supervisor if not already started
case DynamicSupervisor.start_child(
Aprsme.DynamicSupervisor,
{IsSupervisor, []}
) do
{:ok, _pid} ->
Logger.info("APRS-IS connection started successfully")
{:error, {:already_started, _pid}} ->
Logger.info("APRS-IS connection was already running")
{:error, reason} ->
Logger.error("Failed to start APRS-IS connection: #{inspect(reason)}")
end
end
defp stop_aprs_connection do
# Find and terminate the APRS-IS connection supervisor
children = DynamicSupervisor.which_children(Aprsme.DynamicSupervisor)
Enum.each(children, fn
{_, pid, _, [IsSupervisor]} when is_pid(pid) ->
Logger.info("Stopping APRS-IS connection supervisor")
DynamicSupervisor.terminate_child(Aprsme.DynamicSupervisor, pid)
_ ->
:ok
end)
end
end