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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +162 -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 +22 -8
- data/lib/omq/engine/heartbeat.rb +3 -4
- data/lib/omq/engine/maintenance.rb +4 -5
- data/lib/omq/engine/reconnect.rb +12 -11
- data/lib/omq/engine/recv_pump.rb +10 -10
- data/lib/omq/engine/socket_lifecycle.rb +26 -9
- data/lib/omq/engine.rb +202 -90
- 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/readable.rb +5 -1
- data/lib/omq/routing/channel.rb +110 -0
- data/lib/omq/routing/client.rb +70 -0
- data/lib/omq/routing/conn_send_pump.rb +5 -8
- data/lib/omq/routing/dealer.rb +3 -15
- data/lib/omq/routing/dish.rb +94 -0
- data/lib/omq/routing/fan_out.rb +12 -16
- data/lib/omq/routing/gather.rb +60 -0
- data/lib/omq/routing/pair.rb +7 -26
- data/lib/omq/routing/peer.rb +95 -0
- data/lib/omq/routing/pub.rb +2 -13
- data/lib/omq/routing/pull.rb +3 -15
- data/lib/omq/routing/push.rb +4 -13
- data/lib/omq/routing/radio.rb +187 -0
- data/lib/omq/routing/rep.rb +5 -19
- data/lib/omq/routing/req.rb +6 -18
- data/lib/omq/routing/round_robin.rb +15 -19
- data/lib/omq/routing/router.rb +5 -19
- data/lib/omq/routing/scatter.rb +76 -0
- data/lib/omq/routing/server.rb +90 -0
- data/lib/omq/routing/sub.rb +3 -15
- data/lib/omq/routing/xpub.rb +2 -13
- data/lib/omq/routing/xsub.rb +8 -25
- data/lib/omq/scatter_gather.rb +56 -0
- data/lib/omq/socket.rb +8 -23
- data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +26 -24
- data/lib/omq/transport/inproc.rb +22 -14
- 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/writable.rb +11 -42
- data/lib/omq.rb +9 -64
- metadata +17 -3
- 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,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
|
-
#
|
|
107
|
-
#
|
|
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
|
-
|
|
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
|
-
@
|
|
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 [
|
|
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.
|
|
196
|
+
listener = transport.listener(endpoint, self, **opts)
|
|
140
197
|
|
|
141
198
|
start_accept_loops(listener)
|
|
142
199
|
|
|
143
|
-
@listeners
|
|
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 [
|
|
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
|
-
|
|
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
|
-
@
|
|
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.
|
|
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
|
|
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::
|
|
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::
|
|
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
|
-
|
|
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
|
|
306
|
-
@lifecycle.
|
|
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 && @
|
|
369
|
+
return unless endpoint && @dialers.key?(endpoint)
|
|
315
370
|
return unless @lifecycle.open? && @lifecycle.reconnect_enabled
|
|
316
|
-
|
|
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.
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
635
|
-
#
|
|
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.
|
|
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
|