omq 0.5.0 → 0.6.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 +195 -0
- data/README.md +21 -19
- data/exe/omq +6 -0
- data/lib/omq/cli/base_runner.rb +423 -0
- data/lib/omq/cli/channel.rb +8 -0
- data/lib/omq/cli/client_server.rb +106 -0
- data/lib/omq/cli/config.rb +51 -0
- data/lib/omq/cli/formatter.rb +75 -0
- data/lib/omq/cli/pair.rb +31 -0
- data/lib/omq/cli/peer.rb +8 -0
- data/lib/omq/cli/pipe.rb +249 -0
- data/lib/omq/cli/pub_sub.rb +14 -0
- data/lib/omq/cli/push_pull.rb +14 -0
- data/lib/omq/cli/radio_dish.rb +27 -0
- data/lib/omq/cli/req_rep.rb +77 -0
- data/lib/omq/cli/router_dealer.rb +70 -0
- data/lib/omq/cli/scatter_gather.rb +14 -0
- data/lib/omq/cli.rb +444 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +2 -2
- data/lib/omq/socket.rb +74 -27
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/connection.rb +59 -12
- data/lib/omq/zmtp/engine.rb +179 -17
- data/lib/omq/zmtp/options.rb +4 -3
- data/lib/omq/zmtp/reactor.rb +25 -36
- data/lib/omq/zmtp/routing/channel.rb +14 -3
- data/lib/omq/zmtp/routing/fan_out.rb +52 -10
- data/lib/omq/zmtp/routing/pair.rb +14 -3
- data/lib/omq/zmtp/routing/peer.rb +28 -6
- data/lib/omq/zmtp/routing/push.rb +14 -7
- data/lib/omq/zmtp/routing/radio.rb +45 -12
- data/lib/omq/zmtp/routing/rep.rb +32 -13
- data/lib/omq/zmtp/routing/req.rb +1 -2
- data/lib/omq/zmtp/routing/round_robin.rb +72 -3
- data/lib/omq/zmtp/routing/router.rb +30 -10
- data/lib/omq/zmtp/routing/scatter.rb +16 -3
- data/lib/omq/zmtp/routing/server.rb +28 -6
- data/lib/omq/zmtp/routing/xsub.rb +7 -1
- data/lib/omq/zmtp/routing.rb +19 -0
- data/lib/omq/zmtp/transport/inproc.rb +48 -5
- data/lib/omq/zmtp/transport/ipc.rb +9 -7
- data/lib/omq/zmtp/transport/tcp.rb +14 -7
- data/lib/omq/zmtp/writable.rb +21 -4
- data/lib/omq.rb +7 -0
- metadata +18 -3
- data/exe/omqcat +0 -532
data/lib/omq/zmtp/connection.rb
CHANGED
|
@@ -13,14 +13,17 @@ module OMQ
|
|
|
13
13
|
#
|
|
14
14
|
attr_reader :peer_socket_type
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
# @return [String] peer's identity (from READY handshake)
|
|
17
18
|
#
|
|
18
19
|
attr_reader :peer_identity
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
# @return [Object] transport IO (#read, #write, #close)
|
|
21
23
|
#
|
|
22
24
|
attr_reader :io
|
|
23
25
|
|
|
26
|
+
|
|
24
27
|
# @param io [#read, #write, #close] transport IO
|
|
25
28
|
# @param socket_type [String] our socket type name (e.g. "REQ")
|
|
26
29
|
# @param identity [String] our identity
|
|
@@ -51,6 +54,7 @@ module OMQ
|
|
|
51
54
|
@max_message_size = max_message_size
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
|
|
54
58
|
# Performs the full ZMTP handshake via the configured mechanism.
|
|
55
59
|
#
|
|
56
60
|
# @return [void]
|
|
@@ -77,25 +81,44 @@ module OMQ
|
|
|
77
81
|
end
|
|
78
82
|
end
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
|
|
85
|
+
# Sends a multi-frame message (write + flush).
|
|
81
86
|
#
|
|
82
87
|
# @param parts [Array<String>] message frames
|
|
83
88
|
# @return [void]
|
|
84
89
|
#
|
|
85
90
|
def send_message(parts)
|
|
86
91
|
@mutex.synchronize do
|
|
87
|
-
parts
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
write_frames(parts)
|
|
93
|
+
@io.flush
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Writes a multi-frame message to the buffer without flushing.
|
|
99
|
+
# Call {#flush} after batching writes.
|
|
100
|
+
#
|
|
101
|
+
# @param parts [Array<String>] message frames
|
|
102
|
+
# @return [void]
|
|
103
|
+
#
|
|
104
|
+
def write_message(parts)
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
write_frames(parts)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Flushes the write buffer to the underlying IO.
|
|
112
|
+
#
|
|
113
|
+
# @return [void]
|
|
114
|
+
#
|
|
115
|
+
def flush
|
|
116
|
+
@mutex.synchronize do
|
|
95
117
|
@io.flush
|
|
96
118
|
end
|
|
97
119
|
end
|
|
98
120
|
|
|
121
|
+
|
|
99
122
|
# Receives a multi-frame message.
|
|
100
123
|
# PING/PONG commands are handled automatically by #read_frame.
|
|
101
124
|
#
|
|
@@ -110,12 +133,13 @@ module OMQ
|
|
|
110
133
|
yield frame if block_given?
|
|
111
134
|
next
|
|
112
135
|
end
|
|
113
|
-
frames << frame.body
|
|
136
|
+
frames << frame.body.freeze
|
|
114
137
|
break unless frame.more?
|
|
115
138
|
end
|
|
116
|
-
frames
|
|
139
|
+
frames.freeze
|
|
117
140
|
end
|
|
118
141
|
|
|
142
|
+
|
|
119
143
|
# Starts the heartbeat sender task. Call after handshake.
|
|
120
144
|
#
|
|
121
145
|
# @return [#stop, nil] the heartbeat task, or nil if disabled
|
|
@@ -123,7 +147,7 @@ module OMQ
|
|
|
123
147
|
def start_heartbeat
|
|
124
148
|
return nil unless @heartbeat_interval
|
|
125
149
|
@last_received_at = monotonic_now
|
|
126
|
-
@heartbeat_task = Reactor.spawn_pump do
|
|
150
|
+
@heartbeat_task = Reactor.spawn_pump(annotation: "heartbeat") do
|
|
127
151
|
loop do
|
|
128
152
|
sleep @heartbeat_interval
|
|
129
153
|
# Send PING with TTL
|
|
@@ -145,6 +169,7 @@ module OMQ
|
|
|
145
169
|
end
|
|
146
170
|
end
|
|
147
171
|
|
|
172
|
+
|
|
148
173
|
# Sends a command.
|
|
149
174
|
#
|
|
150
175
|
# @param command [Codec::Command]
|
|
@@ -161,6 +186,7 @@ module OMQ
|
|
|
161
186
|
end
|
|
162
187
|
end
|
|
163
188
|
|
|
189
|
+
|
|
164
190
|
# Reads one frame from the wire. Handles PING/PONG automatically.
|
|
165
191
|
# When using an encrypted mechanism, MESSAGE commands are decrypted
|
|
166
192
|
# back to ZMTP frames transparently.
|
|
@@ -200,6 +226,7 @@ module OMQ
|
|
|
200
226
|
end
|
|
201
227
|
end
|
|
202
228
|
|
|
229
|
+
|
|
203
230
|
# Closes the connection.
|
|
204
231
|
#
|
|
205
232
|
# @return [void]
|
|
@@ -211,16 +238,36 @@ module OMQ
|
|
|
211
238
|
# already closed
|
|
212
239
|
end
|
|
213
240
|
|
|
241
|
+
|
|
214
242
|
private
|
|
215
243
|
|
|
244
|
+
|
|
245
|
+
# Writes message parts as ZMTP frames, encrypting if needed.
|
|
246
|
+
#
|
|
247
|
+
# @param parts [Array<String>] message frames
|
|
248
|
+
#
|
|
249
|
+
def write_frames(parts)
|
|
250
|
+
parts.each_with_index do |part, i|
|
|
251
|
+
more = i < parts.size - 1
|
|
252
|
+
if @mechanism.encrypted?
|
|
253
|
+
@io.write(@mechanism.encrypt(part.b, more: more))
|
|
254
|
+
else
|
|
255
|
+
@io.write(Codec::Frame.new(part, more: more).to_wire)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
|
|
216
261
|
def touch_heartbeat
|
|
217
262
|
@last_received_at = monotonic_now if @heartbeat_interval
|
|
218
263
|
end
|
|
219
264
|
|
|
265
|
+
|
|
220
266
|
def monotonic_now
|
|
221
267
|
Async::Clock.now
|
|
222
268
|
end
|
|
223
269
|
|
|
270
|
+
|
|
224
271
|
# Sends one frame to the wire.
|
|
225
272
|
#
|
|
226
273
|
# @param frame [Codec::Frame]
|
data/lib/omq/zmtp/engine.rb
CHANGED
|
@@ -14,22 +14,27 @@ module OMQ
|
|
|
14
14
|
#
|
|
15
15
|
attr_reader :socket_type
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
# @return [Options] socket options
|
|
18
19
|
#
|
|
19
20
|
attr_reader :options
|
|
20
21
|
|
|
22
|
+
|
|
21
23
|
# @return [Routing] routing strategy
|
|
22
24
|
#
|
|
23
25
|
attr_reader :routing
|
|
24
26
|
|
|
27
|
+
|
|
25
28
|
# @return [String, nil] last bound endpoint
|
|
26
29
|
#
|
|
27
30
|
attr_reader :last_endpoint
|
|
28
31
|
|
|
32
|
+
|
|
29
33
|
# @return [Integer, nil] last auto-selected TCP port
|
|
30
34
|
#
|
|
31
35
|
attr_reader :last_tcp_port
|
|
32
36
|
|
|
37
|
+
|
|
33
38
|
# @param socket_type [Symbol] e.g. :REQ, :REP, :PAIR
|
|
34
39
|
# @param options [Options]
|
|
35
40
|
#
|
|
@@ -43,10 +48,24 @@ module OMQ
|
|
|
43
48
|
@listeners = []
|
|
44
49
|
@tasks = []
|
|
45
50
|
@closed = false
|
|
51
|
+
@closing = false
|
|
46
52
|
@last_endpoint = nil
|
|
47
53
|
@last_tcp_port = nil
|
|
54
|
+
@peer_connected = Async::Promise.new
|
|
55
|
+
@all_peers_gone = Async::Promise.new
|
|
56
|
+
@reconnect_enabled = true
|
|
57
|
+
@parent_task = nil
|
|
58
|
+
@connection_promises = {} # connection => Async::Promise
|
|
59
|
+
@fatal_error = nil
|
|
48
60
|
end
|
|
49
61
|
|
|
62
|
+
|
|
63
|
+
attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
attr_writer :reconnect_enabled
|
|
67
|
+
|
|
68
|
+
|
|
50
69
|
# Binds to an endpoint.
|
|
51
70
|
#
|
|
52
71
|
# @param endpoint [String] e.g. "tcp://127.0.0.1:5555", "inproc://foo"
|
|
@@ -54,6 +73,7 @@ module OMQ
|
|
|
54
73
|
# @raise [ArgumentError] on unsupported transport
|
|
55
74
|
#
|
|
56
75
|
def bind(endpoint)
|
|
76
|
+
capture_parent_task
|
|
57
77
|
transport = transport_for(endpoint)
|
|
58
78
|
listener = transport.bind(endpoint, self)
|
|
59
79
|
@listeners << listener
|
|
@@ -61,12 +81,14 @@ module OMQ
|
|
|
61
81
|
@last_tcp_port = extract_tcp_port(listener.endpoint)
|
|
62
82
|
end
|
|
63
83
|
|
|
84
|
+
|
|
64
85
|
# Connects to an endpoint.
|
|
65
86
|
#
|
|
66
87
|
# @param endpoint [String]
|
|
67
88
|
# @return [void]
|
|
68
89
|
#
|
|
69
90
|
def connect(endpoint)
|
|
91
|
+
capture_parent_task
|
|
70
92
|
@connected_endpoints << endpoint
|
|
71
93
|
if endpoint.start_with?("inproc://")
|
|
72
94
|
# Inproc connect is synchronous and instant
|
|
@@ -78,6 +100,7 @@ module OMQ
|
|
|
78
100
|
end
|
|
79
101
|
end
|
|
80
102
|
|
|
103
|
+
|
|
81
104
|
# Disconnects from an endpoint. Closes connections to that endpoint
|
|
82
105
|
# and stops auto-reconnection for it.
|
|
83
106
|
#
|
|
@@ -95,6 +118,7 @@ module OMQ
|
|
|
95
118
|
end
|
|
96
119
|
end
|
|
97
120
|
|
|
121
|
+
|
|
98
122
|
# Unbinds from an endpoint. Stops the listener and closes all
|
|
99
123
|
# connections that were accepted on it.
|
|
100
124
|
#
|
|
@@ -117,6 +141,7 @@ module OMQ
|
|
|
117
141
|
end
|
|
118
142
|
end
|
|
119
143
|
|
|
144
|
+
|
|
120
145
|
# Called by a transport when an incoming connection is accepted.
|
|
121
146
|
#
|
|
122
147
|
# @param io [#read, #write, #close]
|
|
@@ -124,18 +149,20 @@ module OMQ
|
|
|
124
149
|
# @return [void]
|
|
125
150
|
#
|
|
126
151
|
def handle_accepted(io, endpoint: nil)
|
|
127
|
-
|
|
152
|
+
spawn_connection(io, as_server: true, endpoint: endpoint)
|
|
128
153
|
end
|
|
129
154
|
|
|
155
|
+
|
|
130
156
|
# Called by a transport when an outgoing connection is established.
|
|
131
157
|
#
|
|
132
158
|
# @param io [#read, #write, #close]
|
|
133
159
|
# @return [void]
|
|
134
160
|
#
|
|
135
161
|
def handle_connected(io, endpoint: nil)
|
|
136
|
-
|
|
162
|
+
spawn_connection(io, as_server: false, endpoint: endpoint)
|
|
137
163
|
end
|
|
138
164
|
|
|
165
|
+
|
|
139
166
|
# Called by inproc transport with a pre-validated DirectPipe.
|
|
140
167
|
# Skips ZMTP handshake — just registers with routing strategy.
|
|
141
168
|
#
|
|
@@ -146,25 +173,43 @@ module OMQ
|
|
|
146
173
|
@connections << pipe
|
|
147
174
|
@connection_endpoints[pipe] = endpoint if endpoint
|
|
148
175
|
@routing.connection_added(pipe)
|
|
176
|
+
@peer_connected.resolve(pipe)
|
|
149
177
|
end
|
|
150
178
|
|
|
179
|
+
|
|
151
180
|
# Dequeues the next received message. Blocks until available.
|
|
152
181
|
#
|
|
153
182
|
# @return [Array<String>] message parts
|
|
183
|
+
# @raise if a background pump task crashed
|
|
154
184
|
#
|
|
155
185
|
def dequeue_recv
|
|
156
|
-
@
|
|
186
|
+
raise @fatal_error if @fatal_error
|
|
187
|
+
msg = @routing.recv_queue.dequeue
|
|
188
|
+
raise @fatal_error if msg.nil? && @fatal_error
|
|
189
|
+
msg
|
|
157
190
|
end
|
|
158
191
|
|
|
192
|
+
|
|
193
|
+
# Pushes a nil sentinel into the recv queue, unblocking a
|
|
194
|
+
# pending {#dequeue_recv} with a nil return value.
|
|
195
|
+
#
|
|
196
|
+
def dequeue_recv_sentinel
|
|
197
|
+
@routing.recv_queue.push(nil)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
159
201
|
# Enqueues a message for sending. Blocks at HWM.
|
|
160
202
|
#
|
|
161
203
|
# @param parts [Array<String>]
|
|
162
204
|
# @return [void]
|
|
205
|
+
# @raise if a background pump task crashed
|
|
163
206
|
#
|
|
164
207
|
def enqueue_send(parts)
|
|
208
|
+
raise @fatal_error if @fatal_error
|
|
165
209
|
@routing.enqueue(parts)
|
|
166
210
|
end
|
|
167
211
|
|
|
212
|
+
|
|
168
213
|
# Starts a recv pump for a connection, or wires the inproc
|
|
169
214
|
# fast path when the connection is a DirectPipe.
|
|
170
215
|
#
|
|
@@ -180,17 +225,22 @@ module OMQ
|
|
|
180
225
|
return nil
|
|
181
226
|
end
|
|
182
227
|
|
|
183
|
-
Reactor.spawn_pump do
|
|
228
|
+
Reactor.spawn_pump(annotation: "recv pump") do
|
|
184
229
|
loop do
|
|
185
230
|
msg = conn.receive_message
|
|
186
|
-
msg = transform ? transform.call(msg) : msg
|
|
231
|
+
msg = transform ? transform.call(msg).freeze : msg
|
|
187
232
|
recv_queue.enqueue(msg)
|
|
188
233
|
end
|
|
189
|
-
rescue
|
|
234
|
+
rescue Async::Stop
|
|
235
|
+
# normal shutdown
|
|
236
|
+
rescue ProtocolError, *CONNECTION_LOST
|
|
190
237
|
connection_lost(conn)
|
|
238
|
+
rescue => error
|
|
239
|
+
signal_fatal_error(error)
|
|
191
240
|
end
|
|
192
241
|
end
|
|
193
242
|
|
|
243
|
+
|
|
194
244
|
# Called when a connection is lost.
|
|
195
245
|
#
|
|
196
246
|
# @param connection [Connection]
|
|
@@ -202,41 +252,133 @@ module OMQ
|
|
|
202
252
|
@routing.connection_removed(connection)
|
|
203
253
|
connection.close
|
|
204
254
|
|
|
255
|
+
# Signal the connection task to exit.
|
|
256
|
+
done = @connection_promises.delete(connection)
|
|
257
|
+
done&.resolve(true)
|
|
258
|
+
|
|
259
|
+
# Resolve all_peers_gone once: had peers, now have none.
|
|
260
|
+
if @peer_connected.resolved? && @connections.empty?
|
|
261
|
+
@all_peers_gone.resolve(true)
|
|
262
|
+
end
|
|
263
|
+
|
|
205
264
|
# Auto-reconnect if this was a connected (not bound) endpoint
|
|
206
|
-
if endpoint && @connected_endpoints.include?(endpoint) && !@closed
|
|
265
|
+
if endpoint && @connected_endpoints.include?(endpoint) && !@closed && !@closing && @reconnect_enabled
|
|
207
266
|
schedule_reconnect(endpoint)
|
|
208
267
|
end
|
|
209
268
|
end
|
|
210
269
|
|
|
270
|
+
|
|
211
271
|
# Closes all connections and listeners.
|
|
212
272
|
#
|
|
213
273
|
# @return [void]
|
|
214
274
|
#
|
|
215
275
|
def close
|
|
216
|
-
return if @closed
|
|
217
|
-
@
|
|
276
|
+
return if @closed || @closing
|
|
277
|
+
@closing = true
|
|
278
|
+
|
|
279
|
+
# Stop accepting new connections — but only if we already have
|
|
280
|
+
# peers to drain to. With zero connections the listeners must
|
|
281
|
+
# stay open so late-arriving peers can still receive queued
|
|
282
|
+
# messages during the linger period.
|
|
283
|
+
unless @connections.empty?
|
|
284
|
+
@listeners.each(&:stop)
|
|
285
|
+
@listeners.clear
|
|
286
|
+
end
|
|
218
287
|
|
|
219
288
|
# Linger: wait for send queues to drain before closing.
|
|
220
289
|
# linger=0 → close immediately, linger=nil → wait forever.
|
|
290
|
+
# @closed is set AFTER draining so reconnect tasks keep
|
|
291
|
+
# running during the linger period.
|
|
221
292
|
linger = @options.linger
|
|
222
293
|
if linger.nil? || linger > 0
|
|
223
294
|
drain_timeout = linger # nil = wait forever, >0 = seconds
|
|
224
295
|
drain_send_queues(drain_timeout)
|
|
225
296
|
end
|
|
226
297
|
|
|
298
|
+
@closed = true
|
|
299
|
+
|
|
300
|
+
# Stop any remaining listeners.
|
|
301
|
+
@listeners.each(&:stop)
|
|
302
|
+
@listeners.clear
|
|
303
|
+
|
|
227
304
|
# Close connections — causes pump tasks to get EOFError/IOError
|
|
228
305
|
@connections.each(&:close)
|
|
229
306
|
@connections.clear
|
|
230
|
-
@listeners.each(&:stop)
|
|
231
|
-
@listeners.clear
|
|
232
307
|
# Stop any remaining pump tasks
|
|
233
308
|
@routing.stop rescue nil
|
|
234
309
|
@tasks.each { |t| t.stop rescue nil }
|
|
235
310
|
@tasks.clear
|
|
236
311
|
end
|
|
237
312
|
|
|
313
|
+
|
|
314
|
+
# Spawns a transient pump task with error propagation.
|
|
315
|
+
#
|
|
316
|
+
# Unexpected exceptions are caught and forwarded to
|
|
317
|
+
# {#signal_fatal_error} so blocked callers (send/recv)
|
|
318
|
+
# see the real error instead of deadlocking.
|
|
319
|
+
#
|
|
320
|
+
# @param annotation [String] task annotation for debugging
|
|
321
|
+
# @yield the pump loop body
|
|
322
|
+
# @return [Async::Task]
|
|
323
|
+
#
|
|
324
|
+
def spawn_pump_task(annotation:, &block)
|
|
325
|
+
@parent_task.async(transient: true, annotation: annotation) do
|
|
326
|
+
yield
|
|
327
|
+
rescue Async::Stop, ProtocolError, *CONNECTION_LOST
|
|
328
|
+
# normal shutdown / expected disconnect
|
|
329
|
+
rescue => error
|
|
330
|
+
signal_fatal_error(error)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# Wraps an unexpected pump error as {OMQ::SocketDeadError} and
|
|
336
|
+
# unblocks any callers waiting on the recv queue.
|
|
337
|
+
#
|
|
338
|
+
# Must be called from inside a rescue block so that +error+ is
|
|
339
|
+
# +$!+ and Ruby sets it as +#cause+ on the new exception.
|
|
340
|
+
#
|
|
341
|
+
# @param error [Exception]
|
|
342
|
+
#
|
|
343
|
+
def signal_fatal_error(error)
|
|
344
|
+
return if @closing || @closed
|
|
345
|
+
@fatal_error = begin
|
|
346
|
+
raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
|
|
347
|
+
rescue => wrapped
|
|
348
|
+
wrapped
|
|
349
|
+
end
|
|
350
|
+
@routing.recv_queue.enqueue(nil) rescue nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
|
|
238
354
|
private
|
|
239
355
|
|
|
356
|
+
|
|
357
|
+
# Saves the current Async task so connection subtrees can be
|
|
358
|
+
# spawned as siblings of the caller's task.
|
|
359
|
+
#
|
|
360
|
+
def capture_parent_task
|
|
361
|
+
@parent_task ||= Async::Task.current? ? Async::Task.current : nil
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# Spawns an isolated connection task as a sibling of accept/reconnect
|
|
366
|
+
# tasks. All per-connection children (heartbeat, recv pump, reaper)
|
|
367
|
+
# live inside this task. When the connection dies, the entire subtree
|
|
368
|
+
# is cleaned up by Async.
|
|
369
|
+
#
|
|
370
|
+
def spawn_connection(io, as_server:, endpoint: nil)
|
|
371
|
+
task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
|
|
372
|
+
done = Async::Promise.new
|
|
373
|
+
setup_connection(io, as_server: as_server, endpoint: endpoint, done: done)
|
|
374
|
+
done.wait
|
|
375
|
+
rescue ProtocolError, *CONNECTION_LOST
|
|
376
|
+
# handshake failed or connection lost — subtree cleaned up
|
|
377
|
+
end
|
|
378
|
+
@tasks << task if task
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
|
|
240
382
|
# Waits for the send queue to drain.
|
|
241
383
|
#
|
|
242
384
|
# @param timeout [Numeric, nil] max seconds to wait (nil = forever)
|
|
@@ -245,7 +387,8 @@ module OMQ
|
|
|
245
387
|
return unless @routing.respond_to?(:send_queue)
|
|
246
388
|
deadline = timeout ? Async::Clock.now + timeout : nil
|
|
247
389
|
|
|
248
|
-
until @routing.send_queue.empty?
|
|
390
|
+
until @routing.send_queue.empty? &&
|
|
391
|
+
(!@routing.respond_to?(:send_pump_idle?) || @routing.send_pump_idle?)
|
|
249
392
|
if deadline
|
|
250
393
|
remaining = deadline - Async::Clock.now
|
|
251
394
|
break if remaining <= 0
|
|
@@ -254,12 +397,21 @@ module OMQ
|
|
|
254
397
|
end
|
|
255
398
|
end
|
|
256
399
|
|
|
257
|
-
|
|
400
|
+
|
|
401
|
+
# Performs the ZMTP handshake, starts heartbeating, and registers
|
|
402
|
+
# the new connection with the routing strategy.
|
|
403
|
+
#
|
|
404
|
+
# @param io [#read, #write, #close] underlying transport stream
|
|
405
|
+
# @param as_server [Boolean] whether we are the ZMTP server side
|
|
406
|
+
# @param endpoint [String, nil] endpoint for reconnection tracking
|
|
407
|
+
# @param done [Async::Promise, nil] resolved when the connection is lost
|
|
408
|
+
#
|
|
409
|
+
def setup_connection(io, as_server:, endpoint: nil, done: nil)
|
|
258
410
|
conn = Connection.new(
|
|
259
411
|
io,
|
|
260
|
-
socket_type:
|
|
261
|
-
identity:
|
|
262
|
-
as_server:
|
|
412
|
+
socket_type: @socket_type.to_s,
|
|
413
|
+
identity: @options.identity,
|
|
414
|
+
as_server: as_server,
|
|
263
415
|
mechanism: @options.mechanism,
|
|
264
416
|
heartbeat_interval: @options.heartbeat_interval,
|
|
265
417
|
heartbeat_ttl: @options.heartbeat_ttl,
|
|
@@ -270,12 +422,21 @@ module OMQ
|
|
|
270
422
|
conn.start_heartbeat
|
|
271
423
|
@connections << conn
|
|
272
424
|
@connection_endpoints[conn] = endpoint if endpoint
|
|
425
|
+
@connection_promises[conn] = done if done
|
|
273
426
|
@routing.connection_added(conn)
|
|
427
|
+
@peer_connected.resolve(conn)
|
|
274
428
|
rescue ProtocolError, *CONNECTION_LOST
|
|
275
429
|
conn&.close
|
|
276
430
|
raise
|
|
277
431
|
end
|
|
278
432
|
|
|
433
|
+
|
|
434
|
+
# Spawns a background task that reconnects to the given endpoint
|
|
435
|
+
# with exponential back-off based on the reconnect_interval option.
|
|
436
|
+
#
|
|
437
|
+
# @param endpoint [String] endpoint to reconnect to
|
|
438
|
+
# @param delay [Numeric, nil] initial delay in seconds (defaults to reconnect_interval)
|
|
439
|
+
#
|
|
279
440
|
def schedule_reconnect(endpoint, delay: nil)
|
|
280
441
|
ri = @options.reconnect_interval
|
|
281
442
|
if ri.is_a?(Range)
|
|
@@ -286,7 +447,7 @@ module OMQ
|
|
|
286
447
|
max_delay = nil
|
|
287
448
|
end
|
|
288
449
|
|
|
289
|
-
@tasks << Reactor.spawn_pump do
|
|
450
|
+
@tasks << Reactor.spawn_pump(annotation: "reconnect #{endpoint}") do
|
|
290
451
|
loop do
|
|
291
452
|
break if @closed
|
|
292
453
|
sleep delay if delay > 0
|
|
@@ -314,6 +475,7 @@ module OMQ
|
|
|
314
475
|
end
|
|
315
476
|
end
|
|
316
477
|
|
|
478
|
+
|
|
317
479
|
def extract_tcp_port(endpoint)
|
|
318
480
|
return nil unless endpoint&.start_with?("tcp://")
|
|
319
481
|
port = endpoint.split(":").last.to_i
|
data/lib/omq/zmtp/options.rb
CHANGED
|
@@ -24,13 +24,14 @@ module OMQ
|
|
|
24
24
|
@heartbeat_interval = nil # seconds, nil = disabled
|
|
25
25
|
@heartbeat_ttl = nil # seconds, nil = use heartbeat_interval
|
|
26
26
|
@heartbeat_timeout = nil # seconds, nil = use heartbeat_interval
|
|
27
|
-
@max_message_size
|
|
28
|
-
@
|
|
27
|
+
@max_message_size = nil # bytes, nil = unlimited
|
|
28
|
+
@conflate = false
|
|
29
|
+
@mechanism = Mechanism::Null.new
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
attr_accessor :send_hwm, :recv_hwm,
|
|
32
33
|
:linger, :identity,
|
|
33
|
-
:router_mandatory,
|
|
34
|
+
:router_mandatory, :conflate,
|
|
34
35
|
:read_timeout, :write_timeout,
|
|
35
36
|
:reconnect_interval,
|
|
36
37
|
:heartbeat_interval, :heartbeat_ttl, :heartbeat_timeout,
|
data/lib/omq/zmtp/reactor.rb
CHANGED
|
@@ -12,11 +12,9 @@ module OMQ
|
|
|
12
12
|
# tasks — mirroring libzmq's IO thread architecture.
|
|
13
13
|
#
|
|
14
14
|
module Reactor
|
|
15
|
-
@work_queue =
|
|
15
|
+
@work_queue = Async::Queue.new
|
|
16
16
|
@thread = nil
|
|
17
17
|
@mutex = Mutex.new
|
|
18
|
-
@wake_r = nil
|
|
19
|
-
@wake_w = nil
|
|
20
18
|
|
|
21
19
|
class << self
|
|
22
20
|
# Spawns a pump task (recv loop, send loop, accept loop).
|
|
@@ -26,14 +24,13 @@ module OMQ
|
|
|
26
24
|
#
|
|
27
25
|
# @return [#stop] a stoppable handle
|
|
28
26
|
#
|
|
29
|
-
def spawn_pump(&block)
|
|
27
|
+
def spawn_pump(annotation: nil, &block)
|
|
30
28
|
if Async::Task.current?
|
|
31
|
-
Async(transient: true, &block)
|
|
29
|
+
Async(transient: true, annotation: annotation, &block)
|
|
32
30
|
else
|
|
33
31
|
handle = PumpHandle.new
|
|
34
32
|
ensure_started
|
|
35
|
-
@work_queue.push([:spawn, block, handle])
|
|
36
|
-
@wake_w.write_nonblock(".") rescue nil
|
|
33
|
+
@work_queue.push([:spawn, block, handle, annotation])
|
|
37
34
|
handle
|
|
38
35
|
end
|
|
39
36
|
end
|
|
@@ -52,7 +49,6 @@ module OMQ
|
|
|
52
49
|
result_queue = Thread::Queue.new
|
|
53
50
|
ensure_started
|
|
54
51
|
@work_queue.push([:run, block, result_queue])
|
|
55
|
-
@wake_w.write_nonblock(".") rescue nil
|
|
56
52
|
status, value = result_queue.pop
|
|
57
53
|
raise value if status == :error
|
|
58
54
|
value
|
|
@@ -66,9 +62,8 @@ module OMQ
|
|
|
66
62
|
def ensure_started
|
|
67
63
|
@mutex.synchronize do
|
|
68
64
|
return if @thread&.alive?
|
|
69
|
-
@wake_r, @wake_w = IO.pipe
|
|
70
65
|
ready = Thread::Queue.new
|
|
71
|
-
@thread = Thread.new { run_reactor(ready
|
|
66
|
+
@thread = Thread.new { run_reactor(ready) }
|
|
72
67
|
@thread.name = "omq-io"
|
|
73
68
|
ready.pop
|
|
74
69
|
end
|
|
@@ -80,42 +75,35 @@ module OMQ
|
|
|
80
75
|
#
|
|
81
76
|
def stop!
|
|
82
77
|
@work_queue.push([:stop])
|
|
83
|
-
@wake_w&.write_nonblock(".") rescue nil
|
|
84
78
|
@thread&.join(2)
|
|
85
79
|
@thread = nil
|
|
86
|
-
@wake_r&.close rescue nil
|
|
87
|
-
@wake_w&.close rescue nil
|
|
88
|
-
@wake_r = nil
|
|
89
|
-
@wake_w = nil
|
|
90
80
|
end
|
|
91
81
|
|
|
92
82
|
private
|
|
93
83
|
|
|
94
|
-
|
|
84
|
+
# Runs the shared Async reactor loop, dispatching work items.
|
|
85
|
+
#
|
|
86
|
+
# @param ready [Thread::Queue] signaled once the reactor is accepting work
|
|
87
|
+
#
|
|
88
|
+
def run_reactor(ready)
|
|
95
89
|
Async do |task|
|
|
96
90
|
ready.push(true)
|
|
97
91
|
loop do
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
_, block, result_queue = item
|
|
111
|
-
task.async do
|
|
112
|
-
result_queue.push([:ok, block.call])
|
|
113
|
-
rescue => e
|
|
114
|
-
result_queue.push([:error, e])
|
|
115
|
-
end
|
|
116
|
-
when :stop
|
|
117
|
-
return
|
|
92
|
+
item = @work_queue.dequeue
|
|
93
|
+
case item[0]
|
|
94
|
+
when :spawn
|
|
95
|
+
_, block, handle, annotation = item
|
|
96
|
+
async_task = task.async(transient: true, annotation: annotation, &block)
|
|
97
|
+
handle.task = async_task
|
|
98
|
+
when :run
|
|
99
|
+
_, block, result_queue = item
|
|
100
|
+
task.async do
|
|
101
|
+
result_queue.push([:ok, block.call])
|
|
102
|
+
rescue => e
|
|
103
|
+
result_queue.push([:error, e])
|
|
118
104
|
end
|
|
105
|
+
when :stop
|
|
106
|
+
return
|
|
119
107
|
end
|
|
120
108
|
end
|
|
121
109
|
end
|
|
@@ -129,6 +117,7 @@ module OMQ
|
|
|
129
117
|
#
|
|
130
118
|
attr_accessor :task
|
|
131
119
|
|
|
120
|
+
|
|
132
121
|
# Stops the pump task.
|
|
133
122
|
#
|
|
134
123
|
# @return [void]
|