omq 0.12.0 → 0.13.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.
data/lib/omq/engine.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "async"
4
+ require_relative "engine/recv_pump"
5
+ require_relative "engine/heartbeat"
6
+ require_relative "engine/reconnect"
7
+ require_relative "engine/connection_setup"
4
8
 
5
9
  module OMQ
6
10
  # Per-socket orchestrator.
@@ -19,6 +23,12 @@ module OMQ
19
23
  attr_reader :transports
20
24
  end
21
25
 
26
+ # Per-connection metadata: the endpoint it was established on and an
27
+ # optional Promise resolved when the connection is lost (used by
28
+ # {#spawn_connection} to await connection teardown).
29
+ #
30
+ ConnectionRecord = Data.define(:endpoint, :done)
31
+
22
32
 
23
33
  # @return [Symbol] socket type (e.g. :REQ, :PAIR)
24
34
  #
@@ -49,33 +59,32 @@ module OMQ
49
59
  # @param options [Options]
50
60
  #
51
61
  def initialize(socket_type, options)
52
- @socket_type = socket_type
53
- @options = options
54
- @routing = Routing.for(socket_type).new(self)
55
- @connections = []
56
- @connection_endpoints = {} # connection => endpoint (for reconnection)
57
- @connected_endpoints = [] # endpoints we connected to (not bound)
58
- @listeners = []
59
- @tasks = []
60
- @closed = false
61
- @closing = false
62
- @last_endpoint = nil
63
- @last_tcp_port = nil
64
- @peer_connected = Async::Promise.new
65
- @all_peers_gone = Async::Promise.new
66
- @reconnect_enabled = true
67
- @parent_task = nil
68
- @on_io_thread = false
69
- @connection_promises = {} # connection => Async::Promise
70
- @fatal_error = nil
71
- @monitor_queue = nil
72
- end
73
-
74
-
75
- attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
62
+ @socket_type = socket_type
63
+ @options = options
64
+ @routing = Routing.for(socket_type).new(self)
65
+ @connections = {} # connection => ConnectionRecord
66
+ @dialed = Set.new # endpoints we called connect() on (reconnect intent)
67
+ @listeners = []
68
+ @tasks = []
69
+ @state = :open
70
+ @last_endpoint = nil
71
+ @last_tcp_port = nil
72
+ @peer_connected = Async::Promise.new
73
+ @all_peers_gone = Async::Promise.new
74
+ @reconnect_enabled = true
75
+ @parent_task = nil
76
+ @on_io_thread = false
77
+ @fatal_error = nil
78
+ @monitor_queue = nil
79
+ end
80
+
81
+
82
+ attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task, :tasks
76
83
 
77
84
  attr_writer :reconnect_enabled, :monitor_queue
78
85
 
86
+ def closed? = @state == :closed
87
+
79
88
  # Optional proc that wraps new connections (e.g. for serialization).
80
89
  # Called with the raw connection; must return the (possibly wrapped) connection.
81
90
  #
@@ -126,7 +135,7 @@ module OMQ
126
135
  def connect(endpoint)
127
136
  freeze_error_lists!
128
137
  validate_endpoint!(endpoint)
129
- @connected_endpoints << endpoint
138
+ @dialed.add(endpoint)
130
139
  if endpoint.start_with?("inproc://")
131
140
  # Inproc connect is synchronous and instant
132
141
  transport = transport_for(endpoint)
@@ -146,14 +155,8 @@ module OMQ
146
155
  # @return [void]
147
156
  #
148
157
  def disconnect(endpoint)
149
- @connected_endpoints.delete(endpoint)
150
- conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
151
- conns.each do |conn|
152
- @connection_endpoints.delete(conn)
153
- @connections.delete(conn)
154
- @routing.connection_removed(conn)
155
- conn.close
156
- end
158
+ @dialed.delete(endpoint)
159
+ close_connections_at(endpoint)
157
160
  end
158
161
 
159
162
 
@@ -168,15 +171,7 @@ module OMQ
168
171
  return unless listener
169
172
  listener.stop
170
173
  @listeners.delete(listener)
171
-
172
- # Close connections accepted on this endpoint
173
- conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
174
- conns.each do |conn|
175
- @connection_endpoints.delete(conn)
176
- @connections.delete(conn)
177
- @routing.connection_removed(conn)
178
- conn.close
179
- end
174
+ close_connections_at(endpoint)
180
175
  end
181
176
 
182
177
 
@@ -211,8 +206,7 @@ module OMQ
211
206
  #
212
207
  def connection_ready(pipe, endpoint: nil)
213
208
  pipe = @connection_wrapper.call(pipe) if @connection_wrapper
214
- @connections << pipe
215
- @connection_endpoints[pipe] = endpoint if endpoint
209
+ @connections[pipe] = ConnectionRecord.new(endpoint: endpoint, done: nil)
216
210
  @routing.connection_added(pipe)
217
211
  @peer_connected.resolve(pipe)
218
212
  emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
@@ -273,77 +267,17 @@ module OMQ
273
267
  end
274
268
 
275
269
 
276
- # Starts a recv pump for a connection, or wires the inproc
277
- # fast path when the connection is a DirectPipe.
278
- #
279
- # @param conn [Connection, Transport::Inproc::DirectPipe]
280
- # Starts a recv pump that dequeues messages from a connection
281
- # and enqueues them into the routing strategy's recv queue.
282
- #
283
- # When a block is given, each message is yielded for transformation
284
- # before enqueueing. The block is compiled at the call site, giving
285
- # YJIT a monomorphic call per routing strategy instead of a shared
286
- # megamorphic `transform.call` dispatch.
270
+ # Starts a recv pump for a connection, or wires the inproc fast path.
287
271
  #
288
272
  # @param conn [Connection, Transport::Inproc::DirectPipe]
289
- # @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
273
+ # @param recv_queue [SignalingQueue]
290
274
  # @yield [msg] optional per-message transform
291
- # @return [#stop, nil] pump task handle, or nil for DirectPipe bypass
275
+ # @return [Async::Task, nil]
292
276
  #
293
- # Fairness limits for the recv pump. Yield to the scheduler
294
- # after reading this many messages or bytes from one connection,
295
- # whichever comes first. Prevents a fast or large-message
296
- # connection from starving slower peers.
297
- RECV_FAIRNESS_MESSAGES = 64
298
- RECV_FAIRNESS_BYTES = 1 << 20 # 1 MB
299
-
300
277
  def start_recv_pump(conn, recv_queue, &transform)
301
- if conn.is_a?(Transport::Inproc::DirectPipe) && conn.peer
302
- conn.peer.direct_recv_queue = recv_queue
303
- conn.peer.direct_recv_transform = transform
304
- return nil
305
- end
306
-
307
- if transform
308
- @parent_task.async(transient: true, annotation: "recv pump") do |task|
309
- loop do
310
- count = 0
311
- bytes = 0
312
- while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
313
- msg = conn.receive_message
314
- msg = transform.call(msg).freeze
315
- recv_queue.enqueue(msg)
316
- count += 1
317
- bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
318
- end
319
- task.yield
320
- end
321
- rescue Async::Stop
322
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
323
- connection_lost(conn)
324
- rescue => error
325
- signal_fatal_error(error)
326
- end
327
- else
328
- @parent_task.async(transient: true, annotation: "recv pump") do |task|
329
- loop do
330
- count = 0
331
- bytes = 0
332
- while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
333
- msg = conn.receive_message
334
- recv_queue.enqueue(msg)
335
- count += 1
336
- bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
337
- end
338
- task.yield
339
- end
340
- rescue Async::Stop
341
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
342
- connection_lost(conn)
343
- rescue => error
344
- signal_fatal_error(error)
345
- end
346
- end
278
+ task = RecvPump.start(@parent_task, conn, recv_queue, self, transform)
279
+ @tasks << task if task
280
+ task
347
281
  end
348
282
 
349
283
 
@@ -353,25 +287,13 @@ module OMQ
353
287
  # @return [void]
354
288
  #
355
289
  def connection_lost(connection)
356
- endpoint = @connection_endpoints.delete(connection)
357
- @connections.delete(connection)
290
+ entry = @connections.delete(connection)
358
291
  @routing.connection_removed(connection)
359
292
  connection.close
360
- emit_monitor_event(:disconnected, endpoint: endpoint)
361
-
362
- # Signal the connection task to exit.
363
- done = @connection_promises.delete(connection)
364
- done&.resolve(true)
365
-
366
- # Resolve all_peers_gone once: had peers, now have none.
367
- if @peer_connected.resolved? && @connections.empty?
368
- @all_peers_gone.resolve(true)
369
- end
370
-
371
- # Auto-reconnect if this was a connected (not bound) endpoint
372
- if endpoint && @connected_endpoints.include?(endpoint) && !@closed && !@closing && @reconnect_enabled
373
- schedule_reconnect(endpoint)
374
- end
293
+ emit_monitor_event(:disconnected, endpoint: entry&.endpoint)
294
+ entry&.done&.resolve(true)
295
+ @all_peers_gone.resolve(true) if @peer_connected.resolved? && @connections.empty?
296
+ maybe_reconnect(entry&.endpoint)
375
297
  end
376
298
 
377
299
 
@@ -380,42 +302,15 @@ module OMQ
380
302
  # @return [void]
381
303
  #
382
304
  def close
383
- return if @closed || @closing
384
- @closing = true
385
-
386
- # Stop accepting new connections but only if we already have
387
- # peers to drain to. With zero connections the listeners must
388
- # stay open so late-arriving peers can still receive queued
389
- # messages during the linger period.
390
- unless @connections.empty?
391
- @listeners.each(&:stop)
392
- @listeners.clear
393
- end
394
-
395
- # Linger: wait for send queues to drain before closing.
396
- # linger=0 → close immediately, linger=nil → wait forever.
397
- # @closed is set AFTER draining so reconnect tasks keep
398
- # running during the linger period.
399
- linger = @options.linger
400
- if linger.nil? || linger > 0
401
- drain_timeout = linger # nil = wait forever, >0 = seconds
402
- drain_send_queues(drain_timeout)
403
- end
404
-
405
- @closed = true
305
+ return unless @state == :open
306
+ @state = :closing
307
+ stop_listeners unless @connections.empty?
308
+ drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
309
+ @state = :closed
406
310
  Reactor.untrack_linger(@options.linger) if @on_io_thread
407
-
408
- # Stop any remaining listeners.
409
- @listeners.each(&:stop)
410
- @listeners.clear
411
-
412
- # Close connections — causes pump tasks to get EOFError/IOError
413
- @connections.each(&:close)
414
- @connections.clear
415
- # Stop any remaining pump tasks
416
- @routing.stop rescue nil
417
- @tasks.each { |t| t.stop rescue nil }
418
- @tasks.clear
311
+ stop_listeners
312
+ close_connections
313
+ stop_tasks
419
314
  emit_monitor_event(:closed)
420
315
  close_monitor_queue
421
316
  end
@@ -451,13 +346,13 @@ module OMQ
451
346
  # @param error [Exception]
452
347
  #
453
348
  def signal_fatal_error(error)
454
- return if @closing || @closed
349
+ return unless @state == :open
455
350
  @fatal_error = begin
456
351
  raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
457
352
  rescue => wrapped
458
353
  wrapped
459
354
  end
460
- @routing.recv_queue.enqueue(nil) rescue nil
355
+ @routing.recv_queue.push(nil) rescue nil
461
356
  @peer_connected.resolve(nil) rescue nil
462
357
  end
463
358
 
@@ -479,18 +374,24 @@ module OMQ
479
374
  end
480
375
 
481
376
 
482
- private
377
+ def emit_monitor_event(type, endpoint: nil, detail: nil)
378
+ return unless @monitor_queue
379
+ @monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
380
+ rescue Async::Stop, ClosedQueueError
381
+ end
483
382
 
383
+ def transport_for(endpoint)
384
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
385
+ self.class.transports[scheme] or
386
+ raise ArgumentError, "unsupported transport: #{endpoint}"
387
+ end
388
+
389
+ private
484
390
 
485
- # Spawns an isolated connection task as a sibling of accept/reconnect
486
- # tasks. All per-connection children (heartbeat, recv pump, reaper)
487
- # live inside this task. When the connection dies, the entire subtree
488
- # is cleaned up by Async.
489
- #
490
391
  def spawn_connection(io, as_server:, endpoint: nil)
491
392
  task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
492
393
  done = Async::Promise.new
493
- conn = setup_connection(io, as_server: as_server, endpoint: endpoint, done: done)
394
+ conn = ConnectionSetup.run(io, self, as_server: as_server, endpoint: endpoint, done: done)
494
395
  done.wait
495
396
  rescue Protocol::ZMTP::Error, *CONNECTION_LOST
496
397
  # handshake failed or connection lost — subtree cleaned up
@@ -500,154 +401,30 @@ module OMQ
500
401
  @tasks << task if task
501
402
  end
502
403
 
503
-
504
- # Waits for the send queue to drain.
505
- #
506
- # @param timeout [Numeric, nil] max seconds to wait (nil = forever)
507
- #
508
404
  def drain_send_queues(timeout)
509
- return unless @routing.respond_to?(:send_queue)
405
+ return unless @routing.respond_to?(:send_queues_drained?)
510
406
  deadline = timeout ? Async::Clock.now + timeout : nil
511
-
512
- until @routing.send_queue.empty? && @routing.send_pump_idle?
513
- if deadline
514
- remaining = deadline - Async::Clock.now
515
- break if remaining <= 0
516
- end
407
+ until @routing.send_queues_drained?
408
+ break if deadline && (deadline - Async::Clock.now) <= 0
517
409
  sleep 0.001
518
410
  end
519
411
  end
520
412
 
521
-
522
- # Performs the ZMTP handshake, starts heartbeating, and registers
523
- # the new connection with the routing strategy.
524
- #
525
- # @param io [#read, #write, #close] underlying transport stream
526
- # @param as_server [Boolean] whether we are the ZMTP server side
527
- # @param endpoint [String, nil] endpoint for reconnection tracking
528
- # @param done [Async::Promise, nil] resolved when the connection is lost
529
- #
530
- def setup_connection(io, as_server:, endpoint: nil, done: nil)
531
- conn = Protocol::ZMTP::Connection.new(
532
- io,
533
- socket_type: @socket_type.to_s,
534
- identity: @options.identity,
535
- as_server: as_server,
536
- mechanism: @options.mechanism&.dup,
537
- max_message_size: @options.max_message_size,
538
- )
539
- conn.handshake!
540
- start_heartbeat(conn)
541
- conn = @connection_wrapper.call(conn) if @connection_wrapper
542
- @connections << conn
543
- @connection_endpoints[conn] = endpoint if endpoint
544
- @connection_promises[conn] = done if done
545
- @routing.connection_added(conn)
546
- @peer_connected.resolve(conn)
547
- emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
548
- conn
549
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
550
- emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
551
- conn&.close
552
- raise
413
+ def maybe_reconnect(endpoint)
414
+ return unless endpoint && @dialed.include?(endpoint)
415
+ return unless @state == :open && @reconnect_enabled
416
+ Reconnect.schedule(endpoint, @options, @parent_task, self)
553
417
  end
554
418
 
555
-
556
- # Spawns a heartbeat task for the connection.
557
- # The connection only tracks timestamps — the engine drives the loop.
558
- #
559
- # @param conn [Connection]
560
- # @return [void]
561
- #
562
- def start_heartbeat(conn)
563
- interval = @options.heartbeat_interval
564
- return unless interval
565
-
566
- ttl = @options.heartbeat_ttl || interval
567
- timeout = @options.heartbeat_timeout || interval
568
- conn.touch_heartbeat
569
-
570
- @tasks << @parent_task.async(transient: true, annotation: "heartbeat") do
571
- loop do
572
- sleep interval
573
- conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl, context: "".b))
574
- if conn.heartbeat_expired?(timeout)
575
- conn.close
576
- break
577
- end
578
- end
579
- rescue Async::Stop
580
- rescue *CONNECTION_LOST
581
- # connection closed
582
- end
583
- end
584
-
585
-
586
- # Spawns a background task that reconnects to the given endpoint
587
- # with exponential back-off based on the reconnect_interval option.
588
- #
589
- # @param endpoint [String] endpoint to reconnect to
590
- # @param delay [Numeric, nil] initial delay in seconds (defaults to reconnect_interval)
591
- #
592
419
  def schedule_reconnect(endpoint, delay: nil)
593
- ri = @options.reconnect_interval
594
- if ri.is_a?(Range)
595
- delay ||= ri.begin
596
- max_delay = ri.end
597
- else
598
- delay ||= ri
599
- max_delay = nil
600
- end
601
-
602
- @tasks << @parent_task.async(transient: true, annotation: "reconnect #{endpoint}") do
603
- loop do
604
- break if @closed
605
- sleep delay if delay > 0
606
- break if @closed
607
- begin
608
- transport = transport_for(endpoint)
609
- transport.connect(endpoint, self)
610
- break # connected successfully
611
- rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
612
- delay = [delay * 2, max_delay].min if max_delay
613
- # After first attempt with delay: 0, use the configured interval
614
- delay = ri.is_a?(Range) ? ri.begin : ri if delay == 0
615
- emit_monitor_event(:connect_retried, endpoint: endpoint, detail: { interval: delay })
616
- end
617
- end
618
- rescue Async::Stop
619
- # normal shutdown
620
- rescue => error
621
- signal_fatal_error(error)
622
- end
420
+ Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
623
421
  end
624
422
 
625
-
626
- # Eagerly validates TCP hostnames so resolution errors fail
627
- # on connect, not silently in the background reconnect loop.
628
- # Reconnects still re-resolve (DNS may change), and transient
629
- # resolution failures during reconnect are retried with backoff.
630
- #
631
423
  def validate_endpoint!(endpoint)
632
424
  transport = transport_for(endpoint)
633
425
  transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
634
426
  end
635
427
 
636
-
637
-
638
- def transport_for(endpoint)
639
- scheme = endpoint[/\A([^:]+):\/\//, 1]
640
- self.class.transports[scheme] or
641
- raise ArgumentError, "unsupported transport: #{endpoint}"
642
- end
643
-
644
-
645
- # Delegates accept loop startup to the listener.
646
- #
647
- # Stream-based listeners (TCP, IPC, TLS, …) implement
648
- # +#start_accept_loops+. Inproc listeners do not — connections
649
- # are established synchronously during +connect+.
650
- #
651
428
  def start_accept_loops(listener)
652
429
  return unless listener.respond_to?(:start_accept_loops)
653
430
  listener.start_accept_loops(@parent_task) do |io|
@@ -655,20 +432,36 @@ module OMQ
655
432
  end
656
433
  end
657
434
 
435
+ def stop_listeners
436
+ @listeners.each(&:stop)
437
+ @listeners.clear
438
+ end
658
439
 
659
- def freeze_error_lists!
660
- return if OMQ::CONNECTION_LOST.frozen?
661
- OMQ::CONNECTION_LOST.freeze
662
- OMQ::CONNECTION_FAILED.freeze
440
+ def close_connections
441
+ @connections.each_key(&:close)
442
+ @connections.clear
663
443
  end
664
444
 
445
+ def close_connections_at(endpoint)
446
+ conns = @connections.filter_map { |conn, e| conn if e.endpoint == endpoint }
447
+ conns.each do |conn|
448
+ @connections.delete(conn)
449
+ @routing.connection_removed(conn)
450
+ conn.close
451
+ end
452
+ end
665
453
 
666
- def emit_monitor_event(type, endpoint: nil, detail: nil)
667
- return unless @monitor_queue
668
- @monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
669
- rescue Async::Stop, ClosedQueueError
454
+ def stop_tasks
455
+ @routing.stop rescue nil
456
+ @tasks.each { |t| t.stop rescue nil }
457
+ @tasks.clear
670
458
  end
671
459
 
460
+ def freeze_error_lists!
461
+ return if OMQ::CONNECTION_LOST.frozen?
462
+ OMQ::CONNECTION_LOST.freeze
463
+ OMQ::CONNECTION_FAILED.freeze
464
+ end
672
465
 
673
466
  def close_monitor_queue
674
467
  return unless @monitor_queue
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Starts a dedicated send pump for one per-connection send queue.
6
+ #
7
+ # Used by Router and Rep, which have per-connection queues but do not
8
+ # include the RoundRobin mixin.
9
+ #
10
+ class ConnSendPump
11
+ # Spawns the pump task and registers it in +tasks+.
12
+ #
13
+ # @param engine [Engine]
14
+ # @param conn [Connection]
15
+ # @param q [Async::LimitedQueue]
16
+ # @param tasks [Array]
17
+ # @return [Async::Task]
18
+ #
19
+ def self.start(engine, conn, q, tasks)
20
+ task = engine.spawn_pump_task(annotation: "send pump") do
21
+ loop do
22
+ batch = [q.dequeue]
23
+ Routing.drain_send_queue(q, batch)
24
+ batch.each { |parts| conn.write_message(parts) }
25
+ conn.flush
26
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
27
+ engine.connection_lost(conn)
28
+ break
29
+ end
30
+ end
31
+ tasks << task
32
+ task
33
+ end
34
+ end
35
+ end
36
+ end
@@ -8,36 +8,35 @@ module OMQ
8
8
  #
9
9
  class Dealer
10
10
  include RoundRobin
11
+ include FairRecv
11
12
 
12
13
  # @param engine [Engine]
13
14
  #
14
15
  def initialize(engine)
15
16
  @engine = engine
16
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
17
+ @recv_queue = FairQueue.new
17
18
  @tasks = []
18
19
  init_round_robin(engine)
19
20
  end
20
21
 
21
- # @return [Async::LimitedQueue]
22
+ # @return [FairQueue]
22
23
  #
23
- attr_reader :recv_queue, :send_queue
24
+ attr_reader :recv_queue
24
25
 
25
26
  # @param connection [Connection]
26
27
  #
27
28
  def connection_added(connection)
28
29
  @connections << connection
29
- signal_connection_available
30
- update_direct_pipe
31
- task = @engine.start_recv_pump(connection, @recv_queue)
32
- @tasks << task if task
33
- start_send_pump unless @send_pump_started
30
+ add_fair_recv_connection(connection)
31
+ add_round_robin_send_connection(connection)
34
32
  end
35
33
 
36
34
  # @param connection [Connection]
37
35
  #
38
36
  def connection_removed(connection)
39
37
  @connections.delete(connection)
40
- update_direct_pipe
38
+ @recv_queue.remove_queue(connection)
39
+ remove_round_robin_send_connection(connection)
41
40
  end
42
41
 
43
42
  # @param parts [Array<String>]
@@ -51,7 +50,6 @@ module OMQ
51
50
  @tasks.each(&:stop)
52
51
  @tasks.clear
53
52
  end
54
-
55
53
  end
56
54
  end
57
55
  end