Add bounded outbound queue backpressure to connections

This commit is contained in:
2026-03-13 20:58:28 +01:00
parent df3f2dae8d
commit 3b66ed1996
5 changed files with 386 additions and 6 deletions

View File

@@ -5,6 +5,7 @@ defmodule Parrhesia.ConfigTest do
assert Parrhesia.Config.get([:limits, :max_frame_bytes]) == 1_048_576
assert Parrhesia.Config.get([:limits, :max_event_bytes]) == 262_144
assert Parrhesia.Config.get([:limits, :max_event_future_skew_seconds]) == 900
assert Parrhesia.Config.get([:limits, :max_outbound_queue]) == 256
assert Parrhesia.Config.get([:policies, :auth_required_for_writes]) == false
assert Parrhesia.Config.get([:features, :nip_ee_mls]) == false
end

View File

@@ -133,6 +133,107 @@ defmodule Parrhesia.Web.ConnectionTest do
]
end
test "fanout_event enqueues and drains matching events" do
state = subscribed_connection_state([])
event = live_event("event-1", 1)
assert {:ok, queued_state} = Connection.handle_info({:fanout_event, "sub-1", event}, state)
assert queued_state.outbound_queue_size == 1
assert_receive :drain_outbound_queue
assert {:push, [{:text, payload}], drained_state} =
Connection.handle_info(:drain_outbound_queue, queued_state)
assert drained_state.outbound_queue_size == 0
assert Jason.decode!(payload) == ["EVENT", "sub-1", event]
end
test "fanout_event ignores non-matching subscription filters" do
state = subscribed_connection_state([])
assert {:ok, next_state} =
Connection.handle_info({:fanout_event, "sub-1", live_event("event-2", 2)}, state)
assert next_state.outbound_queue_size == 0
refute_received :drain_outbound_queue
end
test "outbound queue overflow closes connection when strategy is close" do
state =
subscribed_connection_state(
max_outbound_queue: 1,
outbound_overflow_strategy: :close,
outbound_drain_batch_size: 1
)
event_one = live_event("event-1", 1)
event_two = live_event("event-2", 1)
assert {:ok, queued_state} =
Connection.handle_info({:fanout_event, "sub-1", event_one}, state)
assert queued_state.outbound_queue_size == 1
assert_receive :drain_outbound_queue
assert {:stop, :normal, {1008, message}, [{:text, notice_payload}], _overflow_state} =
Connection.handle_info({:fanout_event, "sub-1", event_two}, queued_state)
assert message == "rate-limited: outbound queue overflow"
assert Jason.decode!(notice_payload) == ["NOTICE", message]
end
test "outbound queue overflow drops oldest event when strategy is drop_oldest" do
state =
subscribed_connection_state(
max_outbound_queue: 1,
outbound_overflow_strategy: :drop_oldest,
outbound_drain_batch_size: 1
)
event_one = live_event("event-1", 1)
event_two = live_event("event-2", 1)
assert {:ok, queued_state} =
Connection.handle_info({:fanout_event, "sub-1", event_one}, state)
assert queued_state.outbound_queue_size == 1
assert_receive :drain_outbound_queue
assert {:ok, replaced_state} =
Connection.handle_info({:fanout_event, "sub-1", event_two}, queued_state)
assert replaced_state.outbound_queue_size == 1
assert {:push, [{:text, payload}], drained_state} =
Connection.handle_info(:drain_outbound_queue, replaced_state)
assert drained_state.outbound_queue_size == 0
assert Jason.decode!(payload) == ["EVENT", "sub-1", event_two]
end
defp subscribed_connection_state(opts) do
{:ok, initial_state} = Connection.init(Keyword.put_new(opts, :subscription_index, nil))
req_payload = Jason.encode!(["REQ", "sub-1", %{"kinds" => [1]}])
assert {:push, _, subscribed_state} =
Connection.handle_in({req_payload, [opcode: :text]}, initial_state)
subscribed_state
end
defp live_event(id, kind) do
%{
"id" => id,
"pubkey" => String.duplicate("a", 64),
"created_at" => System.system_time(:second),
"kind" => kind,
"tags" => [],
"content" => "live",
"sig" => String.duplicate("b", 128)
}
end
defp valid_event do
base_event = %{
"pubkey" => String.duplicate("1", 64),