From 0f26cd7e22ea477f67a343788c9b77d2b781251b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 25 Jul 2025 13:10:23 -0500 Subject: [PATCH] Add cluster-aware APRS-IS status checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both /status and /status.json endpoints now show cluster-wide APRS-IS connection status instead of just local node status. Features: - get_cluster_aprs_status() checks all nodes in cluster via RPC - Returns status from any connected node (usually the leader) - Shows cluster info: total nodes, connected nodes, leader node - Graceful fallback to local status in non-clustered mode - Enhanced status page UI with cluster information display This ensures status pages accurately reflect the true APRS-IS connection state across the entire cluster. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- lib/aprsme/cluster/leader_election.ex | 76 +++++++++++++++++++ lib/aprsme_web/controllers/page_controller.ex | 4 +- lib/aprsme_web/live/status_live/index.ex | 45 ++++++++++- 3 files changed, 122 insertions(+), 3 deletions(-) diff --git a/lib/aprsme/cluster/leader_election.ex b/lib/aprsme/cluster/leader_election.ex index a1796fa..8c7a256 100644 --- a/lib/aprsme/cluster/leader_election.ex +++ b/lib/aprsme/cluster/leader_election.ex @@ -22,6 +22,21 @@ defmodule Aprsme.Cluster.LeaderElection do GenServer.call(__MODULE__, :current_leader) end + @doc """ + Gets APRS-IS status from across the entire cluster. + Returns the status from whichever node has an active connection. + """ + def get_cluster_aprs_status do + cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false) + + if cluster_enabled do + get_cluster_wide_status() + else + # Non-clustered mode - just return local status + Aprsme.Is.get_status() + end + end + @impl true def init(_opts) do Logger.info("Starting leader election process") @@ -179,6 +194,67 @@ defmodule Aprsme.Cluster.LeaderElection do end end + defp get_cluster_wide_status do + all_nodes = [node() | Node.list()] + + # Check each node for APRS-IS connection status + connected_statuses = + all_nodes + |> Enum.map(&get_node_status/1) + |> Enum.filter(fn status -> status.connected end) + + case connected_statuses do + [status | _] -> + # At least one node is connected - return its status + # Add cluster info to indicate this is cluster-wide status + Map.put(status, :cluster_info, %{ + total_nodes: length(all_nodes), + connected_nodes: length(connected_statuses), + leader_node: get_leader_node_name() + }) + + [] -> + # No nodes are connected - return local status but mark as cluster-wide + local_status = Aprsme.Is.get_status() + + Map.put(local_status, :cluster_info, %{ + total_nodes: length(all_nodes), + connected_nodes: 0, + leader_node: get_leader_node_name() + }) + end + end + + defp get_node_status(node_name) do + if node_name == node() do + # Local node - call directly + Aprsme.Is.get_status() + else + # Remote node - use RPC + case :rpc.call(node_name, Aprsme.Is, :get_status, [], 5000) do + {:badrpc, _reason} -> + # Node unreachable - return disconnected status + %{connected: false, server: "unreachable", port: 0} + + status when is_map(status) -> + status + + _ -> + %{connected: false, server: "error", port: 0} + end + end + rescue + _error -> + %{connected: false, server: "error", port: 0} + end + + defp get_leader_node_name do + case :global.whereis_name(@election_key) do + :undefined -> "none" + pid when is_pid(pid) -> pid |> node() |> to_string() + end + end + defp notify_leadership_change(became_leader) do Phoenix.PubSub.broadcast( Aprsme.PubSub, diff --git a/lib/aprsme_web/controllers/page_controller.ex b/lib/aprsme_web/controllers/page_controller.ex index 74f79e4..3376cdb 100644 --- a/lib/aprsme_web/controllers/page_controller.ex +++ b/lib/aprsme_web/controllers/page_controller.ex @@ -55,8 +55,8 @@ defmodule AprsmeWeb.PageController do end def status_json(conn, _params) do - # Get APRS-IS connection status - aprs_status = Aprsme.Is.get_status() + # Get cluster-wide APRS-IS connection status + aprs_status = Aprsme.Cluster.LeaderElection.get_cluster_aprs_status() # Get application version version = :aprsme |> Application.spec(:vsn) |> List.to_string() diff --git a/lib/aprsme_web/live/status_live/index.ex b/lib/aprsme_web/live/status_live/index.ex index 1921cd5..dc83f16 100644 --- a/lib/aprsme_web/live/status_live/index.ex +++ b/lib/aprsme_web/live/status_live/index.ex @@ -283,6 +283,49 @@ defmodule AprsmeWeb.StatusLive.Index do + + + <%= if Map.has_key?(@aprs_status, :cluster_info) do %> +
+

{gettext("Cluster Status")}

+
+
+
+
+ {gettext("Total Nodes:")} + {@aprs_status.cluster_info.total_nodes} +
+ +
+ {gettext("Connected Nodes:")} + {@aprs_status.cluster_info.connected_nodes} +
+ +
+ {gettext("Leader Node:")} + {@aprs_status.cluster_info.leader_node} +
+
+ +
+ +
+ {gettext("Cluster Mode:")} +
+
+ {gettext("Enabled")} +
+
+ +

+ {gettext( + "Status is aggregated from all nodes in the cluster. Only the leader node maintains the APRS-IS connection." + )} +

+
+
+
+ <% end %> @@ -292,7 +335,7 @@ defmodule AprsmeWeb.StatusLive.Index do # Private functions defp get_aprs_status do - Aprsme.Is.get_status() + Aprsme.Cluster.LeaderElection.get_cluster_aprs_status() rescue error -> require Logger