omq 0.5.1 → 0.6.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +184 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +468 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +24 -3
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +10 -5
  28. data/lib/omq/zmtp/routing/channel.rb +8 -2
  29. data/lib/omq/zmtp/routing/fan_out.rb +38 -8
  30. data/lib/omq/zmtp/routing/pair.rb +8 -2
  31. data/lib/omq/zmtp/routing/peer.rb +7 -1
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +32 -11
  34. data/lib/omq/zmtp/routing/rep.rb +11 -7
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +35 -1
  37. data/lib/omq/zmtp/routing/router.rb +7 -1
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +7 -1
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/transport/inproc.rb +40 -5
  42. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  43. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  44. data/lib/omq/zmtp/writable.rb +21 -4
  45. data/lib/omq.rb +7 -0
  46. metadata +18 -3
  47. data/exe/omqcat +0 -532
@@ -14,22 +14,27 @@ module OMQ
14
14
  #
15
15
  attr_reader :socket_type
16
16
 
17
+
17
18
  # @return [Options] socket options
18
19
  #
19
20
  attr_reader :options
20
21
 
22
+
21
23
  # @return [Routing] routing strategy
22
24
  #
23
25
  attr_reader :routing
24
26
 
27
+
25
28
  # @return [String, nil] last bound endpoint
26
29
  #
27
30
  attr_reader :last_endpoint
28
31
 
32
+
29
33
  # @return [Integer, nil] last auto-selected TCP port
30
34
  #
31
35
  attr_reader :last_tcp_port
32
36
 
37
+
33
38
  # @param socket_type [Symbol] e.g. :REQ, :REP, :PAIR
34
39
  # @param options [Options]
35
40
  #
@@ -43,10 +48,24 @@ module OMQ
43
48
  @listeners = []
44
49
  @tasks = []
45
50
  @closed = false
51
+ @closing = false
46
52
  @last_endpoint = nil
47
53
  @last_tcp_port = nil
54
+ @peer_connected = Async::Promise.new
55
+ @all_peers_gone = Async::Promise.new
56
+ @reconnect_enabled = true
57
+ @parent_task = nil
58
+ @connection_promises = {} # connection => Async::Promise
59
+ @fatal_error = nil
48
60
  end
49
61
 
62
+
63
+ attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
64
+
65
+
66
+ attr_writer :reconnect_enabled
67
+
68
+
50
69
  # Binds to an endpoint.
51
70
  #
52
71
  # @param endpoint [String] e.g. "tcp://127.0.0.1:5555", "inproc://foo"
@@ -54,6 +73,7 @@ module OMQ
54
73
  # @raise [ArgumentError] on unsupported transport
55
74
  #
56
75
  def bind(endpoint)
76
+ capture_parent_task
57
77
  transport = transport_for(endpoint)
58
78
  listener = transport.bind(endpoint, self)
59
79
  @listeners << listener
@@ -61,12 +81,14 @@ module OMQ
61
81
  @last_tcp_port = extract_tcp_port(listener.endpoint)
62
82
  end
63
83
 
84
+
64
85
  # Connects to an endpoint.
65
86
  #
66
87
  # @param endpoint [String]
67
88
  # @return [void]
68
89
  #
69
90
  def connect(endpoint)
91
+ capture_parent_task
70
92
  @connected_endpoints << endpoint
71
93
  if endpoint.start_with?("inproc://")
72
94
  # Inproc connect is synchronous and instant
@@ -78,6 +100,7 @@ module OMQ
78
100
  end
79
101
  end
80
102
 
103
+
81
104
  # Disconnects from an endpoint. Closes connections to that endpoint
82
105
  # and stops auto-reconnection for it.
83
106
  #
@@ -95,6 +118,7 @@ module OMQ
95
118
  end
96
119
  end
97
120
 
121
+
98
122
  # Unbinds from an endpoint. Stops the listener and closes all
99
123
  # connections that were accepted on it.
100
124
  #
@@ -117,6 +141,7 @@ module OMQ
117
141
  end
118
142
  end
119
143
 
144
+
120
145
  # Called by a transport when an incoming connection is accepted.
121
146
  #
122
147
  # @param io [#read, #write, #close]
@@ -124,18 +149,20 @@ module OMQ
124
149
  # @return [void]
125
150
  #
126
151
  def handle_accepted(io, endpoint: nil)
127
- setup_connection(io, as_server: true, endpoint: endpoint)
152
+ spawn_connection(io, as_server: true, endpoint: endpoint)
128
153
  end
129
154
 
155
+
130
156
  # Called by a transport when an outgoing connection is established.
131
157
  #
132
158
  # @param io [#read, #write, #close]
133
159
  # @return [void]
134
160
  #
135
161
  def handle_connected(io, endpoint: nil)
136
- setup_connection(io, as_server: false, endpoint: endpoint)
162
+ spawn_connection(io, as_server: false, endpoint: endpoint)
137
163
  end
138
164
 
165
+
139
166
  # Called by inproc transport with a pre-validated DirectPipe.
140
167
  # Skips ZMTP handshake — just registers with routing strategy.
141
168
  #
@@ -146,25 +173,43 @@ module OMQ
146
173
  @connections << pipe
147
174
  @connection_endpoints[pipe] = endpoint if endpoint
148
175
  @routing.connection_added(pipe)
176
+ @peer_connected.resolve(pipe)
149
177
  end
150
178
 
179
+
151
180
  # Dequeues the next received message. Blocks until available.
152
181
  #
153
182
  # @return [Array<String>] message parts
183
+ # @raise if a background pump task crashed
154
184
  #
155
185
  def dequeue_recv
156
- @routing.recv_queue.dequeue
186
+ raise @fatal_error if @fatal_error
187
+ msg = @routing.recv_queue.dequeue
188
+ raise @fatal_error if msg.nil? && @fatal_error
189
+ msg
157
190
  end
158
191
 
192
+
193
+ # Pushes a nil sentinel into the recv queue, unblocking a
194
+ # pending {#dequeue_recv} with a nil return value.
195
+ #
196
+ def dequeue_recv_sentinel
197
+ @routing.recv_queue.push(nil)
198
+ end
199
+
200
+
159
201
  # Enqueues a message for sending. Blocks at HWM.
160
202
  #
161
203
  # @param parts [Array<String>]
162
204
  # @return [void]
205
+ # @raise if a background pump task crashed
163
206
  #
164
207
  def enqueue_send(parts)
208
+ raise @fatal_error if @fatal_error
165
209
  @routing.enqueue(parts)
166
210
  end
167
211
 
212
+
168
213
  # Starts a recv pump for a connection, or wires the inproc
169
214
  # fast path when the connection is a DirectPipe.
170
215
  #
@@ -180,17 +225,22 @@ module OMQ
180
225
  return nil
181
226
  end
182
227
 
183
- Reactor.spawn_pump do
228
+ Reactor.spawn_pump(annotation: "recv pump") do
184
229
  loop do
185
230
  msg = conn.receive_message
186
- msg = transform ? transform.call(msg) : msg
231
+ msg = transform ? transform.call(msg).freeze : msg
187
232
  recv_queue.enqueue(msg)
188
233
  end
189
- rescue *CONNECTION_LOST
234
+ rescue Async::Stop
235
+ # normal shutdown
236
+ rescue ProtocolError, *CONNECTION_LOST
190
237
  connection_lost(conn)
238
+ rescue => error
239
+ signal_fatal_error(error)
191
240
  end
192
241
  end
193
242
 
243
+
194
244
  # Called when a connection is lost.
195
245
  #
196
246
  # @param connection [Connection]
@@ -202,41 +252,133 @@ module OMQ
202
252
  @routing.connection_removed(connection)
203
253
  connection.close
204
254
 
255
+ # Signal the connection task to exit.
256
+ done = @connection_promises.delete(connection)
257
+ done&.resolve(true)
258
+
259
+ # Resolve all_peers_gone once: had peers, now have none.
260
+ if @peer_connected.resolved? && @connections.empty?
261
+ @all_peers_gone.resolve(true)
262
+ end
263
+
205
264
  # Auto-reconnect if this was a connected (not bound) endpoint
206
- if endpoint && @connected_endpoints.include?(endpoint) && !@closed
265
+ if endpoint && @connected_endpoints.include?(endpoint) && !@closed && !@closing && @reconnect_enabled
207
266
  schedule_reconnect(endpoint)
208
267
  end
209
268
  end
210
269
 
270
+
211
271
  # Closes all connections and listeners.
212
272
  #
213
273
  # @return [void]
214
274
  #
215
275
  def close
216
- return if @closed
217
- @closed = true
276
+ return if @closed || @closing
277
+ @closing = true
278
+
279
+ # Stop accepting new connections — but only if we already have
280
+ # peers to drain to. With zero connections the listeners must
281
+ # stay open so late-arriving peers can still receive queued
282
+ # messages during the linger period.
283
+ unless @connections.empty?
284
+ @listeners.each(&:stop)
285
+ @listeners.clear
286
+ end
218
287
 
219
288
  # Linger: wait for send queues to drain before closing.
220
289
  # linger=0 → close immediately, linger=nil → wait forever.
290
+ # @closed is set AFTER draining so reconnect tasks keep
291
+ # running during the linger period.
221
292
  linger = @options.linger
222
293
  if linger.nil? || linger > 0
223
294
  drain_timeout = linger # nil = wait forever, >0 = seconds
224
295
  drain_send_queues(drain_timeout)
225
296
  end
226
297
 
298
+ @closed = true
299
+
300
+ # Stop any remaining listeners.
301
+ @listeners.each(&:stop)
302
+ @listeners.clear
303
+
227
304
  # Close connections — causes pump tasks to get EOFError/IOError
228
305
  @connections.each(&:close)
229
306
  @connections.clear
230
- @listeners.each(&:stop)
231
- @listeners.clear
232
307
  # Stop any remaining pump tasks
233
308
  @routing.stop rescue nil
234
309
  @tasks.each { |t| t.stop rescue nil }
235
310
  @tasks.clear
236
311
  end
237
312
 
313
+
314
+ # Spawns a transient pump task with error propagation.
315
+ #
316
+ # Unexpected exceptions are caught and forwarded to
317
+ # {#signal_fatal_error} so blocked callers (send/recv)
318
+ # see the real error instead of deadlocking.
319
+ #
320
+ # @param annotation [String] task annotation for debugging
321
+ # @yield the pump loop body
322
+ # @return [Async::Task]
323
+ #
324
+ def spawn_pump_task(annotation:, &block)
325
+ @parent_task.async(transient: true, annotation: annotation) do
326
+ yield
327
+ rescue Async::Stop, ProtocolError, *CONNECTION_LOST
328
+ # normal shutdown / expected disconnect
329
+ rescue => error
330
+ signal_fatal_error(error)
331
+ end
332
+ end
333
+
334
+
335
+ # Wraps an unexpected pump error as {OMQ::SocketDeadError} and
336
+ # unblocks any callers waiting on the recv queue.
337
+ #
338
+ # Must be called from inside a rescue block so that +error+ is
339
+ # +$!+ and Ruby sets it as +#cause+ on the new exception.
340
+ #
341
+ # @param error [Exception]
342
+ #
343
+ def signal_fatal_error(error)
344
+ return if @closing || @closed
345
+ @fatal_error = begin
346
+ raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
347
+ rescue => wrapped
348
+ wrapped
349
+ end
350
+ @routing.recv_queue.enqueue(nil) rescue nil
351
+ end
352
+
353
+
238
354
  private
239
355
 
356
+
357
+ # Saves the current Async task so connection subtrees can be
358
+ # spawned as siblings of the caller's task.
359
+ #
360
+ def capture_parent_task
361
+ @parent_task ||= Async::Task.current? ? Async::Task.current : nil
362
+ end
363
+
364
+
365
+ # Spawns an isolated connection task as a sibling of accept/reconnect
366
+ # tasks. All per-connection children (heartbeat, recv pump, reaper)
367
+ # live inside this task. When the connection dies, the entire subtree
368
+ # is cleaned up by Async.
369
+ #
370
+ def spawn_connection(io, as_server:, endpoint: nil)
371
+ task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
372
+ done = Async::Promise.new
373
+ setup_connection(io, as_server: as_server, endpoint: endpoint, done: done)
374
+ done.wait
375
+ rescue ProtocolError, *CONNECTION_LOST
376
+ # handshake failed or connection lost — subtree cleaned up
377
+ end
378
+ @tasks << task if task
379
+ end
380
+
381
+
240
382
  # Waits for the send queue to drain.
241
383
  #
242
384
  # @param timeout [Numeric, nil] max seconds to wait (nil = forever)
@@ -245,7 +387,8 @@ module OMQ
245
387
  return unless @routing.respond_to?(:send_queue)
246
388
  deadline = timeout ? Async::Clock.now + timeout : nil
247
389
 
248
- until @routing.send_queue.empty?
390
+ until @routing.send_queue.empty? &&
391
+ (!@routing.respond_to?(:send_pump_idle?) || @routing.send_pump_idle?)
249
392
  if deadline
250
393
  remaining = deadline - Async::Clock.now
251
394
  break if remaining <= 0
@@ -254,12 +397,21 @@ module OMQ
254
397
  end
255
398
  end
256
399
 
257
- def setup_connection(io, as_server:, endpoint: nil)
400
+
401
+ # Performs the ZMTP handshake, starts heartbeating, and registers
402
+ # the new connection with the routing strategy.
403
+ #
404
+ # @param io [#read, #write, #close] underlying transport stream
405
+ # @param as_server [Boolean] whether we are the ZMTP server side
406
+ # @param endpoint [String, nil] endpoint for reconnection tracking
407
+ # @param done [Async::Promise, nil] resolved when the connection is lost
408
+ #
409
+ def setup_connection(io, as_server:, endpoint: nil, done: nil)
258
410
  conn = Connection.new(
259
411
  io,
260
- socket_type: @socket_type.to_s,
261
- identity: @options.identity,
262
- as_server: as_server,
412
+ socket_type: @socket_type.to_s,
413
+ identity: @options.identity,
414
+ as_server: as_server,
263
415
  mechanism: @options.mechanism,
264
416
  heartbeat_interval: @options.heartbeat_interval,
265
417
  heartbeat_ttl: @options.heartbeat_ttl,
@@ -270,12 +422,21 @@ module OMQ
270
422
  conn.start_heartbeat
271
423
  @connections << conn
272
424
  @connection_endpoints[conn] = endpoint if endpoint
425
+ @connection_promises[conn] = done if done
273
426
  @routing.connection_added(conn)
427
+ @peer_connected.resolve(conn)
274
428
  rescue ProtocolError, *CONNECTION_LOST
275
429
  conn&.close
276
430
  raise
277
431
  end
278
432
 
433
+
434
+ # Spawns a background task that reconnects to the given endpoint
435
+ # with exponential back-off based on the reconnect_interval option.
436
+ #
437
+ # @param endpoint [String] endpoint to reconnect to
438
+ # @param delay [Numeric, nil] initial delay in seconds (defaults to reconnect_interval)
439
+ #
279
440
  def schedule_reconnect(endpoint, delay: nil)
280
441
  ri = @options.reconnect_interval
281
442
  if ri.is_a?(Range)
@@ -286,7 +447,7 @@ module OMQ
286
447
  max_delay = nil
287
448
  end
288
449
 
289
- @tasks << Reactor.spawn_pump do
450
+ @tasks << Reactor.spawn_pump(annotation: "reconnect #{endpoint}") do
290
451
  loop do
291
452
  break if @closed
292
453
  sleep delay if delay > 0
@@ -314,6 +475,7 @@ module OMQ
314
475
  end
315
476
  end
316
477
 
478
+
317
479
  def extract_tcp_port(endpoint)
318
480
  return nil unless endpoint&.start_with?("tcp://")
319
481
  port = endpoint.split(":").last.to_i
@@ -24,13 +24,14 @@ module OMQ
24
24
  @heartbeat_interval = nil # seconds, nil = disabled
25
25
  @heartbeat_ttl = nil # seconds, nil = use heartbeat_interval
26
26
  @heartbeat_timeout = nil # seconds, nil = use heartbeat_interval
27
- @max_message_size = nil # bytes, nil = unlimited
28
- @mechanism = Mechanism::Null.new
27
+ @max_message_size = nil # bytes, nil = unlimited
28
+ @conflate = false
29
+ @mechanism = Mechanism::Null.new
29
30
  end
30
31
 
31
32
  attr_accessor :send_hwm, :recv_hwm,
32
33
  :linger, :identity,
33
- :router_mandatory,
34
+ :router_mandatory, :conflate,
34
35
  :read_timeout, :write_timeout,
35
36
  :reconnect_interval,
36
37
  :heartbeat_interval, :heartbeat_ttl, :heartbeat_timeout,
@@ -24,13 +24,13 @@ module OMQ
24
24
  #
25
25
  # @return [#stop] a stoppable handle
26
26
  #
27
- def spawn_pump(&block)
27
+ def spawn_pump(annotation: nil, &block)
28
28
  if Async::Task.current?
29
- Async(transient: true, &block)
29
+ Async(transient: true, annotation: annotation, &block)
30
30
  else
31
31
  handle = PumpHandle.new
32
32
  ensure_started
33
- @work_queue.push([:spawn, block, handle])
33
+ @work_queue.push([:spawn, block, handle, annotation])
34
34
  handle
35
35
  end
36
36
  end
@@ -81,6 +81,10 @@ module OMQ
81
81
 
82
82
  private
83
83
 
84
+ # Runs the shared Async reactor loop, dispatching work items.
85
+ #
86
+ # @param ready [Thread::Queue] signaled once the reactor is accepting work
87
+ #
84
88
  def run_reactor(ready)
85
89
  Async do |task|
86
90
  ready.push(true)
@@ -88,8 +92,8 @@ module OMQ
88
92
  item = @work_queue.dequeue
89
93
  case item[0]
90
94
  when :spawn
91
- _, block, handle = item
92
- async_task = task.async(transient: true, &block)
95
+ _, block, handle, annotation = item
96
+ async_task = task.async(transient: true, annotation: annotation, &block)
93
97
  handle.task = async_task
94
98
  when :run
95
99
  _, block, result_queue = item
@@ -113,6 +117,7 @@ module OMQ
113
117
  #
114
118
  attr_accessor :task
115
119
 
120
+
116
121
  # Stops the pump task.
117
122
  #
118
123
  # @return [void]
@@ -14,7 +14,8 @@ module OMQ
14
14
  @connection = nil
15
15
  @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
16
16
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
17
- @tasks = []
17
+ @tasks = []
18
+ @send_pump_idle = true
18
19
  end
19
20
 
20
21
  # @return [Async::LimitedQueue]
@@ -56,10 +57,15 @@ module OMQ
56
57
 
57
58
  private
58
59
 
60
+ def send_pump_idle? = @send_pump_idle
61
+
62
+
59
63
  def start_send_pump(conn)
60
- @send_pump = Reactor.spawn_pump do
64
+ @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
61
65
  loop do
66
+ @send_pump_idle = true
62
67
  batch = [@send_queue.dequeue]
68
+ @send_pump_idle = false
63
69
  Routing.drain_send_queue(@send_queue, batch)
64
70
  batch.each { |parts| conn.write_message(parts) }
65
71
  conn.flush
@@ -12,6 +12,8 @@ module OMQ
12
12
  # their #initialize.
13
13
  #
14
14
  module FanOut
15
+ attr_reader :subscriber_joined
16
+
15
17
  private
16
18
 
17
19
  def init_fan_out(engine)
@@ -19,6 +21,9 @@ module OMQ
19
21
  @subscriptions = {} # connection => Set of prefixes
20
22
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
21
23
  @send_pump_started = false
24
+ @send_pump_idle = true
25
+ @conflate = engine.options.conflate
26
+ @subscriber_joined = Async::Promise.new
22
27
  end
23
28
 
24
29
  # @return [Boolean] whether the connection is subscribed to the topic
@@ -38,6 +43,7 @@ module OMQ
38
43
  #
39
44
  def on_subscribe(conn, prefix)
40
45
  @subscriptions[conn] << prefix
46
+ @subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
41
47
  end
42
48
 
43
49
  # Called when a cancel command is received from a peer.
@@ -50,23 +56,48 @@ module OMQ
50
56
  @subscriptions[conn]&.delete(prefix)
51
57
  end
52
58
 
59
+ # @return [Boolean] true when the send pump is idle (not sending a batch)
60
+ def send_pump_idle? = @send_pump_idle
61
+
62
+
53
63
  def start_send_pump
54
64
  @send_pump_started = true
55
- @tasks << Reactor.spawn_pump do
65
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
56
66
  loop do
67
+ @send_pump_idle = true
57
68
  batch = [@send_queue.dequeue]
69
+ @send_pump_idle = false
58
70
  Routing.drain_send_queue(@send_queue, batch)
59
71
 
60
72
  written = Set.new
61
- batch.each do |parts|
62
- topic = parts.first || "".b
63
- @connections.each do |conn|
64
- next unless subscribed?(conn, topic)
73
+
74
+ if @conflate
75
+ # Keep only the last matching message per connection.
76
+ latest = {} # conn => parts
77
+ batch.each do |parts|
78
+ topic = parts.first || "".b
79
+ @connections.each do |conn|
80
+ next unless subscribed?(conn, topic)
81
+ latest[conn] = parts
82
+ end
83
+ end
84
+ latest.each do |conn, parts|
65
85
  begin
66
86
  conn.write_message(parts)
67
87
  written << conn
68
88
  rescue *ZMTP::CONNECTION_LOST
69
- # connection dead — will be cleaned up
89
+ end
90
+ end
91
+ else
92
+ batch.each do |parts|
93
+ topic = parts.first || "".b
94
+ @connections.each do |conn|
95
+ next unless subscribed?(conn, topic)
96
+ begin
97
+ conn.write_message(parts)
98
+ written << conn
99
+ rescue *ZMTP::CONNECTION_LOST
100
+ end
70
101
  end
71
102
  end
72
103
  end
@@ -74,14 +105,13 @@ module OMQ
74
105
  written.each do |conn|
75
106
  conn.flush
76
107
  rescue *ZMTP::CONNECTION_LOST
77
- # connection dead — will be cleaned up
78
108
  end
79
109
  end
80
110
  end
81
111
  end
82
112
 
83
113
  def start_subscription_listener(conn)
84
- @tasks << Reactor.spawn_pump do
114
+ @tasks << Reactor.spawn_pump(annotation: "recv pump") do
85
115
  loop do
86
116
  frame = conn.read_frame
87
117
  next unless frame.command?
@@ -17,7 +17,8 @@ module OMQ
17
17
  @connection = nil
18
18
  @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
19
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
20
- @tasks = []
20
+ @tasks = []
21
+ @send_pump_idle = true
21
22
  end
22
23
 
23
24
  # @return [Async::LimitedQueue]
@@ -59,10 +60,15 @@ module OMQ
59
60
 
60
61
  private
61
62
 
63
+ def send_pump_idle? = @send_pump_idle
64
+
65
+
62
66
  def start_send_pump(conn)
63
- @send_pump = Reactor.spawn_pump do
67
+ @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
64
68
  loop do
69
+ @send_pump_idle = true
65
70
  batch = [@send_queue.dequeue]
71
+ @send_pump_idle = false
66
72
  Routing.drain_send_queue(@send_queue, batch)
67
73
  batch.each { |parts| conn.write_message(parts) }
68
74
  conn.flush
@@ -21,6 +21,7 @@ module OMQ
21
21
  @connections_by_routing_id = {}
22
22
  @tasks = []
23
23
  @send_pump_started = false
24
+ @send_pump_idle = true
24
25
  end
25
26
 
26
27
  # @return [Async::LimitedQueue]
@@ -59,11 +60,16 @@ module OMQ
59
60
 
60
61
  private
61
62
 
63
+ def send_pump_idle? = @send_pump_idle
64
+
65
+
62
66
  def start_send_pump
63
67
  @send_pump_started = true
64
- @tasks << Reactor.spawn_pump do
68
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
65
69
  loop do
70
+ @send_pump_idle = true
66
71
  batch = [@send_queue.dequeue]
72
+ @send_pump_idle = false
67
73
  Routing.drain_send_queue(@send_queue, batch)
68
74
 
69
75
  written = Set.new