omq 0.22.1 → 0.24.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +162 -0
  3. data/README.md +17 -21
  4. data/lib/omq/channel.rb +35 -0
  5. data/lib/omq/client_server.rb +72 -0
  6. data/lib/omq/constants.rb +68 -0
  7. data/lib/omq/engine/connection_lifecycle.rb +22 -8
  8. data/lib/omq/engine/heartbeat.rb +3 -4
  9. data/lib/omq/engine/maintenance.rb +4 -5
  10. data/lib/omq/engine/reconnect.rb +12 -11
  11. data/lib/omq/engine/recv_pump.rb +10 -10
  12. data/lib/omq/engine/socket_lifecycle.rb +26 -9
  13. data/lib/omq/engine.rb +202 -90
  14. data/lib/omq/peer.rb +49 -0
  15. data/lib/omq/pub_sub.rb +2 -2
  16. data/lib/omq/radio_dish.rb +122 -0
  17. data/lib/omq/reactor.rb +14 -5
  18. data/lib/omq/readable.rb +5 -1
  19. data/lib/omq/routing/channel.rb +110 -0
  20. data/lib/omq/routing/client.rb +70 -0
  21. data/lib/omq/routing/conn_send_pump.rb +5 -8
  22. data/lib/omq/routing/dealer.rb +3 -15
  23. data/lib/omq/routing/dish.rb +94 -0
  24. data/lib/omq/routing/fan_out.rb +12 -16
  25. data/lib/omq/routing/gather.rb +60 -0
  26. data/lib/omq/routing/pair.rb +7 -26
  27. data/lib/omq/routing/peer.rb +95 -0
  28. data/lib/omq/routing/pub.rb +2 -13
  29. data/lib/omq/routing/pull.rb +3 -15
  30. data/lib/omq/routing/push.rb +4 -13
  31. data/lib/omq/routing/radio.rb +187 -0
  32. data/lib/omq/routing/rep.rb +5 -19
  33. data/lib/omq/routing/req.rb +6 -18
  34. data/lib/omq/routing/round_robin.rb +15 -19
  35. data/lib/omq/routing/router.rb +5 -19
  36. data/lib/omq/routing/scatter.rb +76 -0
  37. data/lib/omq/routing/server.rb +90 -0
  38. data/lib/omq/routing/sub.rb +3 -15
  39. data/lib/omq/routing/xpub.rb +2 -13
  40. data/lib/omq/routing/xsub.rb +8 -25
  41. data/lib/omq/scatter_gather.rb +56 -0
  42. data/lib/omq/socket.rb +8 -23
  43. data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +26 -24
  44. data/lib/omq/transport/inproc.rb +22 -14
  45. data/lib/omq/transport/ipc.rb +41 -13
  46. data/lib/omq/transport/tcp.rb +59 -23
  47. data/lib/omq/transport/udp.rb +281 -0
  48. data/lib/omq/version.rb +1 -1
  49. data/lib/omq/writable.rb +11 -42
  50. data/lib/omq.rb +9 -64
  51. metadata +17 -3
  52. data/lib/omq/monitor_event.rb +0 -16
data/lib/omq/engine.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "async"
4
+ require "uri"
4
5
  require_relative "engine/recv_pump"
5
6
  require_relative "engine/heartbeat"
6
7
  require_relative "engine/reconnect"
@@ -37,48 +38,25 @@ module OMQ
37
38
  attr_reader :options
38
39
 
39
40
 
40
- # @return [Routing] routing strategy (created lazily on first access)
41
- #
42
- def routing
43
- @routing ||= Routing.for(@socket_type).new(self)
44
- end
45
-
46
-
47
- # @return [String, nil] last bound endpoint
41
+ # @return [Hash{String => Listener}] active listeners keyed by resolved endpoint
48
42
  #
49
- attr_reader :last_endpoint
43
+ attr_reader :listeners
50
44
 
51
45
 
52
- # @return [Integer, nil] last auto-selected TCP port
46
+ # @return [Hash{Connection => ConnectionLifecycle}] active connections
53
47
  #
54
- attr_reader :last_tcp_port
48
+ attr_reader :connections
55
49
 
56
50
 
57
- # @param socket_type [Symbol] e.g. :REQ, :REP, :PAIR
58
- # @param options [Options]
51
+ # Optional proc that wraps new connections (e.g. for serialization).
52
+ # Called with the raw connection; must return the (possibly wrapped) connection.
59
53
  #
60
- def initialize(socket_type, options)
61
- @socket_type = socket_type
62
- @options = options
63
- @routing = nil
64
- @connections = {} # connection => ConnectionLifecycle
65
- @dialed = Set.new # endpoints we called connect() on (reconnect intent)
66
- @listeners = []
67
- @tasks = []
68
- @lifecycle = SocketLifecycle.new
69
- @last_endpoint = nil
70
- @last_tcp_port = nil
71
- @fatal_error = nil
72
- @monitor_queue = nil
73
- @verbose_monitor = false
74
- end
54
+ attr_accessor :connection_wrapper
75
55
 
76
56
 
77
- # @return [Hash{Connection => ConnectionLifecycle}] active connections
78
- # @return [Array<Async::Task>] background tasks (pumps, heartbeat, reconnect)
79
57
  # @return [SocketLifecycle] socket-level state + signaling
80
58
  #
81
- attr_reader :connections, :tasks, :lifecycle
59
+ attr_reader :lifecycle
82
60
 
83
61
 
84
62
  # @!attribute [w] monitor_queue
@@ -92,21 +70,100 @@ module OMQ
92
70
  attr_accessor :verbose_monitor
93
71
 
94
72
 
73
+ # @return [Routing] routing strategy (created lazily on first access)
74
+ #
75
+ def routing
76
+ @routing ||= Routing.for(@socket_type).new(self)
77
+ end
78
+
79
+
80
+ # @param socket_type [Symbol] e.g. :REQ, :REP, :PAIR
81
+ # @param options [Options]
82
+ #
83
+ def initialize(socket_type, options)
84
+ @socket_type = socket_type
85
+ @options = options
86
+ @routing = nil
87
+ @connections = {} # connection => ConnectionLifecycle
88
+ @dialers = {} # endpoint => Dialer (reconnect intent + connect logic)
89
+ @listeners = {} # endpoint => Listener
90
+ @lifecycle = SocketLifecycle.new
91
+ @fatal_error = nil
92
+ @monitor_queue = nil
93
+ @verbose_monitor = false
94
+ end
95
+
96
+
95
97
  # Delegated to {SocketLifecycle}.
96
98
  def peer_connected = @lifecycle.peer_connected
97
99
  def all_peers_gone = @lifecycle.all_peers_gone
98
100
  def parent_task = @lifecycle.parent_task
99
101
  def barrier = @lifecycle.barrier
100
102
  def closed? = @lifecycle.closed?
103
+ def on_io_thread? = @lifecycle.on_io_thread
104
+
105
+
106
+ # Enables or disables auto-reconnect for dropped connections.
107
+ # Delegated to {SocketLifecycle}. Close paths flip this to +false+
108
+ # so a lost connection doesn't schedule a new retry after linger.
109
+ #
110
+ # @param value [Boolean]
111
+ #
101
112
  def reconnect_enabled=(value)
102
113
  @lifecycle.reconnect_enabled = value
103
114
  end
104
115
 
105
116
 
106
- # Optional proc that wraps new connections (e.g. for serialization).
107
- # Called with the raw connection; must return the (possibly wrapped) connection.
117
+ # Delegated to the routing strategy. Forwards the PUB/XPUB/SUB/XSUB
118
+ # subscription surface so callers don't have to chain through
119
+ # `engine.routing`.
108
120
  #
109
- attr_accessor :connection_wrapper
121
+ def subscriber_joined
122
+ routing.subscriber_joined
123
+ end
124
+
125
+
126
+ # Subscribes to a topic prefix on SUB/XSUB sockets. Delegates to the
127
+ # routing strategy so callers don't have to chain through
128
+ # `engine.routing`.
129
+ #
130
+ # @param prefix [String]
131
+ #
132
+ def subscribe(prefix)
133
+ routing.subscribe(prefix)
134
+ end
135
+
136
+
137
+ # Unsubscribes from a topic prefix on SUB/XSUB sockets. Delegates to
138
+ # the routing strategy so callers don't have to chain through
139
+ # `engine.routing`.
140
+ #
141
+ # @param prefix [String]
142
+ #
143
+ def unsubscribe(prefix)
144
+ routing.unsubscribe(prefix)
145
+ end
146
+
147
+
148
+ # Records the disconnect reason on the {ConnectionLifecycle} for
149
+ # +conn+, if any. Called by the recv pump rescue so the upcoming
150
+ # `:disconnected` monitor event carries an error reason, without
151
+ # exposing the internal connection map.
152
+ #
153
+ def record_disconnect_reason(conn, error)
154
+ @connections[conn]&.record_disconnect_reason(error)
155
+ end
156
+
157
+
158
+ # Returns the transport object (Dialer or Listener) for an endpoint.
159
+ # Used by {ConnectionLifecycle#ready!} to call +#wrap_connection+.
160
+ #
161
+ # @param endpoint [String]
162
+ # @return [Dialer, Listener, nil]
163
+ #
164
+ def transport_object_for(endpoint)
165
+ @dialers[endpoint] || @listeners[endpoint]
166
+ end
110
167
 
111
168
 
112
169
  # Spawns an inproc reconnect retry task under the socket's parent task.
@@ -119,7 +176,7 @@ module OMQ
119
176
  ivl = ri.is_a?(Range) ? ri.begin : ri
120
177
  ann = "inproc reconnect #{endpoint}"
121
178
 
122
- @tasks << @lifecycle.barrier.async(transient: true, annotation: ann) do
179
+ @lifecycle.barrier.async(transient: true, annotation: ann) do
123
180
  yield ivl
124
181
  rescue Async::Stop, Async::Cancel
125
182
  end
@@ -129,21 +186,20 @@ module OMQ
129
186
  # Binds to an endpoint.
130
187
  #
131
188
  # @param endpoint [String] e.g. "tcp://127.0.0.1:5555", "inproc://foo"
132
- # @return [void]
189
+ # @return [URI::Generic] resolved endpoint URI (with auto-selected port for "tcp://host:0")
133
190
  # @raise [ArgumentError] on unsupported transport
134
191
  #
135
- def bind(endpoint, parent: nil)
192
+ def bind(endpoint, parent: nil, **opts)
136
193
  OMQ.freeze_for_ractors!
137
194
  capture_parent_task(parent: parent)
138
195
  transport = transport_for(endpoint)
139
- listener = transport.bind(endpoint, self)
196
+ listener = transport.listener(endpoint, self, **opts)
140
197
 
141
198
  start_accept_loops(listener)
142
199
 
143
- @listeners << listener
144
- @last_endpoint = listener.endpoint
145
- @last_tcp_port = listener.respond_to?(:port) ? listener.port : nil
200
+ @listeners[listener.endpoint] = listener
146
201
  emit_monitor_event(:listening, endpoint: listener.endpoint)
202
+ URI.parse(listener.endpoint)
147
203
  rescue => error
148
204
  emit_monitor_event(:bind_failed, endpoint: endpoint, detail: { error: error })
149
205
  raise
@@ -153,23 +209,26 @@ module OMQ
153
209
  # Connects to an endpoint.
154
210
  #
155
211
  # @param endpoint [String]
156
- # @return [void]
212
+ # @return [URI::Generic] parsed endpoint URI
157
213
  #
158
- def connect(endpoint, parent: nil)
214
+ def connect(endpoint, parent: nil, **opts)
159
215
  OMQ.freeze_for_ractors!
160
216
  capture_parent_task(parent: parent)
161
217
  validate_endpoint!(endpoint)
162
- @dialed.add(endpoint)
163
218
 
164
219
  if endpoint.start_with?("inproc://")
165
- # Inproc connect is synchronous and instant
220
+ # Inproc connect is synchronous and instant — no Dialer
166
221
  transport = transport_for(endpoint)
167
- transport.connect(endpoint, self)
222
+ transport.connect(endpoint, self, **opts)
223
+ @dialers[endpoint] = :inproc # sentinel for reconnect intent
168
224
  else
169
- # TCP/IPC connect in background — never blocks the caller
225
+ transport = transport_for(endpoint)
226
+ @dialers[endpoint] = transport.dialer(endpoint, self, **opts)
170
227
  emit_monitor_event(:connect_delayed, endpoint: endpoint)
171
228
  schedule_reconnect(endpoint, delay: 0)
172
229
  end
230
+
231
+ URI.parse(endpoint)
173
232
  end
174
233
 
175
234
 
@@ -180,7 +239,7 @@ module OMQ
180
239
  # @return [void]
181
240
  #
182
241
  def disconnect(endpoint)
183
- @dialed.delete(endpoint)
242
+ @dialers.delete(endpoint)
184
243
  close_connections_at(endpoint)
185
244
  end
186
245
 
@@ -192,11 +251,9 @@ module OMQ
192
251
  # @return [void]
193
252
  #
194
253
  def unbind(endpoint)
195
- listener = @listeners.find { |l| l.endpoint == endpoint }
196
- return unless listener
254
+ listener = @listeners.delete(endpoint) or return
197
255
 
198
256
  listener.stop
199
- @listeners.delete(listener)
200
257
  close_connections_at(endpoint)
201
258
  end
202
259
 
@@ -224,10 +281,10 @@ module OMQ
224
281
  end
225
282
 
226
283
 
227
- # Called by inproc transport with a pre-validated DirectPipe.
284
+ # Called by inproc transport with a pre-validated Pipe.
228
285
  # Skips ZMTP handshake — just registers with routing strategy.
229
286
  #
230
- # @param pipe [Transport::Inproc::DirectPipe]
287
+ # @param pipe [Transport::Inproc::Pipe]
231
288
  # @return [void]
232
289
  #
233
290
  def connection_ready(pipe, endpoint: nil)
@@ -272,7 +329,7 @@ module OMQ
272
329
 
273
330
  # Starts a recv pump for a connection, or wires the inproc fast path.
274
331
  #
275
- # @param conn [Connection, Transport::Inproc::DirectPipe]
332
+ # @param conn [Protocol::ZMTP::Connection, Transport::Inproc::Pipe]
276
333
  # @param recv_queue [Async::LimitedQueue]
277
334
  # @yield [msg] optional per-message transform
278
335
  # @return [Async::Task, nil]
@@ -282,16 +339,14 @@ module OMQ
282
339
  # torn down together with the rest of its sibling per-connection
283
340
  # pumps when the connection is lost.
284
341
  parent = @connections[conn]&.barrier || @lifecycle.barrier
285
- task = RecvPump.start(parent, conn, recv_queue, self, transform)
286
342
 
287
- @tasks << task if task
288
- task
343
+ RecvPump.start(parent, conn, recv_queue, self, transform)
289
344
  end
290
345
 
291
346
 
292
347
  # Called when a connection is lost.
293
348
  #
294
- # @param connection [Connection]
349
+ # @param connection [Protocol::ZMTP::Connection]
295
350
  # @return [void]
296
351
  #
297
352
  def connection_lost(connection)
@@ -302,8 +357,8 @@ module OMQ
302
357
  # Resolves `all_peers_gone` if we had peers and now have none.
303
358
  # Called by ConnectionLifecycle during teardown.
304
359
  #
305
- def resolve_all_peers_gone_if_empty
306
- @lifecycle.resolve_all_peers_gone_if_empty(@connections)
360
+ def maybe_resolve_all_peers_gone
361
+ @lifecycle.maybe_resolve_all_peers_gone(@connections)
307
362
  end
308
363
 
309
364
 
@@ -311,9 +366,12 @@ module OMQ
311
366
  # and the endpoint is still dialed.
312
367
  #
313
368
  def maybe_reconnect(endpoint)
314
- return unless endpoint && @dialed.include?(endpoint)
369
+ return unless endpoint && @dialers.key?(endpoint)
315
370
  return unless @lifecycle.open? && @lifecycle.reconnect_enabled
316
- Reconnect.schedule(endpoint, @options, @lifecycle.parent_task, self)
371
+
372
+ dialer = @dialers[endpoint]
373
+
374
+ Reconnect.schedule(dialer, @options, @lifecycle.parent_task, self)
317
375
  end
318
376
 
319
377
 
@@ -342,7 +400,6 @@ module OMQ
342
400
 
343
401
  stop_listeners
344
402
  tear_down_barrier
345
- routing.stop rescue nil
346
403
  emit_monitor_event(:closed)
347
404
  close_monitor_queue
348
405
  end
@@ -357,8 +414,7 @@ module OMQ
357
414
  def stop
358
415
  return unless @lifecycle.alive?
359
416
 
360
- @lifecycle.start_closing! if @lifecycle.open?
361
- @lifecycle.finish_closing!
417
+ @lifecycle.force_close!
362
418
 
363
419
  if @lifecycle.on_io_thread
364
420
  Reactor.untrack_linger(@options.linger)
@@ -366,7 +422,6 @@ module OMQ
366
422
 
367
423
  stop_listeners
368
424
  tear_down_barrier
369
- routing.stop rescue nil
370
425
  emit_monitor_event(:closed)
371
426
  close_monitor_queue
372
427
  end
@@ -379,11 +434,16 @@ module OMQ
379
434
  # see the real error instead of deadlocking.
380
435
  #
381
436
  # @param annotation [String] task annotation for debugging
437
+ # @param parent [Async::Task, Async::Barrier] parent for the spawned
438
+ # task. Defaults to the current task. Routing strategies that own
439
+ # loose pump tasks (Radio, Channel) pass their own +Async::Barrier+
440
+ # so a single +barrier.stop+ tears the lot down at strategy
441
+ # shutdown without per-task bookkeeping.
382
442
  # @yield the pump loop body
383
443
  # @return [Async::Task]
384
444
  #
385
- def spawn_pump_task(annotation:, &block)
386
- Async::Task.current.async(transient: true, annotation: annotation) do
445
+ def spawn_pump_task(annotation:, parent: Async::Task.current, &block)
446
+ parent.async(transient: true, annotation: annotation) do
387
447
  yield
388
448
  rescue Async::Stop, Async::Cancel, Protocol::ZMTP::Error, *CONNECTION_LOST
389
449
  # normal shutdown / expected disconnect
@@ -401,12 +461,15 @@ module OMQ
401
461
  # pumps blocked on `dequeue` waiting for messages that will never
402
462
  # be written.
403
463
  #
404
- # @param conn [Connection, Transport::Inproc::DirectPipe]
464
+ # @param conn [Protocol::ZMTP::Connection, Transport::Inproc::Pipe]
405
465
  # @param annotation [String]
406
466
  #
407
467
  def spawn_conn_pump_task(conn, annotation:, &block)
408
468
  lifecycle = @connections[conn]
409
- return spawn_pump_task(annotation: annotation, &block) unless lifecycle
469
+
470
+ unless lifecycle
471
+ return spawn_pump_task(annotation: annotation, &block)
472
+ end
410
473
 
411
474
  lifecycle.barrier.async(transient: true, annotation: annotation) do
412
475
  yield
@@ -471,7 +534,7 @@ module OMQ
471
534
 
472
535
  return unless task
473
536
 
474
- Maintenance.start(@lifecycle.barrier, @options.mechanism, @tasks)
537
+ Maintenance.start(@lifecycle.barrier, @options.mechanism)
475
538
  end
476
539
 
477
540
 
@@ -484,7 +547,10 @@ module OMQ
484
547
  #
485
548
  def emit_monitor_event(type, endpoint: nil, detail: nil)
486
549
  return unless @monitor_queue
487
- @monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
550
+
551
+ event = MonitorEvent.new type: type, endpoint: endpoint, detail: detail
552
+
553
+ @monitor_queue << event
488
554
  rescue Async::Stop, ClosedQueueError
489
555
  end
490
556
 
@@ -508,9 +574,14 @@ module OMQ
508
574
  # +last_wire_size_out+ (installed by ZMTP-Zstd etc.).
509
575
  def emit_verbose_msg_sent(conn, parts)
510
576
  return unless @verbose_monitor
577
+
511
578
  detail = { parts: parts }
512
- detail[:wire_size] = conn.last_wire_size_out if conn.respond_to?(:last_wire_size_out)
513
- emit_monitor_event(:message_sent, detail: detail)
579
+
580
+ if conn.respond_to? :last_wire_size_out
581
+ detail[:wire_size] = conn.last_wire_size_out
582
+ end
583
+
584
+ emit_monitor_event :message_sent, detail: detail
514
585
  end
515
586
 
516
587
 
@@ -522,11 +593,11 @@ module OMQ
522
593
 
523
594
  detail = { parts: parts }
524
595
 
525
- if conn.respond_to?(:last_wire_size_in)
596
+ if conn.respond_to? :last_wire_size_in
526
597
  detail[:wire_size] = conn.last_wire_size_in
527
598
  end
528
599
 
529
- emit_monitor_event(:message_received, detail: detail)
600
+ emit_monitor_event :message_received, detail: detail
530
601
  end
531
602
 
532
603
 
@@ -551,8 +622,18 @@ module OMQ
551
622
  private
552
623
 
553
624
 
625
+ # Spawns a per-connection task on the socket-level barrier that runs
626
+ # the handshake and then blocks on +done+ until the connection is
627
+ # torn down. The +ensure+ path runs {ConnectionLifecycle#close!} so
628
+ # side effects (routing removal, monitor event, reconnect) always
629
+ # fire exactly once, regardless of how the task unwinds.
630
+ #
631
+ # @param io [#read, #write, #close] accepted or dialed socket
632
+ # @param as_server [Boolean] true for accepted connections
633
+ # @param endpoint [String, nil] the endpoint URI, for monitor events
634
+ #
554
635
  def spawn_connection(io, as_server:, endpoint: nil)
555
- task = @lifecycle.barrier&.async(transient: true, annotation: "conn #{endpoint}") do
636
+ @lifecycle.barrier&.async(transient: true, annotation: "conn #{endpoint}") do
556
637
  done = Async::Promise.new
557
638
  lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint, done: done)
558
639
  lifecycle.handshake!(io, as_server: as_server)
@@ -566,16 +647,21 @@ module OMQ
566
647
  ensure
567
648
  lifecycle&.close!
568
649
  end
569
-
570
- @tasks << task if task
571
650
  end
572
651
 
573
652
 
653
+ # Blocks until the routing strategy reports all send queues drained
654
+ # or +timeout+ seconds have elapsed. Called from the linger path on
655
+ # close so pending outgoing messages get a chance to flush.
656
+ #
657
+ # @param timeout [Numeric, nil] max seconds to wait; nil = no limit
658
+ #
574
659
  # TODO: replace the 1 ms busy-poll with a promise/condition that
575
660
  # the send pump resolves when its queue hits empty. The loop exists
576
661
  # because there is currently no signal for "send queue fully
577
662
  # drained"; fixing it cleanly requires plumbing a notifier through
578
663
  # every routing strategy, so it is flagged rather than fixed here.
664
+ #
579
665
  def drain_send_queues(timeout)
580
666
  return unless @routing.respond_to?(:send_queues_drained?)
581
667
 
@@ -594,11 +680,26 @@ module OMQ
594
680
  end
595
681
 
596
682
 
683
+ # Schedules a reconnect attempt for +endpoint+ using its dialer.
684
+ # Called on initial connect (with +delay: 0+) and after a lost
685
+ # connection (with the dialer's backoff delay).
686
+ #
687
+ # @param endpoint [String]
688
+ # @param delay [Numeric, nil] initial delay override in seconds
689
+ #
597
690
  def schedule_reconnect(endpoint, delay: nil)
598
- Reconnect.schedule(endpoint, @options, @lifecycle.parent_task, self, delay: delay)
691
+ dialer = @dialers[endpoint]
692
+ Reconnect.schedule(dialer, @options, @lifecycle.parent_task, self, delay: delay)
599
693
  end
600
694
 
601
695
 
696
+ # Delegates endpoint validation to the transport if it defines one.
697
+ # Called from {#connect} so a bad URI fails fast instead of landing
698
+ # in a reconnect loop.
699
+ #
700
+ # @param endpoint [String]
701
+ # @raise [ArgumentError] if the transport rejects the endpoint
702
+ #
602
703
  def validate_endpoint!(endpoint)
603
704
  transport = transport_for(endpoint)
604
705
 
@@ -608,6 +709,11 @@ module OMQ
608
709
  end
609
710
 
610
711
 
712
+ # Starts the listener's accept loops on the socket-level barrier
713
+ # and routes accepted IOs through {#handle_accepted}.
714
+ #
715
+ # @param listener [#start_accept_loops, #endpoint]
716
+ #
611
717
  def start_accept_loops(listener)
612
718
  return unless listener.respond_to?(:start_accept_loops)
613
719
 
@@ -617,12 +723,21 @@ module OMQ
617
723
  end
618
724
 
619
725
 
726
+ # Stops every active listener and clears the registry. Called from
727
+ # close paths; connection teardown is handled separately via the
728
+ # socket-level barrier.
729
+ #
620
730
  def stop_listeners
621
- @listeners.each(&:stop)
731
+ @listeners.each_value(&:stop)
622
732
  @listeners.clear
623
733
  end
624
734
 
625
735
 
736
+ # Closes all connections currently bound to or connected from
737
+ # +endpoint+. Used by {#disconnect} and {#unbind}.
738
+ #
739
+ # @param endpoint [String]
740
+ #
626
741
  def close_connections_at(endpoint)
627
742
  @connections.values.select { |lc| lc.endpoint == endpoint }.each(&:close!)
628
743
  end
@@ -631,24 +746,21 @@ module OMQ
631
746
  # Cascades teardown through the socket-level barrier. Stopping the
632
747
  # barrier cancels every tracked task: connection supervisors (whose
633
748
  # `ensure lost!` runs the ordered disconnect side effects), accept
634
- # loops, reconnect loops, heartbeat, maintenance. After the cascade,
635
- # clears the legacy +@tasks+ list.
749
+ # loops, heartbeat, maintenance. Reconnect loops live on the user's
750
+ # parent task and self-exit via `@engine.closed?`.
636
751
  #
637
752
  def tear_down_barrier
638
753
  @lifecycle.barrier&.stop
639
- @tasks.clear
640
754
  end
641
755
 
642
756
 
757
+ # Signals end-of-stream on the monitor queue (if any) by pushing a
758
+ # +nil+ sentinel, so consumers iterating with {Socket#each_event}
759
+ # can exit cleanly.
760
+ #
643
761
  def close_monitor_queue
644
762
  return unless @monitor_queue
645
763
  @monitor_queue.push(nil)
646
764
  end
647
765
  end
648
-
649
-
650
- # Register built-in transports.
651
- Engine.transports["tcp"] = Transport::TCP
652
- Engine.transports["ipc"] = Transport::IPC
653
- Engine.transports["inproc"] = Transport::Inproc
654
766
  end
data/lib/omq/peer.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OMQ PEER socket type (ZeroMQ RFC 51).
4
+ #
5
+ # Not loaded by +require "omq"+; opt in with:
6
+ #
7
+ # require "omq/peer"
8
+
9
+ require "omq"
10
+ require_relative "routing/peer"
11
+
12
+ module OMQ
13
+ # Bidirectional multi-peer socket with routing IDs (ZeroMQ RFC 51).
14
+ #
15
+ # Each connected peer is assigned a 4-byte routing ID. Supports
16
+ # directed sends via #send_to and fair-queued receives.
17
+ class PEER < Socket
18
+ include Readable
19
+ include Writable
20
+ include SingleFrame
21
+
22
+ # Creates a new PEER socket.
23
+ #
24
+ # @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
25
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
26
+ # @param backend [Object, nil] optional transport backend
27
+ def initialize(endpoints = nil, linger: Float::INFINITY, backend: nil)
28
+ init_engine(:PEER, backend: backend)
29
+ @options.linger = linger
30
+ attach_endpoints(endpoints, default: :connect)
31
+ end
32
+
33
+
34
+ # Sends a message to a specific peer by routing ID.
35
+ #
36
+ # @param routing_id [String] 4-byte routing ID
37
+ # @param message [String] message body
38
+ # @return [self]
39
+ #
40
+ def send_to(routing_id, message)
41
+ parts = [routing_id, message]
42
+ Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
43
+ self
44
+ end
45
+ end
46
+
47
+
48
+ Routing.register(:PEER, Routing::Peer)
49
+ end
data/lib/omq/pub_sub.rb CHANGED
@@ -61,7 +61,7 @@ module OMQ
61
61
  # @return [void]
62
62
  #
63
63
  def subscribe(prefix = EVERYTHING)
64
- @engine.routing.subscribe(prefix)
64
+ @engine.subscribe(prefix)
65
65
  end
66
66
 
67
67
 
@@ -71,7 +71,7 @@ module OMQ
71
71
  # @return [void]
72
72
  #
73
73
  def unsubscribe(prefix)
74
- @engine.routing.unsubscribe(prefix)
74
+ @engine.unsubscribe(prefix)
75
75
  end
76
76
 
77
77
  end