aprs.me/lib/aprsme/packet_producer.ex
Graham McIntire 871620223d
Performance improvements, code cleanup, remove sobelow
- Switch PacketProducer buffer from list to :queue for O(1) overflow handling
- Add cached leadership check via :persistent_term to eliminate GenServer.call
  bottleneck in packet distribution hot path
- Simplify packets_live coordinate extraction from 70 lines of nested
  conditionals to helper functions (extract_coordinate/2, format_coordinate/1)
- Remove debug logging from hot paths (packets.ex queries, PacketDistributor,
  app.js console.logs)
- Remove sobelow dependency (false positives blocking commits)
- Add 25 new tests for PacketProducer, coordinate helpers, and cached leadership
2026-02-19 14:51:01 -06:00

80 lines
2.4 KiB
Elixir

defmodule Aprsme.PacketProducer do
@moduledoc """
GenStage producer that handles incoming APRS packets and sends them to consumers
for efficient batch processing.
Uses `:queue` for O(1) enqueue/dequeue instead of lists, which avoids
O(n) `Enum.take` on every buffer overflow during traffic bursts.
"""
use GenStage
require Logger
def start_link(opts \\ []) do
GenStage.start_link(__MODULE__, opts, name: __MODULE__)
end
def submit_packet(packet_data) do
GenStage.cast(__MODULE__, {:packet, packet_data})
end
@impl true
def init(opts) do
{:producer, %{demand: 0, buffer: :queue.new(), buffer_size: 0, max_buffer_size: opts[:max_buffer_size] || 1000}}
end
@impl true
def handle_demand(incoming_demand, %{demand: demand} = state) do
{events, new_buffer, remaining_demand} = dispatch_events(state.buffer, demand + incoming_demand)
{:noreply, events,
%{state | demand: remaining_demand, buffer: new_buffer, buffer_size: state.buffer_size - length(events)}}
end
@impl true
def handle_cast(
{:packet, packet_data},
%{demand: demand, buffer: buffer, buffer_size: size, max_buffer_size: max_size} = state
) do
if demand > 0 do
{:noreply, [packet_data], %{state | demand: demand - 1}}
else
new_size = size + 1
if new_size > max_size do
dropped = new_size - max_size
Logger.warning("Packet buffer full, dropping #{dropped} oldest packet(s)",
buffer_size: max_size,
dropped: dropped
)
:telemetry.execute(
[:aprsme, :packet_producer, :buffer_overflow],
%{dropped: dropped, buffer_size: max_size},
%{}
)
# Drop oldest (front of queue), add new to back — O(1) amortized
{_, trimmed} = :queue.out(buffer)
{:noreply, [], %{state | buffer: :queue.in(packet_data, trimmed), buffer_size: max_size}}
else
{:noreply, [], %{state | buffer: :queue.in(packet_data, buffer), buffer_size: new_size}}
end
end
end
defp dispatch_events(buffer, demand) when demand > 0 do
if :queue.is_empty(buffer) do
{[], buffer, demand}
else
{front, remaining} = :queue.split(min(demand, :queue.len(buffer)), buffer)
events = :queue.to_list(front)
{events, remaining, demand - length(events)}
end
end
defp dispatch_events(buffer, demand) do
{[], buffer, demand}
end
end