omq 0.11.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 +143 -0
- data/README.md +3 -1
- data/lib/omq/drop_queue.rb +54 -0
- 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 +145 -371
- data/lib/omq/monitor_event.rb +16 -0
- data/lib/omq/options.rb +5 -3
- data/lib/omq/pub_sub.rb +9 -8
- 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 +116 -63
- 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 -10
- data/lib/omq/routing/rep.rb +31 -51
- data/lib/omq/routing/req.rb +15 -12
- data/lib/omq/routing/round_robin.rb +82 -72
- data/lib/omq/routing/router.rb +23 -48
- data/lib/omq/routing/sub.rb +8 -6
- data/lib/omq/routing/xpub.rb +8 -4
- data/lib/omq/routing/xsub.rb +43 -27
- data/lib/omq/routing.rb +44 -11
- data/lib/omq/socket.rb +46 -5
- data/lib/omq/transport/inproc/direct_pipe.rb +162 -0
- data/lib/omq/transport/inproc.rb +37 -200
- data/lib/omq/transport/ipc.rb +16 -4
- data/lib/omq/transport/tcp.rb +31 -8
- data/lib/omq/version.rb +1 -1
- data/lib/omq.rb +5 -19
- metadata +11 -16
- data/lib/omq/channel.rb +0 -14
- data/lib/omq/client_server.rb +0 -37
- data/lib/omq/peer.rb +0 -26
- data/lib/omq/radio_dish.rb +0 -74
- data/lib/omq/routing/channel.rb +0 -83
- data/lib/omq/routing/client.rb +0 -56
- data/lib/omq/routing/dish.rb +0 -78
- data/lib/omq/routing/gather.rb +0 -46
- data/lib/omq/routing/peer.rb +0 -101
- data/lib/omq/routing/radio.rb +0 -150
- data/lib/omq/routing/scatter.rb +0 -82
- data/lib/omq/routing/server.rb +0 -101
- data/lib/omq/scatter_gather.rb +0 -23
- data/lib/omq/single_frame.rb +0 -18
- data/lib/omq/transport/tls.rb +0 -146
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.
|
|
@@ -9,6 +13,23 @@ module OMQ
|
|
|
9
13
|
# OMQ::Socket instance. Each socket type creates one Engine.
|
|
10
14
|
#
|
|
11
15
|
class Engine
|
|
16
|
+
# Scheme → transport module registry.
|
|
17
|
+
# Plugins add entries via +Engine.transports["scheme"] = MyTransport+.
|
|
18
|
+
#
|
|
19
|
+
@transports = {}
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# @return [Hash{String => Module}] registered transports
|
|
23
|
+
attr_reader :transports
|
|
24
|
+
end
|
|
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
|
+
|
|
32
|
+
|
|
12
33
|
# @return [Symbol] socket type (e.g. :REQ, :PAIR)
|
|
13
34
|
#
|
|
14
35
|
attr_reader :socket_type
|
|
@@ -38,31 +59,31 @@ module OMQ
|
|
|
38
59
|
# @param options [Options]
|
|
39
60
|
#
|
|
40
61
|
def initialize(socket_type, options)
|
|
41
|
-
@socket_type
|
|
42
|
-
@options
|
|
43
|
-
@routing
|
|
44
|
-
@connections
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@
|
|
48
|
-
@
|
|
49
|
-
@
|
|
50
|
-
@
|
|
51
|
-
@
|
|
52
|
-
@
|
|
53
|
-
@
|
|
54
|
-
@
|
|
55
|
-
@
|
|
56
|
-
@
|
|
57
|
-
@
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
83
|
+
|
|
84
|
+
attr_writer :reconnect_enabled, :monitor_queue
|
|
85
|
+
|
|
86
|
+
def closed? = @state == :closed
|
|
66
87
|
|
|
67
88
|
# Optional proc that wraps new connections (e.g. for serialization).
|
|
68
89
|
# Called with the raw connection; must return the (possibly wrapped) connection.
|
|
@@ -92,12 +113,17 @@ module OMQ
|
|
|
92
113
|
# @raise [ArgumentError] on unsupported transport
|
|
93
114
|
#
|
|
94
115
|
def bind(endpoint)
|
|
116
|
+
freeze_error_lists!
|
|
95
117
|
transport = transport_for(endpoint)
|
|
96
118
|
listener = transport.bind(endpoint, self)
|
|
97
119
|
start_accept_loops(listener)
|
|
98
120
|
@listeners << listener
|
|
99
121
|
@last_endpoint = listener.endpoint
|
|
100
|
-
@last_tcp_port =
|
|
122
|
+
@last_tcp_port = listener.respond_to?(:port) ? listener.port : nil
|
|
123
|
+
emit_monitor_event(:listening, endpoint: listener.endpoint)
|
|
124
|
+
rescue => error
|
|
125
|
+
emit_monitor_event(:bind_failed, endpoint: endpoint, detail: { error: error })
|
|
126
|
+
raise
|
|
101
127
|
end
|
|
102
128
|
|
|
103
129
|
|
|
@@ -107,14 +133,16 @@ module OMQ
|
|
|
107
133
|
# @return [void]
|
|
108
134
|
#
|
|
109
135
|
def connect(endpoint)
|
|
136
|
+
freeze_error_lists!
|
|
110
137
|
validate_endpoint!(endpoint)
|
|
111
|
-
@
|
|
138
|
+
@dialed.add(endpoint)
|
|
112
139
|
if endpoint.start_with?("inproc://")
|
|
113
140
|
# Inproc connect is synchronous and instant
|
|
114
141
|
transport = transport_for(endpoint)
|
|
115
142
|
transport.connect(endpoint, self)
|
|
116
143
|
else
|
|
117
144
|
# TCP/IPC connect in background — never blocks the caller
|
|
145
|
+
emit_monitor_event(:connect_delayed, endpoint: endpoint)
|
|
118
146
|
schedule_reconnect(endpoint, delay: 0)
|
|
119
147
|
end
|
|
120
148
|
end
|
|
@@ -127,14 +155,8 @@ module OMQ
|
|
|
127
155
|
# @return [void]
|
|
128
156
|
#
|
|
129
157
|
def disconnect(endpoint)
|
|
130
|
-
@
|
|
131
|
-
|
|
132
|
-
conns.each do |conn|
|
|
133
|
-
@connection_endpoints.delete(conn)
|
|
134
|
-
@connections.delete(conn)
|
|
135
|
-
@routing.connection_removed(conn)
|
|
136
|
-
conn.close
|
|
137
|
-
end
|
|
158
|
+
@dialed.delete(endpoint)
|
|
159
|
+
close_connections_at(endpoint)
|
|
138
160
|
end
|
|
139
161
|
|
|
140
162
|
|
|
@@ -149,15 +171,7 @@ module OMQ
|
|
|
149
171
|
return unless listener
|
|
150
172
|
listener.stop
|
|
151
173
|
@listeners.delete(listener)
|
|
152
|
-
|
|
153
|
-
# Close connections accepted on this endpoint
|
|
154
|
-
conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
|
|
155
|
-
conns.each do |conn|
|
|
156
|
-
@connection_endpoints.delete(conn)
|
|
157
|
-
@connections.delete(conn)
|
|
158
|
-
@routing.connection_removed(conn)
|
|
159
|
-
conn.close
|
|
160
|
-
end
|
|
174
|
+
close_connections_at(endpoint)
|
|
161
175
|
end
|
|
162
176
|
|
|
163
177
|
|
|
@@ -168,6 +182,7 @@ module OMQ
|
|
|
168
182
|
# @return [void]
|
|
169
183
|
#
|
|
170
184
|
def handle_accepted(io, endpoint: nil)
|
|
185
|
+
emit_monitor_event(:accepted, endpoint: endpoint)
|
|
171
186
|
spawn_connection(io, as_server: true, endpoint: endpoint)
|
|
172
187
|
end
|
|
173
188
|
|
|
@@ -178,6 +193,7 @@ module OMQ
|
|
|
178
193
|
# @return [void]
|
|
179
194
|
#
|
|
180
195
|
def handle_connected(io, endpoint: nil)
|
|
196
|
+
emit_monitor_event(:connected, endpoint: endpoint)
|
|
181
197
|
spawn_connection(io, as_server: false, endpoint: endpoint)
|
|
182
198
|
end
|
|
183
199
|
|
|
@@ -190,10 +206,10 @@ module OMQ
|
|
|
190
206
|
#
|
|
191
207
|
def connection_ready(pipe, endpoint: nil)
|
|
192
208
|
pipe = @connection_wrapper.call(pipe) if @connection_wrapper
|
|
193
|
-
@connections
|
|
194
|
-
@connection_endpoints[pipe] = endpoint if endpoint
|
|
209
|
+
@connections[pipe] = ConnectionRecord.new(endpoint: endpoint, done: nil)
|
|
195
210
|
@routing.connection_added(pipe)
|
|
196
211
|
@peer_connected.resolve(pipe)
|
|
212
|
+
emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
|
|
197
213
|
end
|
|
198
214
|
|
|
199
215
|
|
|
@@ -251,77 +267,17 @@ module OMQ
|
|
|
251
267
|
end
|
|
252
268
|
|
|
253
269
|
|
|
254
|
-
# Starts a recv pump for a connection, or wires the inproc
|
|
255
|
-
# fast path when the connection is a DirectPipe.
|
|
256
|
-
#
|
|
257
|
-
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
258
|
-
# Starts a recv pump that dequeues messages from a connection
|
|
259
|
-
# and enqueues them into the routing strategy's recv queue.
|
|
260
|
-
#
|
|
261
|
-
# When a block is given, each message is yielded for transformation
|
|
262
|
-
# before enqueueing. The block is compiled at the call site, giving
|
|
263
|
-
# YJIT a monomorphic call per routing strategy instead of a shared
|
|
264
|
-
# megamorphic `transform.call` dispatch.
|
|
270
|
+
# Starts a recv pump for a connection, or wires the inproc fast path.
|
|
265
271
|
#
|
|
266
272
|
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
267
|
-
# @param recv_queue [
|
|
273
|
+
# @param recv_queue [SignalingQueue]
|
|
268
274
|
# @yield [msg] optional per-message transform
|
|
269
|
-
# @return [
|
|
275
|
+
# @return [Async::Task, nil]
|
|
270
276
|
#
|
|
271
|
-
# Fairness limits for the recv pump. Yield to the scheduler
|
|
272
|
-
# after reading this many messages or bytes from one connection,
|
|
273
|
-
# whichever comes first. Prevents a fast or large-message
|
|
274
|
-
# connection from starving slower peers.
|
|
275
|
-
RECV_FAIRNESS_MESSAGES = 64
|
|
276
|
-
RECV_FAIRNESS_BYTES = 1 << 20 # 1 MB
|
|
277
|
-
|
|
278
277
|
def start_recv_pump(conn, recv_queue, &transform)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return nil
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
if transform
|
|
286
|
-
@parent_task.async(transient: true, annotation: "recv pump") do |task|
|
|
287
|
-
loop do
|
|
288
|
-
count = 0
|
|
289
|
-
bytes = 0
|
|
290
|
-
while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
|
|
291
|
-
msg = conn.receive_message
|
|
292
|
-
msg = transform.call(msg).freeze
|
|
293
|
-
recv_queue.enqueue(msg)
|
|
294
|
-
count += 1
|
|
295
|
-
bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
|
|
296
|
-
end
|
|
297
|
-
task.yield
|
|
298
|
-
end
|
|
299
|
-
rescue Async::Stop
|
|
300
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
301
|
-
connection_lost(conn)
|
|
302
|
-
rescue => error
|
|
303
|
-
signal_fatal_error(error)
|
|
304
|
-
end
|
|
305
|
-
else
|
|
306
|
-
@parent_task.async(transient: true, annotation: "recv pump") do |task|
|
|
307
|
-
loop do
|
|
308
|
-
count = 0
|
|
309
|
-
bytes = 0
|
|
310
|
-
while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
|
|
311
|
-
msg = conn.receive_message
|
|
312
|
-
recv_queue.enqueue(msg)
|
|
313
|
-
count += 1
|
|
314
|
-
bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
|
|
315
|
-
end
|
|
316
|
-
task.yield
|
|
317
|
-
end
|
|
318
|
-
rescue Async::Stop
|
|
319
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
320
|
-
connection_lost(conn)
|
|
321
|
-
rescue => error
|
|
322
|
-
signal_fatal_error(error)
|
|
323
|
-
end
|
|
324
|
-
end
|
|
278
|
+
task = RecvPump.start(@parent_task, conn, recv_queue, self, transform)
|
|
279
|
+
@tasks << task if task
|
|
280
|
+
task
|
|
325
281
|
end
|
|
326
282
|
|
|
327
283
|
|
|
@@ -331,24 +287,13 @@ module OMQ
|
|
|
331
287
|
# @return [void]
|
|
332
288
|
#
|
|
333
289
|
def connection_lost(connection)
|
|
334
|
-
|
|
335
|
-
@connections.delete(connection)
|
|
290
|
+
entry = @connections.delete(connection)
|
|
336
291
|
@routing.connection_removed(connection)
|
|
337
292
|
connection.close
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
# Resolve all_peers_gone once: had peers, now have none.
|
|
344
|
-
if @peer_connected.resolved? && @connections.empty?
|
|
345
|
-
@all_peers_gone.resolve(true)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# Auto-reconnect if this was a connected (not bound) endpoint
|
|
349
|
-
if endpoint && @connected_endpoints.include?(endpoint) && !@closed && !@closing && @reconnect_enabled
|
|
350
|
-
schedule_reconnect(endpoint)
|
|
351
|
-
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)
|
|
352
297
|
end
|
|
353
298
|
|
|
354
299
|
|
|
@@ -357,42 +302,17 @@ module OMQ
|
|
|
357
302
|
# @return [void]
|
|
358
303
|
#
|
|
359
304
|
def close
|
|
360
|
-
return
|
|
361
|
-
@
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# stay open so late-arriving peers can still receive queued
|
|
366
|
-
# messages during the linger period.
|
|
367
|
-
unless @connections.empty?
|
|
368
|
-
@listeners.each(&:stop)
|
|
369
|
-
@listeners.clear
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Linger: wait for send queues to drain before closing.
|
|
373
|
-
# linger=0 → close immediately, linger=nil → wait forever.
|
|
374
|
-
# @closed is set AFTER draining so reconnect tasks keep
|
|
375
|
-
# running during the linger period.
|
|
376
|
-
linger = @options.linger
|
|
377
|
-
if linger.nil? || linger > 0
|
|
378
|
-
drain_timeout = linger # nil = wait forever, >0 = seconds
|
|
379
|
-
drain_send_queues(drain_timeout)
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
@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
|
|
383
310
|
Reactor.untrack_linger(@options.linger) if @on_io_thread
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# Close connections — causes pump tasks to get EOFError/IOError
|
|
390
|
-
@connections.each(&:close)
|
|
391
|
-
@connections.clear
|
|
392
|
-
# Stop any remaining pump tasks
|
|
393
|
-
@routing.stop rescue nil
|
|
394
|
-
@tasks.each { |t| t.stop rescue nil }
|
|
395
|
-
@tasks.clear
|
|
311
|
+
stop_listeners
|
|
312
|
+
close_connections
|
|
313
|
+
stop_tasks
|
|
314
|
+
emit_monitor_event(:closed)
|
|
315
|
+
close_monitor_queue
|
|
396
316
|
end
|
|
397
317
|
|
|
398
318
|
|
|
@@ -426,13 +346,13 @@ module OMQ
|
|
|
426
346
|
# @param error [Exception]
|
|
427
347
|
#
|
|
428
348
|
def signal_fatal_error(error)
|
|
429
|
-
return
|
|
349
|
+
return unless @state == :open
|
|
430
350
|
@fatal_error = begin
|
|
431
351
|
raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
|
|
432
352
|
rescue => wrapped
|
|
433
353
|
wrapped
|
|
434
354
|
end
|
|
435
|
-
@routing.recv_queue.
|
|
355
|
+
@routing.recv_queue.push(nil) rescue nil
|
|
436
356
|
@peer_connected.resolve(nil) rescue nil
|
|
437
357
|
end
|
|
438
358
|
|
|
@@ -454,18 +374,24 @@ module OMQ
|
|
|
454
374
|
end
|
|
455
375
|
|
|
456
376
|
|
|
457
|
-
|
|
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
|
|
458
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
|
|
459
390
|
|
|
460
|
-
# Spawns an isolated connection task as a sibling of accept/reconnect
|
|
461
|
-
# tasks. All per-connection children (heartbeat, recv pump, reaper)
|
|
462
|
-
# live inside this task. When the connection dies, the entire subtree
|
|
463
|
-
# is cleaned up by Async.
|
|
464
|
-
#
|
|
465
391
|
def spawn_connection(io, as_server:, endpoint: nil)
|
|
466
392
|
task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
|
|
467
393
|
done = Async::Promise.new
|
|
468
|
-
conn =
|
|
394
|
+
conn = ConnectionSetup.run(io, self, as_server: as_server, endpoint: endpoint, done: done)
|
|
469
395
|
done.wait
|
|
470
396
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
471
397
|
# handshake failed or connection lost — subtree cleaned up
|
|
@@ -475,228 +401,76 @@ module OMQ
|
|
|
475
401
|
@tasks << task if task
|
|
476
402
|
end
|
|
477
403
|
|
|
478
|
-
|
|
479
|
-
# Waits for the send queue to drain.
|
|
480
|
-
#
|
|
481
|
-
# @param timeout [Numeric, nil] max seconds to wait (nil = forever)
|
|
482
|
-
#
|
|
483
404
|
def drain_send_queues(timeout)
|
|
484
|
-
return unless @routing.respond_to?(:
|
|
405
|
+
return unless @routing.respond_to?(:send_queues_drained?)
|
|
485
406
|
deadline = timeout ? Async::Clock.now + timeout : nil
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if deadline
|
|
489
|
-
remaining = deadline - Async::Clock.now
|
|
490
|
-
break if remaining <= 0
|
|
491
|
-
end
|
|
407
|
+
until @routing.send_queues_drained?
|
|
408
|
+
break if deadline && (deadline - Async::Clock.now) <= 0
|
|
492
409
|
sleep 0.001
|
|
493
410
|
end
|
|
494
411
|
end
|
|
495
412
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
# @param io [#read, #write, #close] underlying transport stream
|
|
501
|
-
# @param as_server [Boolean] whether we are the ZMTP server side
|
|
502
|
-
# @param endpoint [String, nil] endpoint for reconnection tracking
|
|
503
|
-
# @param done [Async::Promise, nil] resolved when the connection is lost
|
|
504
|
-
#
|
|
505
|
-
def setup_connection(io, as_server:, endpoint: nil, done: nil)
|
|
506
|
-
conn = Protocol::ZMTP::Connection.new(
|
|
507
|
-
io,
|
|
508
|
-
socket_type: @socket_type.to_s,
|
|
509
|
-
identity: @options.identity,
|
|
510
|
-
as_server: as_server,
|
|
511
|
-
mechanism: @options.mechanism&.dup,
|
|
512
|
-
max_message_size: @options.max_message_size,
|
|
513
|
-
)
|
|
514
|
-
conn.handshake!
|
|
515
|
-
start_heartbeat(conn)
|
|
516
|
-
conn = @connection_wrapper.call(conn) if @connection_wrapper
|
|
517
|
-
@connections << conn
|
|
518
|
-
@connection_endpoints[conn] = endpoint if endpoint
|
|
519
|
-
@connection_promises[conn] = done if done
|
|
520
|
-
@routing.connection_added(conn)
|
|
521
|
-
@peer_connected.resolve(conn)
|
|
522
|
-
conn
|
|
523
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
524
|
-
conn&.close
|
|
525
|
-
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)
|
|
526
417
|
end
|
|
527
418
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
# The connection only tracks timestamps — the engine drives the loop.
|
|
531
|
-
#
|
|
532
|
-
# @param conn [Connection]
|
|
533
|
-
# @return [void]
|
|
534
|
-
#
|
|
535
|
-
def start_heartbeat(conn)
|
|
536
|
-
interval = @options.heartbeat_interval
|
|
537
|
-
return unless interval
|
|
538
|
-
|
|
539
|
-
ttl = @options.heartbeat_ttl || interval
|
|
540
|
-
timeout = @options.heartbeat_timeout || interval
|
|
541
|
-
conn.touch_heartbeat
|
|
542
|
-
|
|
543
|
-
@tasks << @parent_task.async(transient: true, annotation: "heartbeat") do
|
|
544
|
-
loop do
|
|
545
|
-
sleep interval
|
|
546
|
-
conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl, context: "".b))
|
|
547
|
-
if conn.heartbeat_expired?(timeout)
|
|
548
|
-
conn.close
|
|
549
|
-
break
|
|
550
|
-
end
|
|
551
|
-
end
|
|
552
|
-
rescue Async::Stop
|
|
553
|
-
rescue *CONNECTION_LOST
|
|
554
|
-
# connection closed
|
|
555
|
-
end
|
|
419
|
+
def schedule_reconnect(endpoint, delay: nil)
|
|
420
|
+
Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
|
|
556
421
|
end
|
|
557
422
|
|
|
423
|
+
def validate_endpoint!(endpoint)
|
|
424
|
+
transport = transport_for(endpoint)
|
|
425
|
+
transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
|
|
426
|
+
end
|
|
558
427
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
# @param delay [Numeric, nil] initial delay in seconds (defaults to reconnect_interval)
|
|
564
|
-
#
|
|
565
|
-
def schedule_reconnect(endpoint, delay: nil)
|
|
566
|
-
ri = @options.reconnect_interval
|
|
567
|
-
if ri.is_a?(Range)
|
|
568
|
-
delay ||= ri.begin
|
|
569
|
-
max_delay = ri.end
|
|
570
|
-
else
|
|
571
|
-
delay ||= ri
|
|
572
|
-
max_delay = nil
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
@tasks << @parent_task.async(transient: true, annotation: "reconnect #{endpoint}") do
|
|
576
|
-
loop do
|
|
577
|
-
break if @closed
|
|
578
|
-
sleep delay if delay > 0
|
|
579
|
-
break if @closed
|
|
580
|
-
begin
|
|
581
|
-
transport = transport_for(endpoint)
|
|
582
|
-
transport.connect(endpoint, self)
|
|
583
|
-
break # connected successfully
|
|
584
|
-
rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
|
|
585
|
-
delay = [delay * 2, max_delay].min if max_delay
|
|
586
|
-
# After first attempt with delay: 0, use the configured interval
|
|
587
|
-
delay = ri.is_a?(Range) ? ri.begin : ri if delay == 0
|
|
588
|
-
end
|
|
589
|
-
end
|
|
590
|
-
rescue Async::Stop
|
|
591
|
-
# normal shutdown
|
|
592
|
-
rescue => error
|
|
593
|
-
signal_fatal_error(error)
|
|
428
|
+
def start_accept_loops(listener)
|
|
429
|
+
return unless listener.respond_to?(:start_accept_loops)
|
|
430
|
+
listener.start_accept_loops(@parent_task) do |io|
|
|
431
|
+
handle_accepted(io, endpoint: listener.endpoint)
|
|
594
432
|
end
|
|
595
433
|
end
|
|
596
434
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
# Reconnects still re-resolve (DNS may change), and transient
|
|
601
|
-
# resolution failures during reconnect are retried with backoff.
|
|
602
|
-
#
|
|
603
|
-
def validate_endpoint!(endpoint)
|
|
604
|
-
case endpoint
|
|
605
|
-
when /\Atcp:\/\//
|
|
606
|
-
host = URI.parse(endpoint.sub("tcp://", "http://")).hostname
|
|
607
|
-
when /\Atls\+tcp:\/\//
|
|
608
|
-
host = URI.parse("http://#{endpoint.delete_prefix("tls+tcp://")}").hostname
|
|
609
|
-
else
|
|
610
|
-
return
|
|
611
|
-
end
|
|
612
|
-
Addrinfo.getaddrinfo(host, nil, nil, :STREAM) if host
|
|
435
|
+
def stop_listeners
|
|
436
|
+
@listeners.each(&:stop)
|
|
437
|
+
@listeners.clear
|
|
613
438
|
end
|
|
614
439
|
|
|
440
|
+
def close_connections
|
|
441
|
+
@connections.each_key(&:close)
|
|
442
|
+
@connections.clear
|
|
443
|
+
end
|
|
615
444
|
|
|
616
|
-
def
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
else raise ArgumentError, "unsupported transport: #{endpoint}"
|
|
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
|
|
623
451
|
end
|
|
624
452
|
end
|
|
625
453
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
port.positive? ? port : nil
|
|
454
|
+
def stop_tasks
|
|
455
|
+
@routing.stop rescue nil
|
|
456
|
+
@tasks.each { |t| t.stop rescue nil }
|
|
457
|
+
@tasks.clear
|
|
631
458
|
end
|
|
632
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
|
|
633
465
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
# IPC listeners have one. Inproc listeners have none.
|
|
638
|
-
#
|
|
639
|
-
def start_accept_loops(listener)
|
|
640
|
-
case listener
|
|
641
|
-
when Transport::TLS::Listener
|
|
642
|
-
tasks = listener.servers.map do |server|
|
|
643
|
-
@parent_task.async(transient: true, annotation: "tls accept #{listener.endpoint}") do
|
|
644
|
-
loop do
|
|
645
|
-
client = server.accept
|
|
646
|
-
Async::Task.current.defer_stop do
|
|
647
|
-
ssl = OpenSSL::SSL::SSLSocket.new(client, listener.ssl_context)
|
|
648
|
-
ssl.sync_close = true
|
|
649
|
-
ssl.accept
|
|
650
|
-
handle_accepted(IO::Stream::Buffered.wrap(ssl), endpoint: listener.endpoint)
|
|
651
|
-
rescue OpenSSL::SSL::SSLError
|
|
652
|
-
# Bad certificate, protocol mismatch, etc. — drop this
|
|
653
|
-
# connection but keep the accept loop running.
|
|
654
|
-
ssl&.close rescue nil
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
rescue Async::Stop
|
|
658
|
-
rescue IOError
|
|
659
|
-
# server closed
|
|
660
|
-
ensure
|
|
661
|
-
server.close rescue nil
|
|
662
|
-
end
|
|
663
|
-
end
|
|
664
|
-
listener.accept_tasks = tasks
|
|
665
|
-
|
|
666
|
-
when Transport::TCP::Listener
|
|
667
|
-
tasks = listener.servers.map do |server|
|
|
668
|
-
@parent_task.async(transient: true, annotation: "tcp accept #{listener.endpoint}") do
|
|
669
|
-
loop do
|
|
670
|
-
client = server.accept
|
|
671
|
-
Async::Task.current.defer_stop do
|
|
672
|
-
handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: listener.endpoint)
|
|
673
|
-
end
|
|
674
|
-
end
|
|
675
|
-
rescue Async::Stop
|
|
676
|
-
rescue IOError
|
|
677
|
-
# server closed
|
|
678
|
-
ensure
|
|
679
|
-
server.close rescue nil
|
|
680
|
-
end
|
|
681
|
-
end
|
|
682
|
-
listener.accept_tasks = tasks
|
|
683
|
-
|
|
684
|
-
when Transport::IPC::Listener
|
|
685
|
-
task = @parent_task.async(transient: true, annotation: "ipc accept #{listener.endpoint}") do
|
|
686
|
-
loop do
|
|
687
|
-
client = listener.server.accept
|
|
688
|
-
Async::Task.current.defer_stop do
|
|
689
|
-
handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: listener.endpoint)
|
|
690
|
-
end
|
|
691
|
-
end
|
|
692
|
-
rescue Async::Stop
|
|
693
|
-
rescue IOError
|
|
694
|
-
# server closed
|
|
695
|
-
ensure
|
|
696
|
-
listener.server.close rescue nil
|
|
697
|
-
end
|
|
698
|
-
listener.accept_task = task
|
|
699
|
-
end
|
|
466
|
+
def close_monitor_queue
|
|
467
|
+
return unless @monitor_queue
|
|
468
|
+
@monitor_queue.push(nil)
|
|
700
469
|
end
|
|
701
470
|
end
|
|
471
|
+
|
|
472
|
+
# Register built-in transports.
|
|
473
|
+
Engine.transports["tcp"] = Transport::TCP
|
|
474
|
+
Engine.transports["ipc"] = Transport::IPC
|
|
475
|
+
Engine.transports["inproc"] = Transport::Inproc
|
|
702
476
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
# Lifecycle event emitted by {Socket#monitor}.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute [r] type
|
|
7
|
+
# @return [Symbol] event type (:listening, :connected, :disconnected, etc.)
|
|
8
|
+
# @!attribute [r] endpoint
|
|
9
|
+
# @return [String, nil] the endpoint involved
|
|
10
|
+
# @!attribute [r] detail
|
|
11
|
+
# @return [Hash, nil] extra context (e.g. { error: }, { interval: }, etc.)
|
|
12
|
+
#
|
|
13
|
+
MonitorEvent = Data.define(:type, :endpoint, :detail) do
|
|
14
|
+
def initialize(type:, endpoint: nil, detail: nil) = super
|
|
15
|
+
end
|
|
16
|
+
end
|