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.
107 lines
3.3 KiB
Elixir
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
|