When the PacketProducer buffer crosses 80% capacity, signal Aprsme.Is to switch its TCP socket to passive mode, leveraging TCP flow control to slow APRS-IS intake. Resume active mode when buffer drains below 30%. A 30-second safety valve timer auto-resumes if backpressure gets stuck. Disconnect/reconnect/terminate paths all clear backpressure state.
181 lines
4.9 KiB
Elixir
181 lines
4.9 KiB
Elixir
defmodule Aprsme.PacketProducerTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Aprsme.PacketProducer
|
|
|
|
describe "init/1" do
|
|
test "initializes with default max_buffer_size" do
|
|
{:producer, state} = PacketProducer.init([])
|
|
assert state.demand == 0
|
|
assert state.buffer_size == 0
|
|
assert state.max_buffer_size == 1000
|
|
end
|
|
|
|
test "initializes with custom max_buffer_size" do
|
|
{:producer, state} = PacketProducer.init(max_buffer_size: 500)
|
|
assert state.max_buffer_size == 500
|
|
end
|
|
|
|
test "initializes with empty :queue buffer" do
|
|
{:producer, state} = PacketProducer.init([])
|
|
assert :queue.is_queue(state.buffer)
|
|
assert :queue.is_empty(state.buffer)
|
|
end
|
|
end
|
|
|
|
describe "handle_cast {:packet, _} with demand > 0" do
|
|
test "dispatches packet immediately when demand exists" do
|
|
state = %{
|
|
demand: 3,
|
|
buffer: :queue.new(),
|
|
buffer_size: 0,
|
|
max_buffer_size: 1000,
|
|
high_water_mark: 800,
|
|
low_water_mark: 300,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, events, new_state} = PacketProducer.handle_cast({:packet, %{sender: "TEST"}}, state)
|
|
|
|
assert events == [%{sender: "TEST"}]
|
|
assert new_state.demand == 2
|
|
end
|
|
end
|
|
|
|
describe "handle_cast {:packet, _} with demand == 0" do
|
|
test "buffers packet when no demand" do
|
|
state = %{
|
|
demand: 0,
|
|
buffer: :queue.new(),
|
|
buffer_size: 0,
|
|
max_buffer_size: 1000,
|
|
high_water_mark: 800,
|
|
low_water_mark: 300,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, [], new_state} = PacketProducer.handle_cast({:packet, %{sender: "TEST"}}, state)
|
|
|
|
assert new_state.buffer_size == 1
|
|
assert :queue.len(new_state.buffer) == 1
|
|
end
|
|
|
|
test "drops oldest packet when buffer overflows" do
|
|
# Fill buffer to max
|
|
buffer =
|
|
Enum.reduce(1..3, :queue.new(), fn i, q ->
|
|
:queue.in(%{sender: "PACKET#{i}"}, q)
|
|
end)
|
|
|
|
state = %{
|
|
demand: 0,
|
|
buffer: buffer,
|
|
buffer_size: 3,
|
|
max_buffer_size: 3,
|
|
high_water_mark: 2,
|
|
low_water_mark: 1,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, [], new_state} =
|
|
PacketProducer.handle_cast({:packet, %{sender: "NEW"}}, state)
|
|
|
|
# Buffer size should still be max
|
|
assert new_state.buffer_size == 3
|
|
|
|
# Oldest packet (PACKET1) should be dropped, NEW should be present
|
|
items = :queue.to_list(new_state.buffer)
|
|
senders = Enum.map(items, & &1.sender)
|
|
refute "PACKET1" in senders
|
|
assert "NEW" in senders
|
|
assert "PACKET2" in senders
|
|
assert "PACKET3" in senders
|
|
end
|
|
end
|
|
|
|
describe "handle_demand/2" do
|
|
test "dispatches buffered packets in FIFO order" do
|
|
buffer =
|
|
Enum.reduce(1..5, :queue.new(), fn i, q ->
|
|
:queue.in(%{sender: "P#{i}"}, q)
|
|
end)
|
|
|
|
state = %{
|
|
demand: 0,
|
|
buffer: buffer,
|
|
buffer_size: 5,
|
|
max_buffer_size: 1000,
|
|
high_water_mark: 800,
|
|
low_water_mark: 300,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, events, new_state} = PacketProducer.handle_demand(3, state)
|
|
|
|
# Should dispatch first 3 (FIFO order)
|
|
assert length(events) == 3
|
|
assert Enum.map(events, & &1.sender) == ["P1", "P2", "P3"]
|
|
assert new_state.buffer_size == 2
|
|
assert new_state.demand == 0
|
|
end
|
|
|
|
test "dispatches all available when demand exceeds buffer" do
|
|
buffer = :queue.in(%{sender: "ONLY"}, :queue.new())
|
|
|
|
state = %{
|
|
demand: 0,
|
|
buffer: buffer,
|
|
buffer_size: 1,
|
|
max_buffer_size: 1000,
|
|
high_water_mark: 800,
|
|
low_water_mark: 300,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, events, new_state} = PacketProducer.handle_demand(5, state)
|
|
|
|
assert events == [%{sender: "ONLY"}]
|
|
assert new_state.buffer_size == 0
|
|
assert new_state.demand == 4
|
|
end
|
|
|
|
test "stores demand when buffer is empty" do
|
|
state = %{
|
|
demand: 0,
|
|
buffer: :queue.new(),
|
|
buffer_size: 0,
|
|
max_buffer_size: 1000,
|
|
high_water_mark: 800,
|
|
low_water_mark: 300,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, [], new_state} = PacketProducer.handle_demand(10, state)
|
|
|
|
assert new_state.demand == 10
|
|
end
|
|
|
|
test "accumulates demand" do
|
|
state = %{
|
|
demand: 5,
|
|
buffer: :queue.new(),
|
|
buffer_size: 0,
|
|
max_buffer_size: 1000,
|
|
high_water_mark: 800,
|
|
low_water_mark: 300,
|
|
backpressure_active: false,
|
|
is_monitor_ref: nil
|
|
}
|
|
|
|
{:noreply, [], new_state} = PacketProducer.handle_demand(3, state)
|
|
|
|
assert new_state.demand == 8
|
|
end
|
|
end
|
|
end
|