fix: guard pubsub APIs when not running

This commit is contained in:
Graham McIntire 2026-03-22 17:14:14 -05:00
parent 1e98a71431
commit 4d45c8351a
No known key found for this signature in database
4 changed files with 120 additions and 10 deletions

View file

@ -18,42 +18,64 @@ defmodule Aprsme.SpatialPubSub do
Register a client with their current viewport bounds.
"""
def register_viewport(client_id, bounds) do
GenServer.call(__MODULE__, {:register_viewport, client_id, bounds})
with_server({:error, :not_running}, fn ->
GenServer.call(__MODULE__, {:register_viewport, client_id, bounds})
end)
end
@doc """
Update a client's viewport bounds.
"""
def update_viewport(client_id, bounds) do
GenServer.call(__MODULE__, {:update_viewport, client_id, bounds})
with_server({:error, :not_running}, fn ->
GenServer.call(__MODULE__, {:update_viewport, client_id, bounds})
end)
end
@doc """
Unregister a client.
"""
def unregister_client(client_id) do
GenServer.cast(__MODULE__, {:unregister_client, client_id})
with_server(:ok, fn ->
GenServer.cast(__MODULE__, {:unregister_client, client_id})
end)
end
@doc """
Broadcast a packet to all clients whose viewports contain the packet's location.
"""
def broadcast_packet(packet) do
GenServer.cast(__MODULE__, {:broadcast_packet, packet})
with_server(:ok, fn ->
GenServer.cast(__MODULE__, {:broadcast_packet, packet})
end)
end
@doc """
Get statistics about spatial filtering.
"""
def get_stats do
GenServer.call(__MODULE__, :get_stats)
with_server(
%{
total_broadcasts: 0,
filtered_broadcasts: 0,
total_packets: 0,
clients_count: 0,
grid_cells: 0,
avg_clients_per_cell: 0.0
},
fn ->
GenServer.call(__MODULE__, :get_stats)
end
)
end
@doc """
Start telemetry reporting for LiveDashboard integration.
"""
def start_telemetry_reporting do
GenServer.cast(__MODULE__, :start_telemetry_reporting)
with_server(:ok, fn ->
GenServer.cast(__MODULE__, :start_telemetry_reporting)
end)
end
# Server callbacks
@ -439,4 +461,12 @@ defmodule Aprsme.SpatialPubSub do
%{}
)
end
defp with_server(default, fun) do
if Process.whereis(__MODULE__) do
fun.()
else
default
end
end
end

View file

@ -32,28 +32,36 @@ defmodule Aprsme.StreamingPacketsPubSub do
- :ok
"""
def subscribe_to_bounds(pid, bounds) do
GenServer.call(__MODULE__, {:subscribe, pid, bounds})
with_server({:error, :not_running}, fn ->
GenServer.call(__MODULE__, {:subscribe, pid, bounds})
end)
end
@doc """
Unsubscribe from packet notifications.
"""
def unsubscribe(pid) do
GenServer.call(__MODULE__, {:unsubscribe, pid})
with_server(:ok, fn ->
GenServer.call(__MODULE__, {:unsubscribe, pid})
end)
end
@doc """
Broadcast a packet to all subscribers whose bounds contain the packet's location.
"""
def broadcast_packet(packet) do
GenServer.cast(__MODULE__, {:broadcast, packet})
with_server(:ok, fn ->
GenServer.cast(__MODULE__, {:broadcast, packet})
end)
end
@doc """
List all active subscribers and their bounds.
"""
def list_subscribers do
GenServer.call(__MODULE__, :list_subscribers)
with_server([], fn ->
GenServer.call(__MODULE__, :list_subscribers)
end)
end
# Server Callbacks
@ -200,4 +208,12 @@ defmodule Aprsme.StreamingPacketsPubSub do
defp test_env? do
Application.get_env(:aprsme, :env) == :test
end
defp with_server(default, fun) do
if Process.whereis(__MODULE__) do
fun.()
else
default
end
end
end

View file

@ -97,4 +97,41 @@ defmodule Aprsme.SpatialPubSubTest do
SpatialPubSub.unregister_client(client_id)
end
end
describe "when the server is not running" do
setup do
pid = Process.whereis(SpatialPubSub)
if pid do
GenServer.stop(pid)
end
on_exit(fn ->
if !Process.whereis(SpatialPubSub) do
start_supervised!({SpatialPubSub, []})
end
end)
:ok
end
test "public APIs return safe defaults instead of crashing" do
bounds = %{north: 41.0, south: 40.0, east: -73.0, west: -74.0}
assert {:error, :not_running} = SpatialPubSub.register_viewport("missing", bounds)
assert {:error, :not_running} = SpatialPubSub.update_viewport("missing", bounds)
assert :ok = SpatialPubSub.unregister_client("missing")
assert :ok = SpatialPubSub.broadcast_packet(%{lat: 40.5, lon: -73.5})
assert :ok = SpatialPubSub.start_telemetry_reporting()
assert SpatialPubSub.get_stats() == %{
total_broadcasts: 0,
filtered_broadcasts: 0,
total_packets: 0,
clients_count: 0,
grid_cells: 0,
avg_clients_per_cell: 0.0
}
end
end
end

View file

@ -140,6 +140,33 @@ defmodule Aprsme.StreamingPacketsPubSubTest do
end
end
describe "when the server is not running" do
setup do
pid = Process.whereis(StreamingPacketsPubSub)
if pid do
GenServer.stop(pid)
end
on_exit(fn ->
if !Process.whereis(StreamingPacketsPubSub) do
start_supervised!({StreamingPacketsPubSub, []})
end
end)
:ok
end
test "returns safe defaults instead of crashing" do
bounds = %{north: 40.0, south: 30.0, east: -70.0, west: -80.0}
assert {:error, :not_running} = StreamingPacketsPubSub.subscribe_to_bounds(self(), bounds)
assert :ok = StreamingPacketsPubSub.unsubscribe(self())
assert :ok = StreamingPacketsPubSub.broadcast_packet(%{latitude: 35.0, longitude: -75.0})
assert [] == StreamingPacketsPubSub.list_subscribers()
end
end
describe "performance" do
test "handles high volume of packets efficiently" do
bounds = %{north: 90.0, south: -90.0, east: 180.0, west: -180.0}