omq 0.12.0 → 0.13.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 +49 -1
- data/lib/omq/engine/connection_setup.rb +47 -0
- data/lib/omq/engine/heartbeat.rb +40 -0
- data/lib/omq/engine/reconnect.rb +56 -0
- data/lib/omq/engine/recv_pump.rb +76 -0
- data/lib/omq/engine.rb +104 -311
- data/lib/omq/routing/conn_send_pump.rb +36 -0
- data/lib/omq/routing/dealer.rb +8 -10
- data/lib/omq/routing/fair_queue.rb +144 -0
- data/lib/omq/routing/fair_recv.rb +27 -0
- data/lib/omq/routing/fan_out.rb +113 -72
- data/lib/omq/routing/pair.rb +39 -20
- data/lib/omq/routing/pub.rb +5 -7
- data/lib/omq/routing/pull.rb +5 -4
- data/lib/omq/routing/push.rb +3 -12
- data/lib/omq/routing/rep.rb +31 -51
- data/lib/omq/routing/req.rb +8 -10
- data/lib/omq/routing/round_robin.rb +82 -64
- data/lib/omq/routing/router.rb +23 -48
- data/lib/omq/routing/sub.rb +8 -5
- data/lib/omq/routing/xpub.rb +7 -3
- data/lib/omq/routing/xsub.rb +43 -27
- data/lib/omq/routing.rb +3 -0
- data/lib/omq/socket.rb +2 -2
- data/lib/omq/transport/inproc/direct_pipe.rb +162 -0
- data/lib/omq/transport/inproc.rb +37 -218
- data/lib/omq/version.rb +1 -1
- metadata +9 -1
data/lib/omq/engine.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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"
|
|
4
8
|
|
|
5
9
|
module OMQ
|
|
6
10
|
# Per-socket orchestrator.
|
|
@@ -19,6 +23,12 @@ module OMQ
|
|
|
19
23
|
attr_reader :transports
|
|
20
24
|
end
|
|
21
25
|
|
|
26
|
+
# Per-connection metadata: the endpoint it was established on and an
|
|
27
|
+
# optional Promise resolved when the connection is lost (used by
|
|
28
|
+
# {#spawn_connection} to await connection teardown).
|
|
29
|
+
#
|
|
30
|
+
ConnectionRecord = Data.define(:endpoint, :done)
|
|
31
|
+
|
|
22
32
|
|
|
23
33
|
# @return [Symbol] socket type (e.g. :REQ, :PAIR)
|
|
24
34
|
#
|
|
@@ -49,33 +59,32 @@ module OMQ
|
|
|
49
59
|
# @param options [Options]
|
|
50
60
|
#
|
|
51
61
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
|
|
62
|
+
@socket_type = socket_type
|
|
63
|
+
@options = options
|
|
64
|
+
@routing = Routing.for(socket_type).new(self)
|
|
65
|
+
@connections = {} # connection => ConnectionRecord
|
|
66
|
+
@dialed = Set.new # endpoints we called connect() on (reconnect intent)
|
|
67
|
+
@listeners = []
|
|
68
|
+
@tasks = []
|
|
69
|
+
@state = :open
|
|
70
|
+
@last_endpoint = nil
|
|
71
|
+
@last_tcp_port = nil
|
|
72
|
+
@peer_connected = Async::Promise.new
|
|
73
|
+
@all_peers_gone = Async::Promise.new
|
|
74
|
+
@reconnect_enabled = true
|
|
75
|
+
@parent_task = nil
|
|
76
|
+
@on_io_thread = false
|
|
77
|
+
@fatal_error = nil
|
|
78
|
+
@monitor_queue = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task, :tasks
|
|
76
83
|
|
|
77
84
|
attr_writer :reconnect_enabled, :monitor_queue
|
|
78
85
|
|
|
86
|
+
def closed? = @state == :closed
|
|
87
|
+
|
|
79
88
|
# Optional proc that wraps new connections (e.g. for serialization).
|
|
80
89
|
# Called with the raw connection; must return the (possibly wrapped) connection.
|
|
81
90
|
#
|
|
@@ -126,7 +135,7 @@ module OMQ
|
|
|
126
135
|
def connect(endpoint)
|
|
127
136
|
freeze_error_lists!
|
|
128
137
|
validate_endpoint!(endpoint)
|
|
129
|
-
@
|
|
138
|
+
@dialed.add(endpoint)
|
|
130
139
|
if endpoint.start_with?("inproc://")
|
|
131
140
|
# Inproc connect is synchronous and instant
|
|
132
141
|
transport = transport_for(endpoint)
|
|
@@ -146,14 +155,8 @@ module OMQ
|
|
|
146
155
|
# @return [void]
|
|
147
156
|
#
|
|
148
157
|
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
|
|
158
|
+
@dialed.delete(endpoint)
|
|
159
|
+
close_connections_at(endpoint)
|
|
157
160
|
end
|
|
158
161
|
|
|
159
162
|
|
|
@@ -168,15 +171,7 @@ module OMQ
|
|
|
168
171
|
return unless listener
|
|
169
172
|
listener.stop
|
|
170
173
|
@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
|
|
174
|
+
close_connections_at(endpoint)
|
|
180
175
|
end
|
|
181
176
|
|
|
182
177
|
|
|
@@ -211,8 +206,7 @@ module OMQ
|
|
|
211
206
|
#
|
|
212
207
|
def connection_ready(pipe, endpoint: nil)
|
|
213
208
|
pipe = @connection_wrapper.call(pipe) if @connection_wrapper
|
|
214
|
-
@connections
|
|
215
|
-
@connection_endpoints[pipe] = endpoint if endpoint
|
|
209
|
+
@connections[pipe] = ConnectionRecord.new(endpoint: endpoint, done: nil)
|
|
216
210
|
@routing.connection_added(pipe)
|
|
217
211
|
@peer_connected.resolve(pipe)
|
|
218
212
|
emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
|
|
@@ -273,77 +267,17 @@ module OMQ
|
|
|
273
267
|
end
|
|
274
268
|
|
|
275
269
|
|
|
276
|
-
# Starts a recv pump for a connection, or wires the inproc
|
|
277
|
-
# fast path when the connection is a DirectPipe.
|
|
278
|
-
#
|
|
279
|
-
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
280
|
-
# Starts a recv pump that dequeues messages from a connection
|
|
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.
|
|
270
|
+
# Starts a recv pump for a connection, or wires the inproc fast path.
|
|
287
271
|
#
|
|
288
272
|
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
289
|
-
# @param recv_queue [
|
|
273
|
+
# @param recv_queue [SignalingQueue]
|
|
290
274
|
# @yield [msg] optional per-message transform
|
|
291
|
-
# @return [
|
|
275
|
+
# @return [Async::Task, nil]
|
|
292
276
|
#
|
|
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
277
|
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
|
|
278
|
+
task = RecvPump.start(@parent_task, conn, recv_queue, self, transform)
|
|
279
|
+
@tasks << task if task
|
|
280
|
+
task
|
|
347
281
|
end
|
|
348
282
|
|
|
349
283
|
|
|
@@ -353,25 +287,13 @@ module OMQ
|
|
|
353
287
|
# @return [void]
|
|
354
288
|
#
|
|
355
289
|
def connection_lost(connection)
|
|
356
|
-
|
|
357
|
-
@connections.delete(connection)
|
|
290
|
+
entry = @connections.delete(connection)
|
|
358
291
|
@routing.connection_removed(connection)
|
|
359
292
|
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
|
|
293
|
+
emit_monitor_event(:disconnected, endpoint: entry&.endpoint)
|
|
294
|
+
entry&.done&.resolve(true)
|
|
295
|
+
@all_peers_gone.resolve(true) if @peer_connected.resolved? && @connections.empty?
|
|
296
|
+
maybe_reconnect(entry&.endpoint)
|
|
375
297
|
end
|
|
376
298
|
|
|
377
299
|
|
|
@@ -380,42 +302,15 @@ module OMQ
|
|
|
380
302
|
# @return [void]
|
|
381
303
|
#
|
|
382
304
|
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
|
|
305
|
+
return unless @state == :open
|
|
306
|
+
@state = :closing
|
|
307
|
+
stop_listeners unless @connections.empty?
|
|
308
|
+
drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
|
|
309
|
+
@state = :closed
|
|
406
310
|
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
|
|
311
|
+
stop_listeners
|
|
312
|
+
close_connections
|
|
313
|
+
stop_tasks
|
|
419
314
|
emit_monitor_event(:closed)
|
|
420
315
|
close_monitor_queue
|
|
421
316
|
end
|
|
@@ -451,13 +346,13 @@ module OMQ
|
|
|
451
346
|
# @param error [Exception]
|
|
452
347
|
#
|
|
453
348
|
def signal_fatal_error(error)
|
|
454
|
-
return
|
|
349
|
+
return unless @state == :open
|
|
455
350
|
@fatal_error = begin
|
|
456
351
|
raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
|
|
457
352
|
rescue => wrapped
|
|
458
353
|
wrapped
|
|
459
354
|
end
|
|
460
|
-
@routing.recv_queue.
|
|
355
|
+
@routing.recv_queue.push(nil) rescue nil
|
|
461
356
|
@peer_connected.resolve(nil) rescue nil
|
|
462
357
|
end
|
|
463
358
|
|
|
@@ -479,18 +374,24 @@ module OMQ
|
|
|
479
374
|
end
|
|
480
375
|
|
|
481
376
|
|
|
482
|
-
|
|
377
|
+
def emit_monitor_event(type, endpoint: nil, detail: nil)
|
|
378
|
+
return unless @monitor_queue
|
|
379
|
+
@monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
|
|
380
|
+
rescue Async::Stop, ClosedQueueError
|
|
381
|
+
end
|
|
483
382
|
|
|
383
|
+
def transport_for(endpoint)
|
|
384
|
+
scheme = endpoint[/\A([^:]+):\/\//, 1]
|
|
385
|
+
self.class.transports[scheme] or
|
|
386
|
+
raise ArgumentError, "unsupported transport: #{endpoint}"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
private
|
|
484
390
|
|
|
485
|
-
# Spawns an isolated connection task as a sibling of accept/reconnect
|
|
486
|
-
# tasks. All per-connection children (heartbeat, recv pump, reaper)
|
|
487
|
-
# live inside this task. When the connection dies, the entire subtree
|
|
488
|
-
# is cleaned up by Async.
|
|
489
|
-
#
|
|
490
391
|
def spawn_connection(io, as_server:, endpoint: nil)
|
|
491
392
|
task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
|
|
492
393
|
done = Async::Promise.new
|
|
493
|
-
conn =
|
|
394
|
+
conn = ConnectionSetup.run(io, self, as_server: as_server, endpoint: endpoint, done: done)
|
|
494
395
|
done.wait
|
|
495
396
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
496
397
|
# handshake failed or connection lost — subtree cleaned up
|
|
@@ -500,154 +401,30 @@ module OMQ
|
|
|
500
401
|
@tasks << task if task
|
|
501
402
|
end
|
|
502
403
|
|
|
503
|
-
|
|
504
|
-
# Waits for the send queue to drain.
|
|
505
|
-
#
|
|
506
|
-
# @param timeout [Numeric, nil] max seconds to wait (nil = forever)
|
|
507
|
-
#
|
|
508
404
|
def drain_send_queues(timeout)
|
|
509
|
-
return unless @routing.respond_to?(:
|
|
405
|
+
return unless @routing.respond_to?(:send_queues_drained?)
|
|
510
406
|
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
|
|
407
|
+
until @routing.send_queues_drained?
|
|
408
|
+
break if deadline && (deadline - Async::Clock.now) <= 0
|
|
517
409
|
sleep 0.001
|
|
518
410
|
end
|
|
519
411
|
end
|
|
520
412
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
# @param io [#read, #write, #close] underlying transport stream
|
|
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
|
|
413
|
+
def maybe_reconnect(endpoint)
|
|
414
|
+
return unless endpoint && @dialed.include?(endpoint)
|
|
415
|
+
return unless @state == :open && @reconnect_enabled
|
|
416
|
+
Reconnect.schedule(endpoint, @options, @parent_task, self)
|
|
553
417
|
end
|
|
554
418
|
|
|
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
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
|
|
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
419
|
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
|
|
420
|
+
Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
|
|
623
421
|
end
|
|
624
422
|
|
|
625
|
-
|
|
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
423
|
def validate_endpoint!(endpoint)
|
|
632
424
|
transport = transport_for(endpoint)
|
|
633
425
|
transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
|
|
634
426
|
end
|
|
635
427
|
|
|
636
|
-
|
|
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
428
|
def start_accept_loops(listener)
|
|
652
429
|
return unless listener.respond_to?(:start_accept_loops)
|
|
653
430
|
listener.start_accept_loops(@parent_task) do |io|
|
|
@@ -655,20 +432,36 @@ module OMQ
|
|
|
655
432
|
end
|
|
656
433
|
end
|
|
657
434
|
|
|
435
|
+
def stop_listeners
|
|
436
|
+
@listeners.each(&:stop)
|
|
437
|
+
@listeners.clear
|
|
438
|
+
end
|
|
658
439
|
|
|
659
|
-
def
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
OMQ::CONNECTION_FAILED.freeze
|
|
440
|
+
def close_connections
|
|
441
|
+
@connections.each_key(&:close)
|
|
442
|
+
@connections.clear
|
|
663
443
|
end
|
|
664
444
|
|
|
445
|
+
def close_connections_at(endpoint)
|
|
446
|
+
conns = @connections.filter_map { |conn, e| conn if e.endpoint == endpoint }
|
|
447
|
+
conns.each do |conn|
|
|
448
|
+
@connections.delete(conn)
|
|
449
|
+
@routing.connection_removed(conn)
|
|
450
|
+
conn.close
|
|
451
|
+
end
|
|
452
|
+
end
|
|
665
453
|
|
|
666
|
-
def
|
|
667
|
-
|
|
668
|
-
@
|
|
669
|
-
|
|
454
|
+
def stop_tasks
|
|
455
|
+
@routing.stop rescue nil
|
|
456
|
+
@tasks.each { |t| t.stop rescue nil }
|
|
457
|
+
@tasks.clear
|
|
670
458
|
end
|
|
671
459
|
|
|
460
|
+
def freeze_error_lists!
|
|
461
|
+
return if OMQ::CONNECTION_LOST.frozen?
|
|
462
|
+
OMQ::CONNECTION_LOST.freeze
|
|
463
|
+
OMQ::CONNECTION_FAILED.freeze
|
|
464
|
+
end
|
|
672
465
|
|
|
673
466
|
def close_monitor_queue
|
|
674
467
|
return unless @monitor_queue
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Starts a dedicated send pump for one per-connection send queue.
|
|
6
|
+
#
|
|
7
|
+
# Used by Router and Rep, which have per-connection queues but do not
|
|
8
|
+
# include the RoundRobin mixin.
|
|
9
|
+
#
|
|
10
|
+
class ConnSendPump
|
|
11
|
+
# Spawns the pump task and registers it in +tasks+.
|
|
12
|
+
#
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
# @param conn [Connection]
|
|
15
|
+
# @param q [Async::LimitedQueue]
|
|
16
|
+
# @param tasks [Array]
|
|
17
|
+
# @return [Async::Task]
|
|
18
|
+
#
|
|
19
|
+
def self.start(engine, conn, q, tasks)
|
|
20
|
+
task = engine.spawn_pump_task(annotation: "send pump") do
|
|
21
|
+
loop do
|
|
22
|
+
batch = [q.dequeue]
|
|
23
|
+
Routing.drain_send_queue(q, batch)
|
|
24
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
25
|
+
conn.flush
|
|
26
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
27
|
+
engine.connection_lost(conn)
|
|
28
|
+
break
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
tasks << task
|
|
32
|
+
task
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/omq/routing/dealer.rb
CHANGED
|
@@ -8,36 +8,35 @@ module OMQ
|
|
|
8
8
|
#
|
|
9
9
|
class Dealer
|
|
10
10
|
include RoundRobin
|
|
11
|
+
include FairRecv
|
|
11
12
|
|
|
12
13
|
# @param engine [Engine]
|
|
13
14
|
#
|
|
14
15
|
def initialize(engine)
|
|
15
16
|
@engine = engine
|
|
16
|
-
@recv_queue =
|
|
17
|
+
@recv_queue = FairQueue.new
|
|
17
18
|
@tasks = []
|
|
18
19
|
init_round_robin(engine)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# @return [
|
|
22
|
+
# @return [FairQueue]
|
|
22
23
|
#
|
|
23
|
-
attr_reader :recv_queue
|
|
24
|
+
attr_reader :recv_queue
|
|
24
25
|
|
|
25
26
|
# @param connection [Connection]
|
|
26
27
|
#
|
|
27
28
|
def connection_added(connection)
|
|
28
29
|
@connections << connection
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
32
|
-
@tasks << task if task
|
|
33
|
-
start_send_pump unless @send_pump_started
|
|
30
|
+
add_fair_recv_connection(connection)
|
|
31
|
+
add_round_robin_send_connection(connection)
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
# @param connection [Connection]
|
|
37
35
|
#
|
|
38
36
|
def connection_removed(connection)
|
|
39
37
|
@connections.delete(connection)
|
|
40
|
-
|
|
38
|
+
@recv_queue.remove_queue(connection)
|
|
39
|
+
remove_round_robin_send_connection(connection)
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
# @param parts [Array<String>]
|
|
@@ -51,7 +50,6 @@ module OMQ
|
|
|
51
50
|
@tasks.each(&:stop)
|
|
52
51
|
@tasks.clear
|
|
53
52
|
end
|
|
54
|
-
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
end
|