Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions lib/librevox/listener/base.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# frozen_string_literal: true

require 'async/barrier'
require 'async/semaphore'
require 'securerandom'
require 'set'

module Librevox
module Listener
Expand All @@ -21,7 +21,7 @@ def initialize(connection)
@connection = connection
@reply_promises = []
@app_promises = {}
@event_barrier = Async::Barrier.new
@event_tasks = Set.new
@write_lock = Async::Semaphore.new(1)
end

Expand Down Expand Up @@ -92,9 +92,14 @@ def receive_message(response)
@app_promises.delete(app_uuid)&.resolve(response)
end

@event_barrier.async do
on_event(response)
invoke_event_hooks(response)
Async do |task|
@event_tasks << task
begin
on_event(response)
invoke_event_hooks(response)
ensure
@event_tasks.delete(task)
end
end
end
end
Expand All @@ -108,7 +113,7 @@ def connection_closed
@reply_promises.clear
@app_promises.clear

@event_barrier.wait
@event_tasks.each { |task| task.wait rescue nil }
rescue ConnectionError
# Expected — event hooks may have been mid-command when disconnected
end
Expand Down
17 changes: 17 additions & 0 deletions test/functional/librevox/listener/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,21 @@ def test_on_event_with_api_does_not_block_handle_response

assert completed, "Deadlock: on_event with api command blocked handle_response"
end

# Regression: a long-lived inbound connection dispatches one event-handler
# task per ESL event. These are fire-and-forget, so once finished they must be
# released. Previously they were spawned on a per-connection Async::Barrier,
# whose task list only shrinks on #wait (called just once, on disconnect) — so
# every event leaked a task node for the life of the socket (~GBs over days on
# a busy switch). They must not accumulate.
def test_completed_event_tasks_are_not_retained
Sync do
100.times { event "SOME_EVENT" }

in_flight = @listener.instance_variable_get(:@event_tasks)
assert_empty in_flight,
"completed event-handler tasks must not accumulate on the listener " \
"(got #{in_flight.size} retained after 100 events)"
end
end
end
Loading