nnq 0.4.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 +114 -0
- data/lib/nnq/bus.rb +37 -0
- data/lib/nnq/connection.rb +16 -6
- data/lib/nnq/engine/connection_lifecycle.rb +61 -10
- data/lib/nnq/engine/reconnect.rb +12 -3
- data/lib/nnq/engine/socket_lifecycle.rb +10 -2
- data/lib/nnq/engine.rb +77 -30
- data/lib/nnq/error.rb +26 -6
- data/lib/nnq/monitor_event.rb +3 -1
- data/lib/nnq/options.rb +10 -1
- data/lib/nnq/pair.rb +6 -1
- data/lib/nnq/pub_sub.rb +9 -2
- data/lib/nnq/push_pull.rb +9 -2
- data/lib/nnq/reactor.rb +12 -11
- data/lib/nnq/req_rep.rb +61 -13
- data/lib/nnq/routing/backtrace.rb +47 -0
- data/lib/nnq/routing/bus.rb +108 -0
- data/lib/nnq/routing/pair.rb +4 -1
- data/lib/nnq/routing/pub.rb +9 -5
- data/lib/nnq/routing/pull.rb +2 -1
- data/lib/nnq/routing/push.rb +2 -0
- data/lib/nnq/routing/rep.rb +7 -22
- data/lib/nnq/routing/rep_raw.rb +63 -0
- data/lib/nnq/routing/req.rb +7 -3
- data/lib/nnq/routing/req_raw.rb +73 -0
- data/lib/nnq/routing/respondent.rb +84 -0
- data/lib/nnq/routing/respondent_raw.rb +54 -0
- data/lib/nnq/routing/send_pump.rb +27 -6
- data/lib/nnq/routing/sub.rb +4 -0
- data/lib/nnq/routing/surveyor.rb +138 -0
- data/lib/nnq/routing/surveyor_raw.rb +107 -0
- data/lib/nnq/socket.rb +51 -8
- data/lib/nnq/surveyor_respondent.rb +98 -0
- data/lib/nnq/transport/inproc.rb +5 -0
- data/lib/nnq/transport/ipc.rb +3 -0
- data/lib/nnq/transport/tcp.rb +27 -5
- data/lib/nnq/version.rb +1 -1
- data/lib/nnq.rb +2 -0
- metadata +13 -3
data/lib/nnq/engine.rb
CHANGED
|
@@ -35,33 +35,44 @@ module NNQ
|
|
|
35
35
|
# @return [Integer] our SP protocol id (e.g. Protocols::PUSH_V0)
|
|
36
36
|
attr_reader :protocol
|
|
37
37
|
|
|
38
|
+
|
|
38
39
|
# @return [Options]
|
|
39
40
|
attr_reader :options
|
|
40
41
|
|
|
42
|
+
|
|
43
|
+
# @return [Routing strategy]
|
|
44
|
+
attr_reader :routing
|
|
45
|
+
|
|
46
|
+
|
|
41
47
|
# @return [Hash{NNQ::Connection => ConnectionLifecycle}]
|
|
42
48
|
attr_reader :connections
|
|
43
49
|
|
|
50
|
+
|
|
44
51
|
# @return [SocketLifecycle]
|
|
45
52
|
attr_reader :lifecycle
|
|
46
53
|
|
|
54
|
+
|
|
47
55
|
# @return [String, nil]
|
|
48
56
|
attr_reader :last_endpoint
|
|
49
57
|
|
|
58
|
+
|
|
50
59
|
# @return [Async::Condition] signaled when a new pipe is registered
|
|
51
60
|
attr_reader :new_pipe
|
|
52
61
|
|
|
62
|
+
|
|
53
63
|
# @return [Set<String>] endpoints we have called #connect on; used
|
|
54
64
|
# to decide whether to schedule a reconnect after a connection
|
|
55
65
|
# is lost.
|
|
56
66
|
attr_reader :dialed
|
|
57
67
|
|
|
58
|
-
# @return [Array<Async::Task>] transient tasks owned by the engine
|
|
59
|
-
# (currently just background reconnect loops). Stopped at #close.
|
|
60
|
-
attr_reader :tasks
|
|
61
|
-
|
|
62
68
|
|
|
63
69
|
# @return [Async::Queue, nil] monitor event queue (set by Socket#monitor)
|
|
64
70
|
attr_accessor :monitor_queue
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# @return [Boolean] when true, {#emit_verbose_monitor_event} forwards
|
|
74
|
+
# per-message traces (:message_sent / :message_received) to the
|
|
75
|
+
# monitor queue. Set by {Socket#monitor} via its +verbose:+ kwarg.
|
|
65
76
|
attr_accessor :verbose_monitor
|
|
66
77
|
|
|
67
78
|
|
|
@@ -80,7 +91,6 @@ module NNQ
|
|
|
80
91
|
@monitor_queue = nil
|
|
81
92
|
@verbose_monitor = false
|
|
82
93
|
@dialed = Set.new
|
|
83
|
-
@tasks = []
|
|
84
94
|
@routing = yield(self)
|
|
85
95
|
end
|
|
86
96
|
|
|
@@ -93,32 +103,51 @@ module NNQ
|
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
|
|
96
|
-
# Emits a verbose
|
|
97
|
-
#
|
|
98
|
-
|
|
106
|
+
# Emits a :message_sent verbose event. Early-returns before
|
|
107
|
+
# allocating the detail hash so the hot send path pays nothing
|
|
108
|
+
# when verbose monitoring is off.
|
|
109
|
+
def emit_verbose_msg_sent(body)
|
|
99
110
|
return unless @verbose_monitor
|
|
100
|
-
emit_monitor_event(
|
|
111
|
+
emit_monitor_event(:message_sent, detail: { body: body })
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
|
|
104
|
-
#
|
|
105
|
-
|
|
115
|
+
# Emits a :message_received verbose event. Same early-return
|
|
116
|
+
# discipline as {#emit_verbose_msg_sent}.
|
|
117
|
+
def emit_verbose_msg_received(body)
|
|
118
|
+
return unless @verbose_monitor
|
|
119
|
+
emit_monitor_event(:message_received, detail: { body: body })
|
|
120
|
+
end
|
|
106
121
|
|
|
107
122
|
|
|
108
123
|
# @return [Async::Task, nil]
|
|
109
|
-
def parent_task
|
|
124
|
+
def parent_task
|
|
125
|
+
@lifecycle.parent_task
|
|
126
|
+
end
|
|
110
127
|
|
|
111
128
|
|
|
112
|
-
|
|
129
|
+
# @return [Async::Barrier, nil]
|
|
130
|
+
def barrier
|
|
131
|
+
@lifecycle.barrier
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def closed?
|
|
136
|
+
@lifecycle.closed?
|
|
137
|
+
end
|
|
113
138
|
|
|
114
139
|
|
|
115
140
|
# @return [Async::Promise] resolves with the first connected peer
|
|
116
|
-
def peer_connected
|
|
141
|
+
def peer_connected
|
|
142
|
+
@lifecycle.peer_connected
|
|
143
|
+
end
|
|
117
144
|
|
|
118
145
|
|
|
119
146
|
# @return [Async::Promise] resolves when all peers have disconnected
|
|
120
147
|
# (edge-triggered, after at least one peer connected)
|
|
121
|
-
def all_peers_gone
|
|
148
|
+
def all_peers_gone
|
|
149
|
+
@lifecycle.all_peers_gone
|
|
150
|
+
end
|
|
122
151
|
|
|
123
152
|
|
|
124
153
|
# Called by ConnectionLifecycle teardown. Resolves `all_peers_gone`
|
|
@@ -129,7 +158,9 @@ module NNQ
|
|
|
129
158
|
|
|
130
159
|
|
|
131
160
|
# @return [Boolean]
|
|
132
|
-
def reconnect_enabled
|
|
161
|
+
def reconnect_enabled
|
|
162
|
+
@lifecycle.reconnect_enabled
|
|
163
|
+
end
|
|
133
164
|
|
|
134
165
|
|
|
135
166
|
# Disables or re-enables automatic reconnect. nnq has no reconnect
|
|
@@ -159,7 +190,7 @@ module NNQ
|
|
|
159
190
|
def bind(endpoint)
|
|
160
191
|
transport = transport_for(endpoint)
|
|
161
192
|
listener = transport.bind(endpoint, self)
|
|
162
|
-
listener.start_accept_loop(@lifecycle.
|
|
193
|
+
listener.start_accept_loop(@lifecycle.barrier) do |io, framing = :tcp|
|
|
163
194
|
handle_accepted(io, endpoint: endpoint, framing: framing)
|
|
164
195
|
end
|
|
165
196
|
@listeners << listener
|
|
@@ -175,11 +206,12 @@ module NNQ
|
|
|
175
206
|
def connect(endpoint)
|
|
176
207
|
@dialed << endpoint
|
|
177
208
|
@last_endpoint = endpoint
|
|
209
|
+
|
|
178
210
|
if endpoint.start_with?("inproc://")
|
|
179
211
|
transport_for(endpoint).connect(endpoint, self)
|
|
180
212
|
else
|
|
181
213
|
emit_monitor_event(:connect_delayed, endpoint: endpoint)
|
|
182
|
-
Reconnect.schedule(endpoint, @options, @lifecycle.
|
|
214
|
+
Reconnect.schedule(endpoint, @options, @lifecycle.barrier, self, delay: 0)
|
|
183
215
|
end
|
|
184
216
|
end
|
|
185
217
|
|
|
@@ -191,7 +223,7 @@ module NNQ
|
|
|
191
223
|
return unless endpoint && @dialed.include?(endpoint)
|
|
192
224
|
return unless @lifecycle.alive? && @lifecycle.reconnect_enabled
|
|
193
225
|
return if endpoint.start_with?("inproc://")
|
|
194
|
-
Reconnect.schedule(endpoint, @options, @lifecycle.
|
|
226
|
+
Reconnect.schedule(endpoint, @options, @lifecycle.barrier, self)
|
|
195
227
|
end
|
|
196
228
|
|
|
197
229
|
|
|
@@ -208,6 +240,7 @@ module NNQ
|
|
|
208
240
|
lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint, framing: framing)
|
|
209
241
|
lifecycle.handshake!(io)
|
|
210
242
|
spawn_recv_loop(lifecycle.conn) if @routing.respond_to?(:enqueue) && @connections.key?(lifecycle.conn)
|
|
243
|
+
lifecycle.start_supervisor!
|
|
211
244
|
rescue ConnectionRejected
|
|
212
245
|
# routing rejected this peer (e.g. PAIR already bonded) — lifecycle cleaned up
|
|
213
246
|
rescue => e
|
|
@@ -220,16 +253,19 @@ module NNQ
|
|
|
220
253
|
lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint, framing: framing)
|
|
221
254
|
lifecycle.handshake!(io)
|
|
222
255
|
spawn_recv_loop(lifecycle.conn) if @routing.respond_to?(:enqueue) && @connections.key?(lifecycle.conn)
|
|
256
|
+
lifecycle.start_supervisor!
|
|
223
257
|
rescue ConnectionRejected
|
|
224
258
|
# unusual on connect side, but handled identically
|
|
225
259
|
end
|
|
226
260
|
|
|
227
261
|
|
|
228
|
-
# Spawns a task under the
|
|
229
|
-
# strategies (e.g. PUSH send
|
|
230
|
-
# the engine's lifecycle
|
|
231
|
-
|
|
232
|
-
|
|
262
|
+
# Spawns a task under the given parent barrier (defaults to the
|
|
263
|
+
# socket-level barrier). Used by routing strategies (e.g. PUSH send
|
|
264
|
+
# pump) to attach long-lived fibers to the engine's lifecycle. The
|
|
265
|
+
# parent barrier tracks every spawned task so teardown is a single
|
|
266
|
+
# barrier.stop call.
|
|
267
|
+
def spawn_task(annotation:, parent: @lifecycle.barrier, &block)
|
|
268
|
+
parent.async(annotation: annotation, &block)
|
|
233
269
|
end
|
|
234
270
|
|
|
235
271
|
|
|
@@ -240,17 +276,22 @@ module NNQ
|
|
|
240
276
|
# to abort with IOError.
|
|
241
277
|
def close
|
|
242
278
|
return unless @lifecycle.alive?
|
|
279
|
+
|
|
243
280
|
@lifecycle.start_closing!
|
|
244
281
|
@listeners.each(&:stop)
|
|
245
|
-
@tasks.each { |t| t.stop rescue nil }
|
|
246
|
-
@tasks.clear
|
|
247
282
|
drain_send_queue(@options.linger)
|
|
248
283
|
@routing.close if @routing.respond_to?(:close)
|
|
284
|
+
|
|
249
285
|
# Tear down each remaining connection via its lifecycle. The
|
|
250
286
|
# collection mutates during iteration, so snapshot the values.
|
|
251
287
|
@connections.values.each(&:close!)
|
|
288
|
+
|
|
289
|
+
# Cascade-cancel every remaining task (reconnect loops, accept
|
|
290
|
+
# loops, supervisors) in one shot.
|
|
291
|
+
@lifecycle.barrier&.stop
|
|
252
292
|
@lifecycle.finish_closing!
|
|
253
293
|
@new_pipe.signal
|
|
294
|
+
|
|
254
295
|
# Unblock anyone waiting on peer_connected when the socket is
|
|
255
296
|
# closed before a peer ever arrived.
|
|
256
297
|
@lifecycle.peer_connected.resolve(nil) unless @lifecycle.peer_connected.resolved?
|
|
@@ -268,6 +309,7 @@ module NNQ
|
|
|
268
309
|
|
|
269
310
|
private
|
|
270
311
|
|
|
312
|
+
|
|
271
313
|
def close_monitor_queue
|
|
272
314
|
return unless @monitor_queue
|
|
273
315
|
@monitor_queue.enqueue(nil)
|
|
@@ -277,26 +319,31 @@ module NNQ
|
|
|
277
319
|
def drain_send_queue(timeout)
|
|
278
320
|
return unless @routing.respond_to?(:send_queue_drained?)
|
|
279
321
|
return if @connections.empty?
|
|
322
|
+
|
|
280
323
|
deadline = timeout ? Async::Clock.now + timeout : nil
|
|
324
|
+
|
|
281
325
|
until @routing.send_queue_drained?
|
|
282
326
|
break if deadline && (deadline - Async::Clock.now) <= 0
|
|
283
327
|
sleep 0.001
|
|
284
328
|
end
|
|
329
|
+
rescue Async::Stop
|
|
330
|
+
# Parent task is being cancelled — stop draining and let close
|
|
331
|
+
# proceed with the rest of teardown instead of propagating the
|
|
332
|
+
# cancellation out of the ensure path.
|
|
285
333
|
end
|
|
286
334
|
|
|
287
335
|
|
|
288
336
|
def spawn_recv_loop(conn)
|
|
289
|
-
@
|
|
337
|
+
@connections[conn].barrier.async(annotation: "nnq recv #{conn.endpoint}") do
|
|
290
338
|
loop do
|
|
291
339
|
body = conn.receive_message
|
|
292
|
-
|
|
340
|
+
emit_verbose_msg_received(body)
|
|
293
341
|
@routing.enqueue(body, conn)
|
|
294
342
|
rescue *CONNECTION_LOST, Async::Stop
|
|
295
343
|
break
|
|
296
344
|
end
|
|
297
|
-
ensure
|
|
298
|
-
handle_connection_lost(conn)
|
|
299
345
|
end
|
|
300
346
|
end
|
|
347
|
+
|
|
301
348
|
end
|
|
302
349
|
end
|
data/lib/nnq/error.rb
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module NNQ
|
|
4
|
-
class Error
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class
|
|
9
|
-
|
|
4
|
+
class Error < RuntimeError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClosedError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProtocolError < Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TimeoutError < Error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RequestCancelled < Error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConnectionRejected < Error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TimedOut < Error
|
|
29
|
+
end
|
|
10
30
|
end
|
data/lib/nnq/monitor_event.rb
CHANGED
data/lib/nnq/options.rb
CHANGED
|
@@ -18,14 +18,23 @@ module NNQ
|
|
|
18
18
|
attr_accessor :reconnect_interval
|
|
19
19
|
attr_accessor :max_message_size
|
|
20
20
|
attr_accessor :send_hwm
|
|
21
|
+
attr_accessor :recv_hwm
|
|
22
|
+
attr_accessor :survey_time
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
# @param linger [Numeric] linger period in seconds on close
|
|
26
|
+
# (default Float::INFINITY = wait forever, matching libzmq).
|
|
27
|
+
# Pass 0 for immediate drop-on-close.
|
|
28
|
+
def initialize(linger: Float::INFINITY, send_hwm: DEFAULT_HWM, recv_hwm: DEFAULT_HWM)
|
|
23
29
|
@linger = linger
|
|
24
30
|
@read_timeout = nil
|
|
25
31
|
@write_timeout = nil
|
|
26
32
|
@reconnect_interval = 0.1
|
|
27
33
|
@max_message_size = nil
|
|
28
34
|
@send_hwm = send_hwm
|
|
35
|
+
@recv_hwm = recv_hwm
|
|
36
|
+
@survey_time = 1.0
|
|
29
37
|
end
|
|
38
|
+
|
|
30
39
|
end
|
|
31
40
|
end
|
data/lib/nnq/pair.rb
CHANGED
|
@@ -8,8 +8,9 @@ module NNQ
|
|
|
8
8
|
# peer. First peer to connect wins; subsequent peers are dropped
|
|
9
9
|
# until the current one disconnects. No SP header on the wire.
|
|
10
10
|
#
|
|
11
|
-
class
|
|
11
|
+
class PAIR0 < Socket
|
|
12
12
|
def send(body)
|
|
13
|
+
body = frozen_binary(body)
|
|
13
14
|
Reactor.run { @engine.routing.send(body) }
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -21,6 +22,7 @@ module NNQ
|
|
|
21
22
|
|
|
22
23
|
private
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
def protocol
|
|
25
27
|
Protocol::SP::Protocols::PAIR_V0
|
|
26
28
|
end
|
|
@@ -30,4 +32,7 @@ module NNQ
|
|
|
30
32
|
Routing::Pair.new(engine)
|
|
31
33
|
end
|
|
32
34
|
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PAIR = PAIR0
|
|
33
38
|
end
|
data/lib/nnq/pub_sub.rb
CHANGED
|
@@ -10,14 +10,16 @@ module NNQ
|
|
|
10
10
|
# a slow peer drops messages instead of blocking fast peers.
|
|
11
11
|
# Defaults to listening.
|
|
12
12
|
#
|
|
13
|
-
class
|
|
13
|
+
class PUB0 < Socket
|
|
14
14
|
def send(body)
|
|
15
|
+
body = frozen_binary(body)
|
|
15
16
|
Reactor.run { @engine.routing.send(body) }
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
private
|
|
20
21
|
|
|
22
|
+
|
|
21
23
|
def protocol
|
|
22
24
|
Protocol::SP::Protocols::PUB_V0
|
|
23
25
|
end
|
|
@@ -34,7 +36,7 @@ module NNQ
|
|
|
34
36
|
# are delivered — matching nng (unlike pre-4.x ZeroMQ). Defaults
|
|
35
37
|
# to dialing.
|
|
36
38
|
#
|
|
37
|
-
class
|
|
39
|
+
class SUB0 < Socket
|
|
38
40
|
# Subscribes to +prefix+. Bytes-level match. The empty string
|
|
39
41
|
# matches everything.
|
|
40
42
|
#
|
|
@@ -60,6 +62,7 @@ module NNQ
|
|
|
60
62
|
|
|
61
63
|
private
|
|
62
64
|
|
|
65
|
+
|
|
63
66
|
def protocol
|
|
64
67
|
Protocol::SP::Protocols::SUB_V0
|
|
65
68
|
end
|
|
@@ -69,4 +72,8 @@ module NNQ
|
|
|
69
72
|
Routing::Sub.new
|
|
70
73
|
end
|
|
71
74
|
end
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
PUB = PUB0
|
|
78
|
+
SUB = SUB0
|
|
72
79
|
end
|
data/lib/nnq/push_pull.rb
CHANGED
|
@@ -9,14 +9,16 @@ module NNQ
|
|
|
9
9
|
# bounded send queue (`send_hwm`); per-peer send pumps work-steal from
|
|
10
10
|
# it. Defaults to dialing.
|
|
11
11
|
#
|
|
12
|
-
class
|
|
12
|
+
class PUSH0 < Socket
|
|
13
13
|
def send(body)
|
|
14
|
+
body = frozen_binary(body)
|
|
14
15
|
Reactor.run { @engine.routing.send(body) }
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
private
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
def protocol
|
|
21
23
|
Protocol::SP::Protocols::PUSH_V0
|
|
22
24
|
end
|
|
@@ -32,7 +34,7 @@ module NNQ
|
|
|
32
34
|
# from all live PUSH peers into one unbounded receive queue. Defaults
|
|
33
35
|
# to listening.
|
|
34
36
|
#
|
|
35
|
-
class
|
|
37
|
+
class PULL0 < Socket
|
|
36
38
|
def receive
|
|
37
39
|
Reactor.run do
|
|
38
40
|
if (timeout = @engine.options.read_timeout)
|
|
@@ -46,6 +48,7 @@ module NNQ
|
|
|
46
48
|
|
|
47
49
|
private
|
|
48
50
|
|
|
51
|
+
|
|
49
52
|
def protocol
|
|
50
53
|
Protocol::SP::Protocols::PULL_V0
|
|
51
54
|
end
|
|
@@ -55,4 +58,8 @@ module NNQ
|
|
|
55
58
|
Routing::Pull.new
|
|
56
59
|
end
|
|
57
60
|
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
PUSH = PUSH0
|
|
64
|
+
PULL = PULL0
|
|
58
65
|
end
|
data/lib/nnq/reactor.rb
CHANGED
|
@@ -22,11 +22,14 @@ module NNQ
|
|
|
22
22
|
@root_task = nil
|
|
23
23
|
@work_queue = nil
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
class << self
|
|
26
27
|
def root_task
|
|
27
28
|
return @root_task if @root_task
|
|
29
|
+
|
|
28
30
|
@mutex.synchronize do
|
|
29
31
|
return @root_task if @root_task
|
|
32
|
+
|
|
30
33
|
ready = Thread::Queue.new
|
|
31
34
|
@work_queue = Async::Queue.new
|
|
32
35
|
@thread = Thread.new { run_reactor(ready) }
|
|
@@ -34,6 +37,7 @@ module NNQ
|
|
|
34
37
|
@root_task = ready.pop
|
|
35
38
|
at_exit { stop! }
|
|
36
39
|
end
|
|
40
|
+
|
|
37
41
|
@root_task
|
|
38
42
|
end
|
|
39
43
|
|
|
@@ -42,12 +46,10 @@ module NNQ
|
|
|
42
46
|
if Async::Task.current?
|
|
43
47
|
yield
|
|
44
48
|
else
|
|
45
|
-
result =
|
|
49
|
+
result = Async::Promise.new
|
|
46
50
|
root_task # ensure started
|
|
47
51
|
@work_queue.push([block, result])
|
|
48
|
-
|
|
49
|
-
raise value if status == :error
|
|
50
|
-
value
|
|
52
|
+
result.wait
|
|
51
53
|
end
|
|
52
54
|
end
|
|
53
55
|
|
|
@@ -61,23 +63,22 @@ module NNQ
|
|
|
61
63
|
@work_queue = nil
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
|
|
64
67
|
private
|
|
65
68
|
|
|
69
|
+
|
|
66
70
|
def run_reactor(ready)
|
|
67
71
|
Async do |task|
|
|
68
72
|
ready.push(task)
|
|
73
|
+
|
|
69
74
|
loop do
|
|
70
|
-
item = @work_queue.dequeue
|
|
71
|
-
break if item.nil?
|
|
75
|
+
item = @work_queue.dequeue or break
|
|
72
76
|
block, result = item
|
|
73
|
-
task.async
|
|
74
|
-
result.push([:ok, block.call])
|
|
75
|
-
rescue => e
|
|
76
|
-
result.push([:error, e])
|
|
77
|
-
end
|
|
77
|
+
task.async { result.fulfill { block.call } }
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
|
+
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
end
|
data/lib/nnq/req_rep.rb
CHANGED
|
@@ -3,58 +3,106 @@
|
|
|
3
3
|
require_relative "socket"
|
|
4
4
|
require_relative "routing/req"
|
|
5
5
|
require_relative "routing/rep"
|
|
6
|
+
require_relative "routing/req_raw"
|
|
7
|
+
require_relative "routing/rep_raw"
|
|
6
8
|
|
|
7
9
|
module NNQ
|
|
8
|
-
# REQ (nng req0): client side of request/reply.
|
|
9
|
-
# request
|
|
10
|
-
#
|
|
10
|
+
# REQ (nng req0): client side of request/reply. Cooked mode keeps a
|
|
11
|
+
# single in-flight request and matches replies by id; raw mode bypasses
|
|
12
|
+
# the state machine entirely and delivers replies as
|
|
13
|
+
# `[pipe, header, body]` tuples so the app can correlate verbatim.
|
|
11
14
|
#
|
|
12
|
-
class
|
|
13
|
-
#
|
|
14
|
-
# arrives. Returns the reply body (without the id header).
|
|
15
|
+
class REQ0 < Socket
|
|
16
|
+
# Cooked: sends +body+ as a request, blocks until the matching reply
|
|
17
|
+
# arrives. Returns the reply body (without the id header). Raises in
|
|
18
|
+
# raw mode — use {#send} / {#receive} there.
|
|
15
19
|
def send_request(body)
|
|
20
|
+
raise Error, "REQ#send_request not available in raw mode" if raw?
|
|
21
|
+
body = frozen_binary(body)
|
|
16
22
|
Reactor.run { @engine.routing.send_request(body) }
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
|
|
26
|
+
# Raw: round-robins +body+ to the next connected peer with
|
|
27
|
+
# +header+ (typically `[id | 0x80000000].pack("N")`) written
|
|
28
|
+
# verbatim between the SP length prefix and the body. Raises in
|
|
29
|
+
# cooked mode.
|
|
30
|
+
def send(body, header:)
|
|
31
|
+
raise Error, "REQ#send not available in cooked mode" unless raw?
|
|
32
|
+
body = frozen_binary(body)
|
|
33
|
+
Reactor.run { @engine.routing.send(body, header: header) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Raw: blocks until the next reply arrives, returns
|
|
38
|
+
# `[pipe, header, body]`. Raises in cooked mode.
|
|
39
|
+
def receive
|
|
40
|
+
raise Error, "REQ#receive not available in cooked mode" unless raw?
|
|
41
|
+
Reactor.run { @engine.routing.receive }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
20
45
|
private
|
|
21
46
|
|
|
47
|
+
|
|
22
48
|
def protocol
|
|
23
49
|
Protocol::SP::Protocols::REQ_V0
|
|
24
50
|
end
|
|
25
51
|
|
|
26
52
|
|
|
27
53
|
def build_routing(engine)
|
|
28
|
-
Routing::Req.new(engine)
|
|
54
|
+
raw? ? Routing::ReqRaw.new(engine) : Routing::Req.new(engine)
|
|
29
55
|
end
|
|
30
56
|
end
|
|
31
57
|
|
|
32
58
|
|
|
33
|
-
# REP (nng rep0): server side of request/reply.
|
|
34
|
-
#
|
|
59
|
+
# REP (nng rep0): server side of request/reply. Cooked mode strictly
|
|
60
|
+
# alternates #receive / #send_reply and stashes the backtrace
|
|
61
|
+
# internally; raw mode exposes the backtrace as an opaque +header+ and
|
|
62
|
+
# the originating pipe as a live Connection, so the app can drive the
|
|
63
|
+
# reply protocol itself (e.g. proxy/device use cases).
|
|
35
64
|
#
|
|
36
|
-
class
|
|
37
|
-
#
|
|
65
|
+
class REP0 < Socket
|
|
66
|
+
# Cooked: returns the next request body. Raw: returns
|
|
67
|
+
# `[pipe, header, body]`.
|
|
38
68
|
def receive
|
|
39
69
|
Reactor.run { @engine.routing.receive }
|
|
40
70
|
end
|
|
41
71
|
|
|
42
72
|
|
|
43
|
-
#
|
|
73
|
+
# Cooked: routes +body+ back to the pipe the most recent #receive
|
|
74
|
+
# came from. Raises in raw mode.
|
|
44
75
|
def send_reply(body)
|
|
76
|
+
raise Error, "REP#send_reply not available in raw mode" if raw?
|
|
77
|
+
body = frozen_binary(body)
|
|
45
78
|
Reactor.run { @engine.routing.send_reply(body) }
|
|
46
79
|
end
|
|
47
80
|
|
|
48
81
|
|
|
82
|
+
# Raw: writes +body+ with +header+ (the opaque backtrace handed out
|
|
83
|
+
# by a prior #receive) back to +to+ (the Connection from the same
|
|
84
|
+
# tuple). Silent drop if +to+ is closed. Raises in cooked mode.
|
|
85
|
+
def send(body, to:, header:)
|
|
86
|
+
raise Error, "REP#send not available in cooked mode" unless raw?
|
|
87
|
+
body = frozen_binary(body)
|
|
88
|
+
Reactor.run { @engine.routing.send(body, to: to, header: header) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
|
|
49
92
|
private
|
|
50
93
|
|
|
94
|
+
|
|
51
95
|
def protocol
|
|
52
96
|
Protocol::SP::Protocols::REP_V0
|
|
53
97
|
end
|
|
54
98
|
|
|
55
99
|
|
|
56
100
|
def build_routing(engine)
|
|
57
|
-
Routing::Rep.new(engine)
|
|
101
|
+
raw? ? Routing::RepRaw.new(engine) : Routing::Rep.new(engine)
|
|
58
102
|
end
|
|
59
103
|
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
REQ = REQ0
|
|
107
|
+
REP = REP0
|
|
60
108
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Shared backtrace parsing for SP protocols that use the
|
|
6
|
+
# request-id / hop-stack wire format (REQ/REP, SURVEYOR/RESPONDENT).
|
|
7
|
+
#
|
|
8
|
+
# Wire format: one or more 4-byte big-endian words. The terminal
|
|
9
|
+
# word (request or survey id) has its high bit set (0x80). Preceding
|
|
10
|
+
# words (hop ids added by devices) have the high bit clear.
|
|
11
|
+
#
|
|
12
|
+
module Backtrace
|
|
13
|
+
MAX_HOPS = 8 # nng's default ttl
|
|
14
|
+
|
|
15
|
+
# Reads 4-byte BE words off the front of +body+, stopping at the
|
|
16
|
+
# first one whose top byte has its high bit set. Returns
|
|
17
|
+
# [backtrace_bytes, remaining_payload] or nil on malformed input.
|
|
18
|
+
def parse_backtrace(body)
|
|
19
|
+
offset = 0
|
|
20
|
+
hops = 0
|
|
21
|
+
|
|
22
|
+
while hops < MAX_HOPS
|
|
23
|
+
return nil if body.bytesize - offset < 4
|
|
24
|
+
|
|
25
|
+
word = body.byteslice(offset, 4)
|
|
26
|
+
offset += 4
|
|
27
|
+
hops += 1
|
|
28
|
+
|
|
29
|
+
if word.getbyte(0) & 0x80 != 0
|
|
30
|
+
return [body.byteslice(0, offset), body.byteslice(offset..)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
nil # exceeded TTL without finding terminator
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Raw-mode TTL check: returns true if +header+ contains at least
|
|
39
|
+
# MAX_HOPS 4-byte words (i.e. forwarding it would push total hops
|
|
40
|
+
# over the cap). Cheap: just bytesize arithmetic.
|
|
41
|
+
def self.too_many_hops?(header)
|
|
42
|
+
header.bytesize >= MAX_HOPS * 4
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|