omq 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/README.md +27 -0
  4. data/lib/omq/drop_queue.rb +3 -0
  5. data/lib/omq/engine/connection_setup.rb +70 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/maintenance.rb +35 -0
  8. data/lib/omq/engine/reconnect.rb +82 -0
  9. data/lib/omq/engine/recv_pump.rb +119 -0
  10. data/lib/omq/engine.rb +139 -304
  11. data/lib/omq/options.rb +44 -0
  12. data/lib/omq/pair.rb +6 -0
  13. data/lib/omq/pub_sub.rb +25 -0
  14. data/lib/omq/push_pull.rb +17 -0
  15. data/lib/omq/queue_interface.rb +1 -0
  16. data/lib/omq/readable.rb +2 -0
  17. data/lib/omq/req_rep.rb +13 -0
  18. data/lib/omq/router_dealer.rb +12 -0
  19. data/lib/omq/routing/conn_send_pump.rb +36 -0
  20. data/lib/omq/routing/dealer.rb +15 -10
  21. data/lib/omq/routing/fair_queue.rb +172 -0
  22. data/lib/omq/routing/fair_recv.rb +27 -0
  23. data/lib/omq/routing/fan_out.rb +127 -74
  24. data/lib/omq/routing/pair.rb +47 -20
  25. data/lib/omq/routing/pub.rb +12 -6
  26. data/lib/omq/routing/pull.rb +12 -4
  27. data/lib/omq/routing/push.rb +3 -12
  28. data/lib/omq/routing/rep.rb +41 -51
  29. data/lib/omq/routing/req.rb +15 -10
  30. data/lib/omq/routing/round_robin.rb +82 -63
  31. data/lib/omq/routing/router.rb +32 -48
  32. data/lib/omq/routing/sub.rb +18 -5
  33. data/lib/omq/routing/xpub.rb +15 -3
  34. data/lib/omq/routing/xsub.rb +53 -27
  35. data/lib/omq/routing.rb +29 -11
  36. data/lib/omq/socket.rb +25 -7
  37. data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
  38. data/lib/omq/transport/inproc.rb +41 -217
  39. data/lib/omq/transport/ipc.rb +7 -1
  40. data/lib/omq/transport/tcp.rb +12 -7
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +2 -0
  43. data/lib/omq.rb +4 -1
  44. metadata +14 -5
@@ -5,40 +5,52 @@ module OMQ
5
5
  # Mixin for routing strategies that fan-out to subscribers.
6
6
  #
7
7
  # Manages per-connection subscription sets, subscription command
8
- # listeners, and a send pump that delivers to all matching peers.
8
+ # listeners, and per-connection send queues/pumps that deliver
9
+ # to each matching peer independently.
10
+ #
11
+ # HWM is enforced per subscriber: each connection gets its own
12
+ # bounded send queue. DropQueues (for :drop_newest/:drop_oldest)
13
+ # silently drop messages for a slow subscriber without affecting
14
+ # others. LimitedQueues (for :block) block the publisher.
9
15
  #
10
16
  # Including classes must call `init_fan_out(engine)` from
11
17
  # their #initialize.
12
18
  #
13
19
  module FanOut
20
+ # @return [Async::Promise] resolves when the first subscriber joins
21
+ #
14
22
  attr_reader :subscriber_joined
15
23
 
16
- # @return [Boolean] true when the send pump is idle (not sending a batch)
17
- def send_pump_idle? = @send_pump_idle
24
+ # @return [Boolean] true when all per-connection send queues are empty
25
+ #
26
+ def send_queues_drained?
27
+ @conn_queues.values.all?(&:empty?)
28
+ end
18
29
 
19
30
  private
20
31
 
21
32
  def init_fan_out(engine)
22
- @connections = []
33
+ @connections = Set.new
23
34
  @subscriptions = {} # connection => Set of prefixes
24
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
25
- @on_mute = engine.options.on_mute
26
- @send_pump_started = false
27
- @send_pump_idle = true
35
+ @subscribe_all = Set.new # connections subscribed to "" (match-all fast path)
36
+ @conn_queues = {} # connection => per-connection send queue
37
+ @conn_send_tasks = {} # connection => send pump task
28
38
  @conflate = engine.options.conflate
29
39
  @subscriber_joined = Async::Promise.new
30
- @written = Set.new
31
40
  @latest = {} if @conflate
32
41
  end
33
42
 
43
+
34
44
  # @return [Boolean] whether the connection is subscribed to the topic
35
45
  #
36
46
  def subscribed?(conn, topic)
47
+ return true if @subscribe_all.include?(conn)
37
48
  subs = @subscriptions[conn]
38
49
  return false unless subs
39
50
  subs.any? { |prefix| topic.start_with?(prefix) }
40
51
  end
41
52
 
53
+
42
54
  # Called when a subscription command is received from a peer.
43
55
  # Override in subclasses to expose subscriptions to the
44
56
  # application (e.g. XPUB enqueues to recv_queue).
@@ -48,9 +60,11 @@ module OMQ
48
60
  #
49
61
  def on_subscribe(conn, prefix)
50
62
  @subscriptions[conn] << prefix.b.freeze
63
+ @subscribe_all.add(conn) if prefix.empty?
51
64
  @subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
52
65
  end
53
66
 
67
+
54
68
  # Called when a cancel command is received from a peer.
55
69
  # Override in subclasses (e.g. XPUB enqueues to recv_queue).
56
70
  #
@@ -59,79 +73,54 @@ module OMQ
59
73
  #
60
74
  def on_cancel(conn, prefix)
61
75
  @subscriptions[conn]&.delete(prefix)
76
+ @subscribe_all.delete(conn) if prefix.empty?
62
77
  end
63
78
 
64
- # Returns true if the connection is muted (recv queue full) and
65
- # the on_mute strategy says to drop rather than block.
79
+
80
+ # Creates a per-connection send queue and starts its send pump.
81
+ # Call from #connection_added.
82
+ #
83
+ # @param conn [Connection]
66
84
  #
67
- def muted?(conn)
68
- return false if @on_mute == :block
69
- q = conn.direct_recv_queue if conn.respond_to?(:direct_recv_queue)
70
- q&.respond_to?(:limited?) && q.limited?
85
+ def add_fan_out_send_connection(conn)
86
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
87
+ @conn_queues[conn] = q
88
+ start_conn_send_pump(conn, q)
71
89
  end
72
90
 
73
91
 
74
- def start_send_pump
75
- @send_pump_started = true
76
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
77
- loop do
78
- @send_pump_idle = true
79
- batch = [@send_queue.dequeue]
80
- @send_pump_idle = false
81
- Routing.drain_send_queue(@send_queue, batch)
82
-
83
- @written.clear
84
-
85
- if @conflate
86
- # Keep only the last matching message per connection.
87
- @latest.clear
88
- batch.each do |parts|
89
- topic = parts.first || EMPTY_BINARY
90
- @connections.each do |conn|
91
- next unless subscribed?(conn, topic)
92
- @latest[conn] = parts
93
- end
94
- end
95
- @latest.each do |conn, parts|
96
- next if muted?(conn)
97
- begin
98
- conn.write_message(parts)
99
- @written << conn
100
- rescue *CONNECTION_LOST
101
- end
102
- end
103
- else
104
- batch.each do |parts|
105
- topic = parts.first || EMPTY_BINARY
106
- wire_bytes = nil
107
-
108
- @connections.each do |conn|
109
- next unless subscribed?(conn, topic)
110
- next if muted?(conn)
111
- begin
112
- if conn.respond_to?(:curve?) && conn.curve?
113
- conn.write_message(parts)
114
- elsif conn.respond_to?(:write_wire)
115
- wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(parts)
116
- conn.write_wire(wire_bytes)
117
- else
118
- conn.write_message(parts)
119
- end
120
- @written << conn
121
- rescue *CONNECTION_LOST
122
- end
123
- end
124
- end
125
- end
92
+ # Stops the per-connection send pump and removes the queue.
93
+ # Call from #connection_removed.
94
+ #
95
+ # @param conn [Connection]
96
+ #
97
+ def remove_fan_out_send_connection(conn)
98
+ @subscribe_all.delete(conn)
99
+ @conn_queues.delete(conn)
100
+ @conn_send_tasks.delete(conn)&.stop
101
+ end
126
102
 
127
- @written.each do |conn|
128
- conn.flush
129
- rescue *CONNECTION_LOST
130
- end
131
- end
103
+
104
+ # Fans a message out to every connected peer's send queue.
105
+ # Subscription filtering happens in the per-connection send pump so
106
+ # that late-arriving subscriptions (e.g. inproc connect-before-subscribe)
107
+ # are respected: a message enqueued before the async subscription listener
108
+ # has processed SUBSCRIBE commands will still be delivered correctly.
109
+ #
110
+ # Per-connection queues use :block (Async::LimitedQueue) for
111
+ # backpressure: when a subscriber's queue is full, the publisher
112
+ # yields until the send pump drains it. This matches the old
113
+ # shared-queue behavior and keeps the publisher fiber-friendly.
114
+ #
115
+ # @param parts [Array<String>]
116
+ #
117
+ def fan_out_enqueue(parts)
118
+ @connections.each do |conn|
119
+ @conn_queues[conn].enqueue(parts)
132
120
  end
133
121
  end
134
122
 
123
+
135
124
  def start_subscription_listener(conn)
136
125
  @tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
137
126
  loop do
@@ -139,14 +128,78 @@ module OMQ
139
128
  next unless frame.command?
140
129
  cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
141
130
  case cmd.name
142
- when "SUBSCRIBE" then on_subscribe(conn, cmd.data)
143
- when "CANCEL" then on_cancel(conn, cmd.data)
131
+ when "SUBSCRIBE"
132
+ on_subscribe(conn, cmd.data)
133
+ when "CANCEL"
134
+ on_cancel(conn, cmd.data)
144
135
  end
145
136
  end
146
137
  rescue *CONNECTION_LOST
147
138
  @engine.connection_lost(conn)
148
139
  end
149
140
  end
141
+
142
+
143
+ # Starts a dedicated send pump for one subscriber connection.
144
+ # Uses write_wire (pre-encoded bytes) for non-encrypted TCP connections
145
+ # to avoid re-encoding the same message N times during fan-out.
146
+ # In conflate mode, drains the batch and keeps only the latest
147
+ # message per topic before writing.
148
+ #
149
+ # @param conn [Connection]
150
+ # @param q [Async::LimitedQueue, DropQueue]
151
+ #
152
+ def start_conn_send_pump(conn, q)
153
+ use_wire = conn.respond_to?(:write_wire) && !conn.encrypted?
154
+ task = @conflate ? start_conn_send_pump_conflate(conn, q) : start_conn_send_pump_normal(conn, q, use_wire)
155
+ @conn_send_tasks[conn] = task
156
+ @tasks << task
157
+ end
158
+
159
+
160
+ def start_conn_send_pump_normal(conn, q, use_wire)
161
+ @engine.spawn_pump_task(annotation: "send pump") do
162
+ loop do
163
+ batch = [q.dequeue]
164
+ Routing.drain_send_queue(q, batch)
165
+ conn.flush if write_matching_batch(conn, batch, use_wire)
166
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
167
+ @engine.connection_lost(conn)
168
+ break
169
+ end
170
+ end
171
+ end
172
+
173
+
174
+ def write_matching_batch(conn, batch, use_wire)
175
+ sent = false
176
+ batch.each do |parts|
177
+ next unless subscribed?(conn, parts.first || EMPTY_BINARY)
178
+ use_wire ? conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts)) : conn.write_message(parts)
179
+ sent = true
180
+ end
181
+ sent
182
+ end
183
+
184
+
185
+ def start_conn_send_pump_conflate(conn, q)
186
+ @engine.spawn_pump_task(annotation: "send pump") do
187
+ loop do
188
+ batch = [q.dequeue]
189
+ Routing.drain_send_queue(q, batch)
190
+ # Keep only the latest message that matches the subscription.
191
+ latest = batch.reverse.find { |parts| subscribed?(conn, parts.first || EMPTY_BINARY) }
192
+ next unless latest
193
+ begin
194
+ conn.write_message(latest)
195
+ conn.flush
196
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
197
+ @engine.connection_lost(conn)
198
+ break
199
+ end
200
+ end
201
+ end
202
+ end
150
203
  end
151
204
  end
152
205
  end
@@ -4,25 +4,29 @@ module OMQ
4
4
  module Routing
5
5
  # PAIR socket routing: exclusive 1-to-1 bidirectional.
6
6
  #
7
- # Only one peer connection is allowed. Messages flow through
8
- # internal send/recv queues backed by Async::LimitedQueue.
7
+ # Only one peer connection is allowed. Send and recv queues are
8
+ # created per-connection (and destroyed on disconnection) so
9
+ # HWM is consistent with multi-peer socket types.
9
10
  #
10
11
  class Pair
12
+ include FairRecv
11
13
 
12
14
  # @param engine [Engine]
13
15
  #
14
16
  def initialize(engine)
15
- @engine = engine
16
- @connection = nil
17
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
18
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
17
+ @engine = engine
18
+ @connection = nil
19
+ @recv_queue = FairQueue.new
20
+ @send_queue = nil # created per-connection
21
+ @staging_queue = Routing.build_queue(@engine.options.send_hwm, :block)
22
+ @send_pump = nil
19
23
  @tasks = []
20
- @send_pump_idle = true
21
24
  end
22
25
 
23
- # @return [Async::LimitedQueue]
26
+
27
+ # @return [FairQueue]
24
28
  #
25
- attr_reader :recv_queue, :send_queue
29
+ attr_reader :recv_queue
26
30
 
27
31
  # @param connection [Connection]
28
32
  # @raise [RuntimeError] if a connection already exists
@@ -30,54 +34,77 @@ module OMQ
30
34
  def connection_added(connection)
31
35
  raise "PAIR allows only one peer" if @connection
32
36
  @connection = connection
33
- task = @engine.start_recv_pump(connection, @recv_queue)
34
- @tasks << task if task
35
- start_send_pump(connection) unless connection.is_a?(Transport::Inproc::DirectPipe)
37
+
38
+ add_fair_recv_connection(connection)
39
+
40
+ unless connection.is_a?(Transport::Inproc::DirectPipe)
41
+ @send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
42
+ while (msg = @staging_queue.dequeue(timeout: 0))
43
+ @send_queue.enqueue(msg)
44
+ end
45
+ start_send_pump(connection)
46
+ end
36
47
  end
37
48
 
49
+
38
50
  # @param connection [Connection]
39
51
  #
40
52
  def connection_removed(connection)
41
53
  if @connection == connection
42
54
  @connection = nil
55
+ @recv_queue.remove_queue(connection)
56
+ @send_queue = nil
43
57
  @send_pump&.stop
44
58
  @send_pump = nil
45
59
  end
46
60
  end
47
61
 
62
+
48
63
  # @param parts [Array<String>]
49
64
  #
50
65
  def enqueue(parts)
51
66
  conn = @connection
52
67
  if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
53
68
  conn.send_message(parts)
54
- else
69
+ elsif @send_queue
55
70
  @send_queue.enqueue(parts)
71
+ else
72
+ @staging_queue.enqueue(parts)
56
73
  end
57
74
  end
58
75
 
76
+
77
+ # Stops all background tasks.
78
+ #
79
+ # @return [void]
59
80
  #
60
81
  def stop
61
82
  @tasks.each(&:stop)
62
83
  @tasks.clear
63
84
  end
64
85
 
65
- def send_pump_idle? = @send_pump_idle
86
+
87
+ # @return [Boolean] true when the staging and send queues are empty
88
+ #
89
+ def send_queues_drained?
90
+ @staging_queue.empty? && (@send_queue.nil? || @send_queue.empty?)
91
+ end
66
92
 
67
93
  private
68
94
 
69
95
  def start_send_pump(conn)
70
96
  @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
71
97
  loop do
72
- @send_pump_idle = true
73
98
  batch = [@send_queue.dequeue]
74
- @send_pump_idle = false
75
99
  Routing.drain_send_queue(@send_queue, batch)
76
- batch.each { |parts| conn.write_message(parts) }
77
- conn.flush
100
+ begin
101
+ batch.each { |parts| conn.write_message(parts) }
102
+ conn.flush
103
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
104
+ @engine.connection_lost(conn)
105
+ break
106
+ end
78
107
  end
79
- rescue *CONNECTION_LOST
80
- @engine.connection_lost(conn)
81
108
  end
82
109
  @tasks << @send_pump
83
110
  end
@@ -5,7 +5,8 @@ module OMQ
5
5
  # PUB socket routing: fan-out to all subscribers.
6
6
  #
7
7
  # Listens for SUBSCRIBE/CANCEL commands from peers.
8
- # Drops messages if a subscriber's connection write fails.
8
+ # Each subscriber gets its own bounded send queue; slow subscribers
9
+ # are muted via the socket's on_mute strategy (drop by default).
9
10
  #
10
11
  class Pub
11
12
  include FanOut
@@ -18,9 +19,6 @@ module OMQ
18
19
  init_fan_out(engine)
19
20
  end
20
21
 
21
- # @return [Async::LimitedQueue]
22
- #
23
- attr_reader :send_queue
24
22
 
25
23
  # PUB is write-only.
26
24
  #
@@ -28,28 +26,36 @@ module OMQ
28
26
  raise "PUB sockets cannot receive"
29
27
  end
30
28
 
29
+
31
30
  # @param connection [Connection]
32
31
  #
33
32
  def connection_added(connection)
34
33
  @connections << connection
35
34
  @subscriptions[connection] = Set.new
36
35
  start_subscription_listener(connection)
37
- start_send_pump unless @send_pump_started
36
+ add_fan_out_send_connection(connection)
38
37
  end
39
38
 
39
+
40
40
  # @param connection [Connection]
41
41
  #
42
42
  def connection_removed(connection)
43
43
  @connections.delete(connection)
44
44
  @subscriptions.delete(connection)
45
+ remove_fan_out_send_connection(connection)
45
46
  end
46
47
 
48
+
47
49
  # @param parts [Array<String>]
48
50
  #
49
51
  def enqueue(parts)
50
- @send_queue.enqueue(parts)
52
+ fan_out_enqueue(parts)
51
53
  end
52
54
 
55
+
56
+ # Stops all background tasks.
57
+ #
58
+ # @return [void]
53
59
  #
54
60
  def stop
55
61
  @tasks.each(&:stop)
@@ -5,37 +5,45 @@ module OMQ
5
5
  # PULL socket routing: fair-queue receive from PUSH peers.
6
6
  #
7
7
  class Pull
8
+ include FairRecv
8
9
  # @param engine [Engine]
9
10
  #
10
11
  def initialize(engine)
11
12
  @engine = engine
12
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
13
+ @recv_queue = FairQueue.new
13
14
  @tasks = []
14
15
  end
15
16
 
16
- # @return [Async::LimitedQueue]
17
+
18
+ # @return [FairQueue]
17
19
  #
18
20
  attr_reader :recv_queue
19
21
 
20
22
  # @param connection [Connection]
21
23
  #
22
24
  def connection_added(connection)
23
- task = @engine.start_recv_pump(connection, @recv_queue)
24
- @tasks << task if task
25
+ add_fair_recv_connection(connection)
25
26
  end
26
27
 
28
+
27
29
  # @param connection [Connection]
28
30
  #
29
31
  def connection_removed(connection)
32
+ @recv_queue.remove_queue(connection)
30
33
  # recv pump stops on EOFError
31
34
  end
32
35
 
36
+
33
37
  # PULL is read-only.
34
38
  #
35
39
  def enqueue(_parts)
36
40
  raise "PULL sockets cannot send"
37
41
  end
38
42
 
43
+
44
+ # Stops all background tasks.
45
+ #
46
+ # @return [void]
39
47
  #
40
48
  def stop
41
49
  @tasks.each(&:stop)
@@ -16,11 +16,6 @@ module OMQ
16
16
  end
17
17
 
18
18
 
19
- # @return [Async::LimitedQueue]
20
- #
21
- attr_reader :send_queue
22
-
23
-
24
19
  # PUSH is write-only.
25
20
  #
26
21
  def recv_queue
@@ -32,9 +27,7 @@ module OMQ
32
27
  #
33
28
  def connection_added(connection)
34
29
  @connections << connection
35
- signal_connection_available
36
- update_direct_pipe
37
- start_send_pump unless @send_pump_started
30
+ add_round_robin_send_connection(connection)
38
31
  start_reaper(connection)
39
32
  end
40
33
 
@@ -43,7 +36,7 @@ module OMQ
43
36
  #
44
37
  def connection_removed(connection)
45
38
  @connections.delete(connection)
46
- update_direct_pipe
39
+ remove_round_robin_send_connection(connection)
47
40
  end
48
41
 
49
42
 
@@ -54,7 +47,7 @@ module OMQ
54
47
  end
55
48
 
56
49
 
57
- # Stops all background tasks (send pump, reapers).
50
+ # Stops all background tasks (send pumps, reapers).
58
51
  #
59
52
  def stop
60
53
  @tasks.each(&:stop)
@@ -76,8 +69,6 @@ module OMQ
76
69
  @engine.connection_lost(conn)
77
70
  end
78
71
  end
79
-
80
-
81
72
  end
82
73
  end
83
74
  end
@@ -9,93 +9,83 @@ module OMQ
9
9
  # on send.
10
10
  #
11
11
  class Rep
12
- # @param engine [Engine]
13
- #
12
+ include FairRecv
13
+
14
14
  EMPTY_FRAME = "".b.freeze
15
15
 
16
+
17
+ # @param engine [Engine]
18
+ #
16
19
  def initialize(engine)
17
- @engine = engine
18
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
20
- @pending_replies = []
21
- @tasks = []
22
- @send_pump_started = false
23
- @send_pump_idle = true
24
- @written = Set.new
20
+ @engine = engine
21
+ @recv_queue = FairQueue.new
22
+ @pending_replies = []
23
+ @conn_queues = {} # connection => per-connection send queue
24
+ @conn_send_tasks = {} # connection => send pump task
25
+ @tasks = []
25
26
  end
26
27
 
27
- # @return [Async::LimitedQueue]
28
+
29
+ # @return [FairQueue]
28
30
  #
29
- attr_reader :recv_queue, :send_queue
31
+ attr_reader :recv_queue
30
32
 
31
33
  # @param connection [Connection]
32
34
  #
33
35
  def connection_added(connection)
34
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
36
+ add_fair_recv_connection(connection) do |msg|
35
37
  delimiter = msg.index(&:empty?) || msg.size
36
38
  envelope = msg[0, delimiter]
37
39
  body = msg[(delimiter + 1)..] || []
38
40
  @pending_replies << { conn: connection, envelope: envelope }
39
41
  body
40
42
  end
41
- @tasks << task if task
42
- start_send_pump unless @send_pump_started
43
+
44
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
45
+ @conn_queues[connection] = q
46
+ @conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
43
47
  end
44
48
 
49
+
45
50
  # @param connection [Connection]
46
51
  #
47
52
  def connection_removed(connection)
48
- # Remove any pending replies for this connection
49
53
  @pending_replies.reject! { |r| r[:conn] == connection }
54
+ @recv_queue.remove_queue(connection)
55
+ @conn_queues.delete(connection)
56
+ @conn_send_tasks.delete(connection)&.stop
50
57
  end
51
58
 
52
- # Enqueues a reply for sending.
59
+
60
+ # Enqueues a reply. Routes to the connection that sent the matching
61
+ # request by consuming the next pending_reply entry.
53
62
  #
54
63
  # @param parts [Array<String>]
55
64
  #
56
65
  def enqueue(parts)
57
- @send_queue.enqueue(parts)
66
+ reply_info = @pending_replies.shift
67
+ return unless reply_info
68
+ conn = reply_info[:conn]
69
+ @conn_queues[conn]&.enqueue([*reply_info[:envelope], EMPTY_FRAME, *parts])
58
70
  end
59
71
 
72
+
73
+ # Stops all background tasks.
74
+ #
75
+ # @return [void]
76
+ #
60
77
  def stop
61
78
  @tasks.each(&:stop)
62
79
  @tasks.clear
63
80
  end
64
81
 
65
- def send_pump_idle? = @send_pump_idle
66
-
67
- private
68
-
69
- def start_send_pump
70
- @send_pump_started = true
71
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
72
- loop do
73
- @send_pump_idle = true
74
- batch = [@send_queue.dequeue]
75
- @send_pump_idle = false
76
- Routing.drain_send_queue(@send_queue, batch)
77
-
78
- @written.clear
79
- batch.each do |parts|
80
- reply_info = @pending_replies.shift
81
- next unless reply_info
82
- conn = reply_info[:conn]
83
- begin
84
- conn.write_message([*reply_info[:envelope], EMPTY_FRAME, *parts])
85
- @written << conn
86
- rescue *CONNECTION_LOST
87
- # connection lost mid-write
88
- end
89
- end
90
-
91
- @written.each do |conn|
92
- conn.flush
93
- rescue *CONNECTION_LOST
94
- # connection lost mid-flush
95
- end
96
- end
97
- end
82
+
83
+ # @return [Boolean] true when all per-connection send queues are empty
84
+ #
85
+ def send_queues_drained?
86
+ @conn_queues.values.all?(&:empty?)
98
87
  end
88
+
99
89
  end
100
90
  end
101
91
  end