omq 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -1
- data/README.md +27 -0
- data/lib/omq/drop_queue.rb +3 -0
- data/lib/omq/engine/connection_setup.rb +70 -0
- data/lib/omq/engine/heartbeat.rb +40 -0
- data/lib/omq/engine/maintenance.rb +35 -0
- data/lib/omq/engine/reconnect.rb +82 -0
- data/lib/omq/engine/recv_pump.rb +119 -0
- data/lib/omq/engine.rb +139 -304
- data/lib/omq/options.rb +44 -0
- data/lib/omq/pair.rb +6 -0
- data/lib/omq/pub_sub.rb +25 -0
- data/lib/omq/push_pull.rb +17 -0
- data/lib/omq/queue_interface.rb +1 -0
- data/lib/omq/readable.rb +2 -0
- data/lib/omq/req_rep.rb +13 -0
- data/lib/omq/router_dealer.rb +12 -0
- data/lib/omq/routing/conn_send_pump.rb +36 -0
- data/lib/omq/routing/dealer.rb +15 -10
- data/lib/omq/routing/fair_queue.rb +172 -0
- data/lib/omq/routing/fair_recv.rb +27 -0
- data/lib/omq/routing/fan_out.rb +127 -74
- data/lib/omq/routing/pair.rb +47 -20
- data/lib/omq/routing/pub.rb +12 -6
- data/lib/omq/routing/pull.rb +12 -4
- data/lib/omq/routing/push.rb +3 -12
- data/lib/omq/routing/rep.rb +41 -51
- data/lib/omq/routing/req.rb +15 -10
- data/lib/omq/routing/round_robin.rb +82 -63
- data/lib/omq/routing/router.rb +32 -48
- data/lib/omq/routing/sub.rb +18 -5
- data/lib/omq/routing/xpub.rb +15 -3
- data/lib/omq/routing/xsub.rb +53 -27
- data/lib/omq/routing.rb +29 -11
- data/lib/omq/socket.rb +25 -7
- data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
- data/lib/omq/transport/inproc.rb +41 -217
- data/lib/omq/transport/ipc.rb +7 -1
- data/lib/omq/transport/tcp.rb +12 -7
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +2 -0
- data/lib/omq.rb +4 -1
- metadata +14 -5
data/lib/omq/engine.rb
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "async"
|
|
4
|
+
require_relative "engine/recv_pump"
|
|
5
|
+
require_relative "engine/heartbeat"
|
|
6
|
+
require_relative "engine/reconnect"
|
|
7
|
+
require_relative "engine/connection_setup"
|
|
8
|
+
require_relative "engine/maintenance"
|
|
4
9
|
|
|
5
10
|
module OMQ
|
|
6
11
|
# Per-socket orchestrator.
|
|
@@ -20,6 +25,13 @@ module OMQ
|
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
|
|
28
|
+
# Per-connection metadata: the endpoint it was established on and an
|
|
29
|
+
# optional Promise resolved when the connection is lost (used by
|
|
30
|
+
# {#spawn_connection} to await connection teardown).
|
|
31
|
+
#
|
|
32
|
+
ConnectionRecord = Data.define(:endpoint, :done)
|
|
33
|
+
|
|
34
|
+
|
|
23
35
|
# @return [Symbol] socket type (e.g. :REQ, :PAIR)
|
|
24
36
|
#
|
|
25
37
|
attr_reader :socket_type
|
|
@@ -49,33 +61,45 @@ module OMQ
|
|
|
49
61
|
# @param options [Options]
|
|
50
62
|
#
|
|
51
63
|
def initialize(socket_type, options)
|
|
52
|
-
@socket_type
|
|
53
|
-
@options
|
|
54
|
-
@routing
|
|
55
|
-
@connections
|
|
56
|
-
@
|
|
57
|
-
@
|
|
58
|
-
@
|
|
59
|
-
@
|
|
60
|
-
@
|
|
61
|
-
@
|
|
62
|
-
@
|
|
63
|
-
@
|
|
64
|
-
@
|
|
65
|
-
@
|
|
66
|
-
@
|
|
67
|
-
@
|
|
68
|
-
@
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@monitor_queue = nil
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
|
|
64
|
+
@socket_type = socket_type
|
|
65
|
+
@options = options
|
|
66
|
+
@routing = Routing.for(socket_type).new(self)
|
|
67
|
+
@connections = {} # connection => ConnectionRecord
|
|
68
|
+
@dialed = Set.new # endpoints we called connect() on (reconnect intent)
|
|
69
|
+
@listeners = []
|
|
70
|
+
@tasks = []
|
|
71
|
+
@state = :open
|
|
72
|
+
@last_endpoint = nil
|
|
73
|
+
@last_tcp_port = nil
|
|
74
|
+
@peer_connected = Async::Promise.new
|
|
75
|
+
@all_peers_gone = Async::Promise.new
|
|
76
|
+
@reconnect_enabled = true
|
|
77
|
+
@parent_task = nil
|
|
78
|
+
@on_io_thread = false
|
|
79
|
+
@fatal_error = nil
|
|
80
|
+
@monitor_queue = nil
|
|
81
|
+
end
|
|
82
|
+
|
|
76
83
|
|
|
84
|
+
# @return [Async::Promise] resolves when first peer completes handshake
|
|
85
|
+
# @return [Async::Promise] resolves when all peers disconnect (after having had peers)
|
|
86
|
+
# @return [Hash{Connection => ConnectionRecord}] active connections
|
|
87
|
+
# @return [Async::Task, nil] root task for spawning subtrees
|
|
88
|
+
# @return [Array<Async::Task>] background tasks (pumps, heartbeat, reconnect)
|
|
89
|
+
#
|
|
90
|
+
attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task, :tasks
|
|
91
|
+
|
|
92
|
+
# @!attribute [w] reconnect_enabled
|
|
93
|
+
# @param value [Boolean] enable or disable auto-reconnect
|
|
94
|
+
# @!attribute [w] monitor_queue
|
|
95
|
+
# @param value [Async::Queue, nil] queue for monitor events
|
|
96
|
+
#
|
|
77
97
|
attr_writer :reconnect_enabled, :monitor_queue
|
|
78
98
|
|
|
99
|
+
# @return [Boolean] true if the engine has been closed
|
|
100
|
+
#
|
|
101
|
+
def closed? = @state == :closed
|
|
102
|
+
|
|
79
103
|
# Optional proc that wraps new connections (e.g. for serialization).
|
|
80
104
|
# Called with the raw connection; must return the (possibly wrapped) connection.
|
|
81
105
|
#
|
|
@@ -126,7 +150,7 @@ module OMQ
|
|
|
126
150
|
def connect(endpoint)
|
|
127
151
|
freeze_error_lists!
|
|
128
152
|
validate_endpoint!(endpoint)
|
|
129
|
-
@
|
|
153
|
+
@dialed.add(endpoint)
|
|
130
154
|
if endpoint.start_with?("inproc://")
|
|
131
155
|
# Inproc connect is synchronous and instant
|
|
132
156
|
transport = transport_for(endpoint)
|
|
@@ -146,14 +170,8 @@ module OMQ
|
|
|
146
170
|
# @return [void]
|
|
147
171
|
#
|
|
148
172
|
def disconnect(endpoint)
|
|
149
|
-
@
|
|
150
|
-
|
|
151
|
-
conns.each do |conn|
|
|
152
|
-
@connection_endpoints.delete(conn)
|
|
153
|
-
@connections.delete(conn)
|
|
154
|
-
@routing.connection_removed(conn)
|
|
155
|
-
conn.close
|
|
156
|
-
end
|
|
173
|
+
@dialed.delete(endpoint)
|
|
174
|
+
close_connections_at(endpoint)
|
|
157
175
|
end
|
|
158
176
|
|
|
159
177
|
|
|
@@ -168,15 +186,7 @@ module OMQ
|
|
|
168
186
|
return unless listener
|
|
169
187
|
listener.stop
|
|
170
188
|
@listeners.delete(listener)
|
|
171
|
-
|
|
172
|
-
# Close connections accepted on this endpoint
|
|
173
|
-
conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
|
|
174
|
-
conns.each do |conn|
|
|
175
|
-
@connection_endpoints.delete(conn)
|
|
176
|
-
@connections.delete(conn)
|
|
177
|
-
@routing.connection_removed(conn)
|
|
178
|
-
conn.close
|
|
179
|
-
end
|
|
189
|
+
close_connections_at(endpoint)
|
|
180
190
|
end
|
|
181
191
|
|
|
182
192
|
|
|
@@ -211,8 +221,7 @@ module OMQ
|
|
|
211
221
|
#
|
|
212
222
|
def connection_ready(pipe, endpoint: nil)
|
|
213
223
|
pipe = @connection_wrapper.call(pipe) if @connection_wrapper
|
|
214
|
-
@connections
|
|
215
|
-
@connection_endpoints[pipe] = endpoint if endpoint
|
|
224
|
+
@connections[pipe] = ConnectionRecord.new(endpoint: endpoint, done: nil)
|
|
216
225
|
@routing.connection_added(pipe)
|
|
217
226
|
@peer_connected.resolve(pipe)
|
|
218
227
|
emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
|
|
@@ -273,77 +282,17 @@ module OMQ
|
|
|
273
282
|
end
|
|
274
283
|
|
|
275
284
|
|
|
276
|
-
# Starts a recv pump for a connection, or wires the inproc
|
|
277
|
-
# fast path when the connection is a DirectPipe.
|
|
285
|
+
# Starts a recv pump for a connection, or wires the inproc fast path.
|
|
278
286
|
#
|
|
279
287
|
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
280
|
-
#
|
|
281
|
-
# and enqueues them into the routing strategy's recv queue.
|
|
282
|
-
#
|
|
283
|
-
# When a block is given, each message is yielded for transformation
|
|
284
|
-
# before enqueueing. The block is compiled at the call site, giving
|
|
285
|
-
# YJIT a monomorphic call per routing strategy instead of a shared
|
|
286
|
-
# megamorphic `transform.call` dispatch.
|
|
287
|
-
#
|
|
288
|
-
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
289
|
-
# @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
|
|
288
|
+
# @param recv_queue [SignalingQueue]
|
|
290
289
|
# @yield [msg] optional per-message transform
|
|
291
|
-
# @return [
|
|
290
|
+
# @return [Async::Task, nil]
|
|
292
291
|
#
|
|
293
|
-
# Fairness limits for the recv pump. Yield to the scheduler
|
|
294
|
-
# after reading this many messages or bytes from one connection,
|
|
295
|
-
# whichever comes first. Prevents a fast or large-message
|
|
296
|
-
# connection from starving slower peers.
|
|
297
|
-
RECV_FAIRNESS_MESSAGES = 64
|
|
298
|
-
RECV_FAIRNESS_BYTES = 1 << 20 # 1 MB
|
|
299
|
-
|
|
300
292
|
def start_recv_pump(conn, recv_queue, &transform)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return nil
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
if transform
|
|
308
|
-
@parent_task.async(transient: true, annotation: "recv pump") do |task|
|
|
309
|
-
loop do
|
|
310
|
-
count = 0
|
|
311
|
-
bytes = 0
|
|
312
|
-
while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
|
|
313
|
-
msg = conn.receive_message
|
|
314
|
-
msg = transform.call(msg).freeze
|
|
315
|
-
recv_queue.enqueue(msg)
|
|
316
|
-
count += 1
|
|
317
|
-
bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
|
|
318
|
-
end
|
|
319
|
-
task.yield
|
|
320
|
-
end
|
|
321
|
-
rescue Async::Stop
|
|
322
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
323
|
-
connection_lost(conn)
|
|
324
|
-
rescue => error
|
|
325
|
-
signal_fatal_error(error)
|
|
326
|
-
end
|
|
327
|
-
else
|
|
328
|
-
@parent_task.async(transient: true, annotation: "recv pump") do |task|
|
|
329
|
-
loop do
|
|
330
|
-
count = 0
|
|
331
|
-
bytes = 0
|
|
332
|
-
while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
|
|
333
|
-
msg = conn.receive_message
|
|
334
|
-
recv_queue.enqueue(msg)
|
|
335
|
-
count += 1
|
|
336
|
-
bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
|
|
337
|
-
end
|
|
338
|
-
task.yield
|
|
339
|
-
end
|
|
340
|
-
rescue Async::Stop
|
|
341
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
342
|
-
connection_lost(conn)
|
|
343
|
-
rescue => error
|
|
344
|
-
signal_fatal_error(error)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
293
|
+
task = RecvPump.start(@parent_task, conn, recv_queue, self, transform)
|
|
294
|
+
@tasks << task if task
|
|
295
|
+
task
|
|
347
296
|
end
|
|
348
297
|
|
|
349
298
|
|
|
@@ -353,25 +302,13 @@ module OMQ
|
|
|
353
302
|
# @return [void]
|
|
354
303
|
#
|
|
355
304
|
def connection_lost(connection)
|
|
356
|
-
|
|
357
|
-
@connections.delete(connection)
|
|
305
|
+
entry = @connections.delete(connection)
|
|
358
306
|
@routing.connection_removed(connection)
|
|
359
307
|
connection.close
|
|
360
|
-
emit_monitor_event(:disconnected, endpoint: endpoint)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
done&.resolve(true)
|
|
365
|
-
|
|
366
|
-
# Resolve all_peers_gone once: had peers, now have none.
|
|
367
|
-
if @peer_connected.resolved? && @connections.empty?
|
|
368
|
-
@all_peers_gone.resolve(true)
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
# Auto-reconnect if this was a connected (not bound) endpoint
|
|
372
|
-
if endpoint && @connected_endpoints.include?(endpoint) && !@closed && !@closing && @reconnect_enabled
|
|
373
|
-
schedule_reconnect(endpoint)
|
|
374
|
-
end
|
|
308
|
+
emit_monitor_event(:disconnected, endpoint: entry&.endpoint)
|
|
309
|
+
entry&.done&.resolve(true)
|
|
310
|
+
@all_peers_gone.resolve(true) if @peer_connected.resolved? && @connections.empty?
|
|
311
|
+
maybe_reconnect(entry&.endpoint)
|
|
375
312
|
end
|
|
376
313
|
|
|
377
314
|
|
|
@@ -380,42 +317,15 @@ module OMQ
|
|
|
380
317
|
# @return [void]
|
|
381
318
|
#
|
|
382
319
|
def close
|
|
383
|
-
return
|
|
384
|
-
@
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
# stay open so late-arriving peers can still receive queued
|
|
389
|
-
# messages during the linger period.
|
|
390
|
-
unless @connections.empty?
|
|
391
|
-
@listeners.each(&:stop)
|
|
392
|
-
@listeners.clear
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# Linger: wait for send queues to drain before closing.
|
|
396
|
-
# linger=0 → close immediately, linger=nil → wait forever.
|
|
397
|
-
# @closed is set AFTER draining so reconnect tasks keep
|
|
398
|
-
# running during the linger period.
|
|
399
|
-
linger = @options.linger
|
|
400
|
-
if linger.nil? || linger > 0
|
|
401
|
-
drain_timeout = linger # nil = wait forever, >0 = seconds
|
|
402
|
-
drain_send_queues(drain_timeout)
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
@closed = true
|
|
320
|
+
return unless @state == :open
|
|
321
|
+
@state = :closing
|
|
322
|
+
stop_listeners unless @connections.empty?
|
|
323
|
+
drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
|
|
324
|
+
@state = :closed
|
|
406
325
|
Reactor.untrack_linger(@options.linger) if @on_io_thread
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
@listeners.clear
|
|
411
|
-
|
|
412
|
-
# Close connections — causes pump tasks to get EOFError/IOError
|
|
413
|
-
@connections.each(&:close)
|
|
414
|
-
@connections.clear
|
|
415
|
-
# Stop any remaining pump tasks
|
|
416
|
-
@routing.stop rescue nil
|
|
417
|
-
@tasks.each { |t| t.stop rescue nil }
|
|
418
|
-
@tasks.clear
|
|
326
|
+
stop_listeners
|
|
327
|
+
close_connections
|
|
328
|
+
stop_tasks
|
|
419
329
|
emit_monitor_event(:closed)
|
|
420
330
|
close_monitor_queue
|
|
421
331
|
end
|
|
@@ -451,13 +361,13 @@ module OMQ
|
|
|
451
361
|
# @param error [Exception]
|
|
452
362
|
#
|
|
453
363
|
def signal_fatal_error(error)
|
|
454
|
-
return
|
|
364
|
+
return unless @state == :open
|
|
455
365
|
@fatal_error = begin
|
|
456
366
|
raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
|
|
457
367
|
rescue => wrapped
|
|
458
368
|
wrapped
|
|
459
369
|
end
|
|
460
|
-
@routing.recv_queue.
|
|
370
|
+
@routing.recv_queue.push(nil) rescue nil
|
|
461
371
|
@peer_connected.resolve(nil) rescue nil
|
|
462
372
|
end
|
|
463
373
|
|
|
@@ -476,21 +386,42 @@ module OMQ
|
|
|
476
386
|
@on_io_thread = true
|
|
477
387
|
Reactor.track_linger(@options.linger)
|
|
478
388
|
end
|
|
389
|
+
Maintenance.start(@parent_task, @options.mechanism, @tasks)
|
|
479
390
|
end
|
|
480
391
|
|
|
481
392
|
|
|
482
|
-
|
|
393
|
+
# Emits a lifecycle event to the monitor queue, if one is attached.
|
|
394
|
+
#
|
|
395
|
+
# @param type [Symbol] event type (e.g. :listening, :connected, :disconnected)
|
|
396
|
+
# @param endpoint [String, nil] the endpoint involved
|
|
397
|
+
# @param detail [Hash, nil] extra context
|
|
398
|
+
# @return [void]
|
|
399
|
+
#
|
|
400
|
+
def emit_monitor_event(type, endpoint: nil, detail: nil)
|
|
401
|
+
return unless @monitor_queue
|
|
402
|
+
@monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
|
|
403
|
+
rescue Async::Stop, ClosedQueueError
|
|
404
|
+
end
|
|
483
405
|
|
|
484
406
|
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
#
|
|
488
|
-
#
|
|
407
|
+
# Looks up the transport module for an endpoint URI.
|
|
408
|
+
#
|
|
409
|
+
# @param endpoint [String] endpoint URI (e.g. "tcp://...", "inproc://...")
|
|
410
|
+
# @return [Module] the transport module
|
|
411
|
+
# @raise [ArgumentError] if the scheme is not registered
|
|
489
412
|
#
|
|
413
|
+
def transport_for(endpoint)
|
|
414
|
+
scheme = endpoint[/\A([^:]+):\/\//, 1]
|
|
415
|
+
self.class.transports[scheme] or
|
|
416
|
+
raise ArgumentError, "unsupported transport: #{endpoint}"
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
private
|
|
420
|
+
|
|
490
421
|
def spawn_connection(io, as_server:, endpoint: nil)
|
|
491
422
|
task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
|
|
492
423
|
done = Async::Promise.new
|
|
493
|
-
conn =
|
|
424
|
+
conn = ConnectionSetup.run(io, self, as_server: as_server, endpoint: endpoint, done: done)
|
|
494
425
|
done.wait
|
|
495
426
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
496
427
|
# handshake failed or connection lost — subtree cleaned up
|
|
@@ -501,153 +432,34 @@ module OMQ
|
|
|
501
432
|
end
|
|
502
433
|
|
|
503
434
|
|
|
504
|
-
# Waits for the send queue to drain.
|
|
505
|
-
#
|
|
506
|
-
# @param timeout [Numeric, nil] max seconds to wait (nil = forever)
|
|
507
|
-
#
|
|
508
435
|
def drain_send_queues(timeout)
|
|
509
|
-
return unless @routing.respond_to?(:
|
|
436
|
+
return unless @routing.respond_to?(:send_queues_drained?)
|
|
510
437
|
deadline = timeout ? Async::Clock.now + timeout : nil
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if deadline
|
|
514
|
-
remaining = deadline - Async::Clock.now
|
|
515
|
-
break if remaining <= 0
|
|
516
|
-
end
|
|
438
|
+
until @routing.send_queues_drained?
|
|
439
|
+
break if deadline && (deadline - Async::Clock.now) <= 0
|
|
517
440
|
sleep 0.001
|
|
518
441
|
end
|
|
519
442
|
end
|
|
520
443
|
|
|
521
444
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# @param as_server [Boolean] whether we are the ZMTP server side
|
|
527
|
-
# @param endpoint [String, nil] endpoint for reconnection tracking
|
|
528
|
-
# @param done [Async::Promise, nil] resolved when the connection is lost
|
|
529
|
-
#
|
|
530
|
-
def setup_connection(io, as_server:, endpoint: nil, done: nil)
|
|
531
|
-
conn = Protocol::ZMTP::Connection.new(
|
|
532
|
-
io,
|
|
533
|
-
socket_type: @socket_type.to_s,
|
|
534
|
-
identity: @options.identity,
|
|
535
|
-
as_server: as_server,
|
|
536
|
-
mechanism: @options.mechanism&.dup,
|
|
537
|
-
max_message_size: @options.max_message_size,
|
|
538
|
-
)
|
|
539
|
-
conn.handshake!
|
|
540
|
-
start_heartbeat(conn)
|
|
541
|
-
conn = @connection_wrapper.call(conn) if @connection_wrapper
|
|
542
|
-
@connections << conn
|
|
543
|
-
@connection_endpoints[conn] = endpoint if endpoint
|
|
544
|
-
@connection_promises[conn] = done if done
|
|
545
|
-
@routing.connection_added(conn)
|
|
546
|
-
@peer_connected.resolve(conn)
|
|
547
|
-
emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
|
|
548
|
-
conn
|
|
549
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
|
|
550
|
-
emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
|
|
551
|
-
conn&.close
|
|
552
|
-
raise
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
# Spawns a heartbeat task for the connection.
|
|
557
|
-
# The connection only tracks timestamps — the engine drives the loop.
|
|
558
|
-
#
|
|
559
|
-
# @param conn [Connection]
|
|
560
|
-
# @return [void]
|
|
561
|
-
#
|
|
562
|
-
def start_heartbeat(conn)
|
|
563
|
-
interval = @options.heartbeat_interval
|
|
564
|
-
return unless interval
|
|
565
|
-
|
|
566
|
-
ttl = @options.heartbeat_ttl || interval
|
|
567
|
-
timeout = @options.heartbeat_timeout || interval
|
|
568
|
-
conn.touch_heartbeat
|
|
569
|
-
|
|
570
|
-
@tasks << @parent_task.async(transient: true, annotation: "heartbeat") do
|
|
571
|
-
loop do
|
|
572
|
-
sleep interval
|
|
573
|
-
conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl, context: "".b))
|
|
574
|
-
if conn.heartbeat_expired?(timeout)
|
|
575
|
-
conn.close
|
|
576
|
-
break
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
rescue Async::Stop
|
|
580
|
-
rescue *CONNECTION_LOST
|
|
581
|
-
# connection closed
|
|
582
|
-
end
|
|
445
|
+
def maybe_reconnect(endpoint)
|
|
446
|
+
return unless endpoint && @dialed.include?(endpoint)
|
|
447
|
+
return unless @state == :open && @reconnect_enabled
|
|
448
|
+
Reconnect.schedule(endpoint, @options, @parent_task, self)
|
|
583
449
|
end
|
|
584
450
|
|
|
585
451
|
|
|
586
|
-
# Spawns a background task that reconnects to the given endpoint
|
|
587
|
-
# with exponential back-off based on the reconnect_interval option.
|
|
588
|
-
#
|
|
589
|
-
# @param endpoint [String] endpoint to reconnect to
|
|
590
|
-
# @param delay [Numeric, nil] initial delay in seconds (defaults to reconnect_interval)
|
|
591
|
-
#
|
|
592
452
|
def schedule_reconnect(endpoint, delay: nil)
|
|
593
|
-
|
|
594
|
-
if ri.is_a?(Range)
|
|
595
|
-
delay ||= ri.begin
|
|
596
|
-
max_delay = ri.end
|
|
597
|
-
else
|
|
598
|
-
delay ||= ri
|
|
599
|
-
max_delay = nil
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
@tasks << @parent_task.async(transient: true, annotation: "reconnect #{endpoint}") do
|
|
603
|
-
loop do
|
|
604
|
-
break if @closed
|
|
605
|
-
sleep delay if delay > 0
|
|
606
|
-
break if @closed
|
|
607
|
-
begin
|
|
608
|
-
transport = transport_for(endpoint)
|
|
609
|
-
transport.connect(endpoint, self)
|
|
610
|
-
break # connected successfully
|
|
611
|
-
rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
|
|
612
|
-
delay = [delay * 2, max_delay].min if max_delay
|
|
613
|
-
# After first attempt with delay: 0, use the configured interval
|
|
614
|
-
delay = ri.is_a?(Range) ? ri.begin : ri if delay == 0
|
|
615
|
-
emit_monitor_event(:connect_retried, endpoint: endpoint, detail: { interval: delay })
|
|
616
|
-
end
|
|
617
|
-
end
|
|
618
|
-
rescue Async::Stop
|
|
619
|
-
# normal shutdown
|
|
620
|
-
rescue => error
|
|
621
|
-
signal_fatal_error(error)
|
|
622
|
-
end
|
|
453
|
+
Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
|
|
623
454
|
end
|
|
624
455
|
|
|
625
456
|
|
|
626
|
-
# Eagerly validates TCP hostnames so resolution errors fail
|
|
627
|
-
# on connect, not silently in the background reconnect loop.
|
|
628
|
-
# Reconnects still re-resolve (DNS may change), and transient
|
|
629
|
-
# resolution failures during reconnect are retried with backoff.
|
|
630
|
-
#
|
|
631
457
|
def validate_endpoint!(endpoint)
|
|
632
458
|
transport = transport_for(endpoint)
|
|
633
459
|
transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
|
|
634
460
|
end
|
|
635
461
|
|
|
636
462
|
|
|
637
|
-
|
|
638
|
-
def transport_for(endpoint)
|
|
639
|
-
scheme = endpoint[/\A([^:]+):\/\//, 1]
|
|
640
|
-
self.class.transports[scheme] or
|
|
641
|
-
raise ArgumentError, "unsupported transport: #{endpoint}"
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
# Delegates accept loop startup to the listener.
|
|
646
|
-
#
|
|
647
|
-
# Stream-based listeners (TCP, IPC, TLS, …) implement
|
|
648
|
-
# +#start_accept_loops+. Inproc listeners do not — connections
|
|
649
|
-
# are established synchronously during +connect+.
|
|
650
|
-
#
|
|
651
463
|
def start_accept_loops(listener)
|
|
652
464
|
return unless listener.respond_to?(:start_accept_loops)
|
|
653
465
|
listener.start_accept_loops(@parent_task) do |io|
|
|
@@ -656,6 +468,35 @@ module OMQ
|
|
|
656
468
|
end
|
|
657
469
|
|
|
658
470
|
|
|
471
|
+
def stop_listeners
|
|
472
|
+
@listeners.each(&:stop)
|
|
473
|
+
@listeners.clear
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def close_connections
|
|
478
|
+
@connections.each_key(&:close)
|
|
479
|
+
@connections.clear
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def close_connections_at(endpoint)
|
|
484
|
+
conns = @connections.filter_map { |conn, e| conn if e.endpoint == endpoint }
|
|
485
|
+
conns.each do |conn|
|
|
486
|
+
@connections.delete(conn)
|
|
487
|
+
@routing.connection_removed(conn)
|
|
488
|
+
conn.close
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def stop_tasks
|
|
494
|
+
@routing.stop rescue nil
|
|
495
|
+
@tasks.each { |t| t.stop rescue nil }
|
|
496
|
+
@tasks.clear
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
|
|
659
500
|
def freeze_error_lists!
|
|
660
501
|
return if OMQ::CONNECTION_LOST.frozen?
|
|
661
502
|
OMQ::CONNECTION_LOST.freeze
|
|
@@ -663,19 +504,13 @@ module OMQ
|
|
|
663
504
|
end
|
|
664
505
|
|
|
665
506
|
|
|
666
|
-
def emit_monitor_event(type, endpoint: nil, detail: nil)
|
|
667
|
-
return unless @monitor_queue
|
|
668
|
-
@monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
|
|
669
|
-
rescue Async::Stop, ClosedQueueError
|
|
670
|
-
end
|
|
671
|
-
|
|
672
|
-
|
|
673
507
|
def close_monitor_queue
|
|
674
508
|
return unless @monitor_queue
|
|
675
509
|
@monitor_queue.push(nil)
|
|
676
510
|
end
|
|
677
511
|
end
|
|
678
512
|
|
|
513
|
+
|
|
679
514
|
# Register built-in transports.
|
|
680
515
|
Engine.transports["tcp"] = Transport::TCP
|
|
681
516
|
Engine.transports["ipc"] = Transport::IPC
|
data/lib/omq/options.rb
CHANGED
|
@@ -9,6 +9,7 @@ module OMQ
|
|
|
9
9
|
class Options
|
|
10
10
|
DEFAULT_HWM = 1000
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
# @param linger [Integer] linger period in seconds (default 0)
|
|
13
14
|
#
|
|
14
15
|
def initialize(linger: 0)
|
|
@@ -30,6 +31,40 @@ module OMQ
|
|
|
30
31
|
@qos = 0 # 0 = fire-and-forget, 1 = at-least-once (see omq-qos gem)
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
|
|
35
|
+
# @!attribute send_hwm
|
|
36
|
+
# @return [Integer] send high water mark (default 1000, 0 = unbounded)
|
|
37
|
+
# @!attribute recv_hwm
|
|
38
|
+
# @return [Integer] receive high water mark (default 1000, 0 = unbounded)
|
|
39
|
+
# @!attribute linger
|
|
40
|
+
# @return [Integer, nil] linger period in seconds (nil = wait forever, 0 = immediate)
|
|
41
|
+
# @!attribute identity
|
|
42
|
+
# @return [String] socket identity for ROUTER addressing (default "")
|
|
43
|
+
# @!attribute router_mandatory
|
|
44
|
+
# @return [Boolean] raise on unroutable messages (default false)
|
|
45
|
+
# @!attribute conflate
|
|
46
|
+
# @return [Boolean] keep only the latest message per topic (default false)
|
|
47
|
+
# @!attribute read_timeout
|
|
48
|
+
# @return [Numeric, nil] read timeout in seconds (nil = no timeout)
|
|
49
|
+
# @!attribute write_timeout
|
|
50
|
+
# @return [Numeric, nil] write timeout in seconds (nil = no timeout)
|
|
51
|
+
# @!attribute reconnect_interval
|
|
52
|
+
# @return [Numeric, Range] reconnect interval in seconds, or Range for exponential backoff
|
|
53
|
+
# @!attribute heartbeat_interval
|
|
54
|
+
# @return [Numeric, nil] PING interval in seconds (nil = disabled)
|
|
55
|
+
# @!attribute heartbeat_ttl
|
|
56
|
+
# @return [Numeric, nil] TTL advertised in PING (nil = use heartbeat_interval)
|
|
57
|
+
# @!attribute heartbeat_timeout
|
|
58
|
+
# @return [Numeric, nil] time without traffic before closing (nil = use heartbeat_interval)
|
|
59
|
+
# @!attribute max_message_size
|
|
60
|
+
# @return [Integer, nil] maximum message size in bytes
|
|
61
|
+
# @!attribute on_mute
|
|
62
|
+
# @return [Symbol] mute strategy (:block, :drop_newest, :drop_oldest)
|
|
63
|
+
# @!attribute mechanism
|
|
64
|
+
# @return [Protocol::ZMTP::Mechanism::Null, Protocol::ZMTP::Mechanism::Curve] security mechanism
|
|
65
|
+
# @!attribute qos
|
|
66
|
+
# @return [Integer] quality of service level (0 = fire-and-forget)
|
|
67
|
+
#
|
|
33
68
|
attr_accessor :send_hwm, :recv_hwm,
|
|
34
69
|
:linger, :identity,
|
|
35
70
|
:router_mandatory, :conflate,
|
|
@@ -41,10 +76,19 @@ module OMQ
|
|
|
41
76
|
:mechanism,
|
|
42
77
|
:qos
|
|
43
78
|
|
|
79
|
+
# @return [Boolean] true if router_mandatory is set
|
|
44
80
|
alias_method :router_mandatory?, :router_mandatory
|
|
81
|
+
|
|
82
|
+
# @return [Numeric, nil] alias for #read_timeout
|
|
45
83
|
alias_method :recv_timeout, :read_timeout
|
|
84
|
+
|
|
85
|
+
# @param val [Numeric, nil] alias for #read_timeout=
|
|
46
86
|
alias_method :recv_timeout=, :read_timeout=
|
|
87
|
+
|
|
88
|
+
# @return [Numeric, nil] alias for #write_timeout
|
|
47
89
|
alias_method :send_timeout, :write_timeout
|
|
90
|
+
|
|
91
|
+
# @param val [Numeric, nil] alias for #write_timeout=
|
|
48
92
|
alias_method :send_timeout=, :write_timeout=
|
|
49
93
|
end
|
|
50
94
|
end
|
data/lib/omq/pair.rb
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
|
+
# PAIR socket — exclusive 1-to-1 bidirectional communication.
|
|
5
|
+
#
|
|
4
6
|
class PAIR < Socket
|
|
5
7
|
include Readable
|
|
6
8
|
include Writable
|
|
7
9
|
|
|
10
|
+
# @param endpoints [String, nil] endpoint to bind/connect
|
|
11
|
+
# @param linger [Integer] linger period in seconds
|
|
12
|
+
# @param backend [Symbol, nil] :ruby (default) or :ffi
|
|
13
|
+
#
|
|
8
14
|
def initialize(endpoints = nil, linger: 0, backend: nil)
|
|
9
15
|
_init_engine(:PAIR, linger: linger, backend: backend)
|
|
10
16
|
_attach(endpoints, default: :connect)
|