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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -0
- data/README.md +17 -21
- data/lib/omq/channel.rb +35 -0
- data/lib/omq/client_server.rb +72 -0
- data/lib/omq/constants.rb +68 -0
- data/lib/omq/engine/connection_lifecycle.rb +11 -3
- data/lib/omq/engine/heartbeat.rb +2 -3
- data/lib/omq/engine/maintenance.rb +4 -5
- data/lib/omq/engine/reconnect.rb +12 -11
- data/lib/omq/engine/recv_pump.rb +3 -4
- data/lib/omq/engine/socket_lifecycle.rb +26 -9
- data/lib/omq/engine.rb +196 -85
- data/lib/omq/peer.rb +49 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +122 -0
- data/lib/omq/reactor.rb +14 -5
- data/lib/omq/routing/channel.rb +110 -0
- data/lib/omq/routing/client.rb +70 -0
- data/lib/omq/routing/conn_send_pump.rb +4 -7
- data/lib/omq/routing/dealer.rb +1 -13
- data/lib/omq/routing/dish.rb +94 -0
- data/lib/omq/routing/fan_out.rb +5 -9
- data/lib/omq/routing/gather.rb +60 -0
- data/lib/omq/routing/pair.rb +3 -22
- data/lib/omq/routing/peer.rb +95 -0
- data/lib/omq/routing/pub.rb +0 -11
- data/lib/omq/routing/pull.rb +1 -13
- data/lib/omq/routing/push.rb +1 -10
- data/lib/omq/routing/radio.rb +187 -0
- data/lib/omq/routing/rep.rb +3 -17
- data/lib/omq/routing/req.rb +4 -16
- data/lib/omq/routing/round_robin.rb +11 -15
- data/lib/omq/routing/router.rb +3 -17
- data/lib/omq/routing/scatter.rb +77 -0
- data/lib/omq/routing/server.rb +90 -0
- data/lib/omq/routing/sub.rb +1 -13
- data/lib/omq/routing/xpub.rb +0 -11
- data/lib/omq/routing/xsub.rb +6 -23
- data/lib/omq/scatter_gather.rb +56 -0
- data/lib/omq/socket.rb +8 -23
- data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
- data/lib/omq/transport/inproc.rb +11 -3
- data/lib/omq/transport/ipc.rb +41 -13
- data/lib/omq/transport/tcp.rb +59 -23
- data/lib/omq/transport/udp.rb +281 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq.rb +9 -64
- metadata +16 -2
- 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 [
|
|
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 :
|
|
43
|
+
attr_reader :listeners
|
|
50
44
|
|
|
51
45
|
|
|
52
|
-
# @return [
|
|
46
|
+
# @return [Hash{Connection => ConnectionLifecycle}] active connections
|
|
53
47
|
#
|
|
54
|
-
attr_reader :
|
|
48
|
+
attr_reader :connections
|
|
55
49
|
|
|
56
50
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
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
|
-
|
|
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 :
|
|
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
|
-
#
|
|
107
|
-
#
|
|
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
|
-
|
|
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
|
-
@
|
|
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 [
|
|
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.
|
|
195
|
+
listener = transport.listener(endpoint, self, **opts)
|
|
140
196
|
|
|
141
197
|
start_accept_loops(listener)
|
|
142
198
|
|
|
143
|
-
@listeners
|
|
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 [
|
|
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
|
-
|
|
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
|
-
@
|
|
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.
|
|
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
|
-
|
|
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
|
|
306
|
-
@lifecycle.
|
|
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 && @
|
|
368
|
+
return unless endpoint && @dialers.key?(endpoint)
|
|
315
369
|
return unless @lifecycle.open? && @lifecycle.reconnect_enabled
|
|
316
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
635
|
-
#
|
|
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.
|
|
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.
|
|
74
|
+
@engine.unsubscribe(prefix)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
end
|