diff --git a/lib/librevox/listener/base.rb b/lib/librevox/listener/base.rb index ab06335..32a26af 100644 --- a/lib/librevox/listener/base.rb +++ b/lib/librevox/listener/base.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'async/barrier' require 'async/semaphore' require 'securerandom' +require 'set' module Librevox module Listener @@ -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 @@ -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 @@ -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 diff --git a/test/functional/librevox/listener/base_test.rb b/test/functional/librevox/listener/base_test.rb index 7892439..6f6ad0b 100644 --- a/test/functional/librevox/listener/base_test.rb +++ b/test/functional/librevox/listener/base_test.rb @@ -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