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