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
data/lib/omq/engine.rb CHANGED
@@ -1,6 +1,11 @@
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"
8
+ require_relative "engine/maintenance"
4
9
 
5
10
  module OMQ
6
11
  # Per-socket orchestrator.
@@ -20,6 +25,13 @@ module OMQ
20
25
  end
21
26
 
22
27
 
28
+ # Per-connection metadata: the endpoint it was established on and an
29
+ # optional Promise resolved when the connection is lost (used by
30
+ # {#spawn_connection} to await connection teardown).
31
+ #
32
+ ConnectionRecord = Data.define(:endpoint, :done)
33
+
34
+
23
35
  # @return [Symbol] socket type (e.g. :REQ, :PAIR)
24
36
  #
25
37
  attr_reader :socket_type
@@ -49,33 +61,45 @@ module OMQ
49
61
  # @param options [Options]
50
62
  #
51
63
  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
64
+ @socket_type = socket_type
65
+ @options = options
66
+ @routing = Routing.for(socket_type).new(self)
67
+ @connections = {} # connection => ConnectionRecord
68
+ @dialed = Set.new # endpoints we called connect() on (reconnect intent)
69
+ @listeners = []
70
+ @tasks = []
71
+ @state = :open
72
+ @last_endpoint = nil
73
+ @last_tcp_port = nil
74
+ @peer_connected = Async::Promise.new
75
+ @all_peers_gone = Async::Promise.new
76
+ @reconnect_enabled = true
77
+ @parent_task = nil
78
+ @on_io_thread = false
79
+ @fatal_error = nil
80
+ @monitor_queue = nil
81
+ end
82
+
76
83
 
84
+ # @return [Async::Promise] resolves when first peer completes handshake
85
+ # @return [Async::Promise] resolves when all peers disconnect (after having had peers)
86
+ # @return [Hash{Connection => ConnectionRecord}] active connections
87
+ # @return [Async::Task, nil] root task for spawning subtrees
88
+ # @return [Array<Async::Task>] background tasks (pumps, heartbeat, reconnect)
89
+ #
90
+ attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task, :tasks
91
+
92
+ # @!attribute [w] reconnect_enabled
93
+ # @param value [Boolean] enable or disable auto-reconnect
94
+ # @!attribute [w] monitor_queue
95
+ # @param value [Async::Queue, nil] queue for monitor events
96
+ #
77
97
  attr_writer :reconnect_enabled, :monitor_queue
78
98
 
99
+ # @return [Boolean] true if the engine has been closed
100
+ #
101
+ def closed? = @state == :closed
102
+
79
103
  # Optional proc that wraps new connections (e.g. for serialization).
80
104
  # Called with the raw connection; must return the (possibly wrapped) connection.
81
105
  #
@@ -126,7 +150,7 @@ module OMQ
126
150
  def connect(endpoint)
127
151
  freeze_error_lists!
128
152
  validate_endpoint!(endpoint)
129
- @connected_endpoints << endpoint
153
+ @dialed.add(endpoint)
130
154
  if endpoint.start_with?("inproc://")
131
155
  # Inproc connect is synchronous and instant
132
156
  transport = transport_for(endpoint)
@@ -146,14 +170,8 @@ module OMQ
146
170
  # @return [void]
147
171
  #
148
172
  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
173
+ @dialed.delete(endpoint)
174
+ close_connections_at(endpoint)
157
175
  end
158
176
 
159
177
 
@@ -168,15 +186,7 @@ module OMQ
168
186
  return unless listener
169
187
  listener.stop
170
188
  @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
189
+ close_connections_at(endpoint)
180
190
  end
181
191
 
182
192
 
@@ -211,8 +221,7 @@ module OMQ
211
221
  #
212
222
  def connection_ready(pipe, endpoint: nil)
213
223
  pipe = @connection_wrapper.call(pipe) if @connection_wrapper
214
- @connections << pipe
215
- @connection_endpoints[pipe] = endpoint if endpoint
224
+ @connections[pipe] = ConnectionRecord.new(endpoint: endpoint, done: nil)
216
225
  @routing.connection_added(pipe)
217
226
  @peer_connected.resolve(pipe)
218
227
  emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
@@ -273,77 +282,17 @@ module OMQ
273
282
  end
274
283
 
275
284
 
276
- # Starts a recv pump for a connection, or wires the inproc
277
- # fast path when the connection is a DirectPipe.
285
+ # Starts a recv pump for a connection, or wires the inproc fast path.
278
286
  #
279
287
  # @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.
287
- #
288
- # @param conn [Connection, Transport::Inproc::DirectPipe]
289
- # @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
288
+ # @param recv_queue [SignalingQueue]
290
289
  # @yield [msg] optional per-message transform
291
- # @return [#stop, nil] pump task handle, or nil for DirectPipe bypass
290
+ # @return [Async::Task, nil]
292
291
  #
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
292
  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
293
+ task = RecvPump.start(@parent_task, conn, recv_queue, self, transform)
294
+ @tasks << task if task
295
+ task
347
296
  end
348
297
 
349
298
 
@@ -353,25 +302,13 @@ module OMQ
353
302
  # @return [void]
354
303
  #
355
304
  def connection_lost(connection)
356
- endpoint = @connection_endpoints.delete(connection)
357
- @connections.delete(connection)
305
+ entry = @connections.delete(connection)
358
306
  @routing.connection_removed(connection)
359
307
  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
308
+ emit_monitor_event(:disconnected, endpoint: entry&.endpoint)
309
+ entry&.done&.resolve(true)
310
+ @all_peers_gone.resolve(true) if @peer_connected.resolved? && @connections.empty?
311
+ maybe_reconnect(entry&.endpoint)
375
312
  end
376
313
 
377
314
 
@@ -380,42 +317,15 @@ module OMQ
380
317
  # @return [void]
381
318
  #
382
319
  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
320
+ return unless @state == :open
321
+ @state = :closing
322
+ stop_listeners unless @connections.empty?
323
+ drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
324
+ @state = :closed
406
325
  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
326
+ stop_listeners
327
+ close_connections
328
+ stop_tasks
419
329
  emit_monitor_event(:closed)
420
330
  close_monitor_queue
421
331
  end
@@ -451,13 +361,13 @@ module OMQ
451
361
  # @param error [Exception]
452
362
  #
453
363
  def signal_fatal_error(error)
454
- return if @closing || @closed
364
+ return unless @state == :open
455
365
  @fatal_error = begin
456
366
  raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
457
367
  rescue => wrapped
458
368
  wrapped
459
369
  end
460
- @routing.recv_queue.enqueue(nil) rescue nil
370
+ @routing.recv_queue.push(nil) rescue nil
461
371
  @peer_connected.resolve(nil) rescue nil
462
372
  end
463
373
 
@@ -476,21 +386,42 @@ module OMQ
476
386
  @on_io_thread = true
477
387
  Reactor.track_linger(@options.linger)
478
388
  end
389
+ Maintenance.start(@parent_task, @options.mechanism, @tasks)
479
390
  end
480
391
 
481
392
 
482
- private
393
+ # Emits a lifecycle event to the monitor queue, if one is attached.
394
+ #
395
+ # @param type [Symbol] event type (e.g. :listening, :connected, :disconnected)
396
+ # @param endpoint [String, nil] the endpoint involved
397
+ # @param detail [Hash, nil] extra context
398
+ # @return [void]
399
+ #
400
+ def emit_monitor_event(type, endpoint: nil, detail: nil)
401
+ return unless @monitor_queue
402
+ @monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
403
+ rescue Async::Stop, ClosedQueueError
404
+ end
483
405
 
484
406
 
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.
407
+ # Looks up the transport module for an endpoint URI.
408
+ #
409
+ # @param endpoint [String] endpoint URI (e.g. "tcp://...", "inproc://...")
410
+ # @return [Module] the transport module
411
+ # @raise [ArgumentError] if the scheme is not registered
489
412
  #
413
+ def transport_for(endpoint)
414
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
415
+ self.class.transports[scheme] or
416
+ raise ArgumentError, "unsupported transport: #{endpoint}"
417
+ end
418
+
419
+ private
420
+
490
421
  def spawn_connection(io, as_server:, endpoint: nil)
491
422
  task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
492
423
  done = Async::Promise.new
493
- conn = setup_connection(io, as_server: as_server, endpoint: endpoint, done: done)
424
+ conn = ConnectionSetup.run(io, self, as_server: as_server, endpoint: endpoint, done: done)
494
425
  done.wait
495
426
  rescue Protocol::ZMTP::Error, *CONNECTION_LOST
496
427
  # handshake failed or connection lost — subtree cleaned up
@@ -501,153 +432,34 @@ module OMQ
501
432
  end
502
433
 
503
434
 
504
- # Waits for the send queue to drain.
505
- #
506
- # @param timeout [Numeric, nil] max seconds to wait (nil = forever)
507
- #
508
435
  def drain_send_queues(timeout)
509
- return unless @routing.respond_to?(:send_queue)
436
+ return unless @routing.respond_to?(:send_queues_drained?)
510
437
  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
438
+ until @routing.send_queues_drained?
439
+ break if deadline && (deadline - Async::Clock.now) <= 0
517
440
  sleep 0.001
518
441
  end
519
442
  end
520
443
 
521
444
 
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
553
- end
554
-
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
445
+ def maybe_reconnect(endpoint)
446
+ return unless endpoint && @dialed.include?(endpoint)
447
+ return unless @state == :open && @reconnect_enabled
448
+ Reconnect.schedule(endpoint, @options, @parent_task, self)
583
449
  end
584
450
 
585
451
 
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
452
  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
453
+ Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
623
454
  end
624
455
 
625
456
 
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
457
  def validate_endpoint!(endpoint)
632
458
  transport = transport_for(endpoint)
633
459
  transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
634
460
  end
635
461
 
636
462
 
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
463
  def start_accept_loops(listener)
652
464
  return unless listener.respond_to?(:start_accept_loops)
653
465
  listener.start_accept_loops(@parent_task) do |io|
@@ -656,6 +468,35 @@ module OMQ
656
468
  end
657
469
 
658
470
 
471
+ def stop_listeners
472
+ @listeners.each(&:stop)
473
+ @listeners.clear
474
+ end
475
+
476
+
477
+ def close_connections
478
+ @connections.each_key(&:close)
479
+ @connections.clear
480
+ end
481
+
482
+
483
+ def close_connections_at(endpoint)
484
+ conns = @connections.filter_map { |conn, e| conn if e.endpoint == endpoint }
485
+ conns.each do |conn|
486
+ @connections.delete(conn)
487
+ @routing.connection_removed(conn)
488
+ conn.close
489
+ end
490
+ end
491
+
492
+
493
+ def stop_tasks
494
+ @routing.stop rescue nil
495
+ @tasks.each { |t| t.stop rescue nil }
496
+ @tasks.clear
497
+ end
498
+
499
+
659
500
  def freeze_error_lists!
660
501
  return if OMQ::CONNECTION_LOST.frozen?
661
502
  OMQ::CONNECTION_LOST.freeze
@@ -663,19 +504,13 @@ module OMQ
663
504
  end
664
505
 
665
506
 
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
670
- end
671
-
672
-
673
507
  def close_monitor_queue
674
508
  return unless @monitor_queue
675
509
  @monitor_queue.push(nil)
676
510
  end
677
511
  end
678
512
 
513
+
679
514
  # Register built-in transports.
680
515
  Engine.transports["tcp"] = Transport::TCP
681
516
  Engine.transports["ipc"] = Transport::IPC
data/lib/omq/options.rb CHANGED
@@ -9,6 +9,7 @@ module OMQ
9
9
  class Options
10
10
  DEFAULT_HWM = 1000
11
11
 
12
+
12
13
  # @param linger [Integer] linger period in seconds (default 0)
13
14
  #
14
15
  def initialize(linger: 0)
@@ -30,6 +31,40 @@ module OMQ
30
31
  @qos = 0 # 0 = fire-and-forget, 1 = at-least-once (see omq-qos gem)
31
32
  end
32
33
 
34
+
35
+ # @!attribute send_hwm
36
+ # @return [Integer] send high water mark (default 1000, 0 = unbounded)
37
+ # @!attribute recv_hwm
38
+ # @return [Integer] receive high water mark (default 1000, 0 = unbounded)
39
+ # @!attribute linger
40
+ # @return [Integer, nil] linger period in seconds (nil = wait forever, 0 = immediate)
41
+ # @!attribute identity
42
+ # @return [String] socket identity for ROUTER addressing (default "")
43
+ # @!attribute router_mandatory
44
+ # @return [Boolean] raise on unroutable messages (default false)
45
+ # @!attribute conflate
46
+ # @return [Boolean] keep only the latest message per topic (default false)
47
+ # @!attribute read_timeout
48
+ # @return [Numeric, nil] read timeout in seconds (nil = no timeout)
49
+ # @!attribute write_timeout
50
+ # @return [Numeric, nil] write timeout in seconds (nil = no timeout)
51
+ # @!attribute reconnect_interval
52
+ # @return [Numeric, Range] reconnect interval in seconds, or Range for exponential backoff
53
+ # @!attribute heartbeat_interval
54
+ # @return [Numeric, nil] PING interval in seconds (nil = disabled)
55
+ # @!attribute heartbeat_ttl
56
+ # @return [Numeric, nil] TTL advertised in PING (nil = use heartbeat_interval)
57
+ # @!attribute heartbeat_timeout
58
+ # @return [Numeric, nil] time without traffic before closing (nil = use heartbeat_interval)
59
+ # @!attribute max_message_size
60
+ # @return [Integer, nil] maximum message size in bytes
61
+ # @!attribute on_mute
62
+ # @return [Symbol] mute strategy (:block, :drop_newest, :drop_oldest)
63
+ # @!attribute mechanism
64
+ # @return [Protocol::ZMTP::Mechanism::Null, Protocol::ZMTP::Mechanism::Curve] security mechanism
65
+ # @!attribute qos
66
+ # @return [Integer] quality of service level (0 = fire-and-forget)
67
+ #
33
68
  attr_accessor :send_hwm, :recv_hwm,
34
69
  :linger, :identity,
35
70
  :router_mandatory, :conflate,
@@ -41,10 +76,19 @@ module OMQ
41
76
  :mechanism,
42
77
  :qos
43
78
 
79
+ # @return [Boolean] true if router_mandatory is set
44
80
  alias_method :router_mandatory?, :router_mandatory
81
+
82
+ # @return [Numeric, nil] alias for #read_timeout
45
83
  alias_method :recv_timeout, :read_timeout
84
+
85
+ # @param val [Numeric, nil] alias for #read_timeout=
46
86
  alias_method :recv_timeout=, :read_timeout=
87
+
88
+ # @return [Numeric, nil] alias for #write_timeout
47
89
  alias_method :send_timeout, :write_timeout
90
+
91
+ # @param val [Numeric, nil] alias for #write_timeout=
48
92
  alias_method :send_timeout=, :write_timeout=
49
93
  end
50
94
  end
data/lib/omq/pair.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
+ # PAIR socket — exclusive 1-to-1 bidirectional communication.
5
+ #
4
6
  class PAIR < Socket
5
7
  include Readable
6
8
  include Writable
7
9
 
10
+ # @param endpoints [String, nil] endpoint to bind/connect
11
+ # @param linger [Integer] linger period in seconds
12
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
13
+ #
8
14
  def initialize(endpoints = nil, linger: 0, backend: nil)
9
15
  _init_engine(:PAIR, linger: linger, backend: backend)
10
16
  _attach(endpoints, default: :connect)