nnq 0.4.0 → 0.5.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 +77 -0
- data/lib/nnq/bus.rb +37 -0
- data/lib/nnq/connection.rb +9 -2
- 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 +8 -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 +10 -2
- data/lib/nnq/routing/backtrace.rb +39 -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 +6 -21
- data/lib/nnq/routing/req.rb +6 -2
- data/lib/nnq/routing/respondent.rb +84 -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/socket.rb +50 -7
- data/lib/nnq/surveyor_respondent.rb +78 -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 +7 -1
data/lib/nnq/options.rb
CHANGED
|
@@ -18,14 +18,21 @@ module NNQ
|
|
|
18
18
|
attr_accessor :reconnect_interval
|
|
19
19
|
attr_accessor :max_message_size
|
|
20
20
|
attr_accessor :send_hwm
|
|
21
|
+
attr_accessor :survey_time
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
|
|
24
|
+
# @param linger [Numeric] linger period in seconds on close
|
|
25
|
+
# (default Float::INFINITY = wait forever, matching libzmq).
|
|
26
|
+
# Pass 0 for immediate drop-on-close.
|
|
27
|
+
def initialize(linger: Float::INFINITY, send_hwm: DEFAULT_HWM)
|
|
23
28
|
@linger = linger
|
|
24
29
|
@read_timeout = nil
|
|
25
30
|
@write_timeout = nil
|
|
26
31
|
@reconnect_interval = 0.1
|
|
27
32
|
@max_message_size = nil
|
|
28
33
|
@send_hwm = send_hwm
|
|
34
|
+
@survey_time = 1.0
|
|
29
35
|
end
|
|
36
|
+
|
|
30
37
|
end
|
|
31
38
|
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
|
@@ -9,16 +9,18 @@ module NNQ
|
|
|
9
9
|
# request per socket. #send_request blocks until the matching reply
|
|
10
10
|
# comes back.
|
|
11
11
|
#
|
|
12
|
-
class
|
|
12
|
+
class REQ0 < Socket
|
|
13
13
|
# Sends +body+ as a request, blocks until the matching reply
|
|
14
14
|
# arrives. Returns the reply body (without the id header).
|
|
15
15
|
def send_request(body)
|
|
16
|
+
body = frozen_binary(body)
|
|
16
17
|
Reactor.run { @engine.routing.send_request(body) }
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
private
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
def protocol
|
|
23
25
|
Protocol::SP::Protocols::REQ_V0
|
|
24
26
|
end
|
|
@@ -33,7 +35,7 @@ module NNQ
|
|
|
33
35
|
# REP (nng rep0): server side of request/reply. Strict alternation
|
|
34
36
|
# of #receive then #send_reply, per request.
|
|
35
37
|
#
|
|
36
|
-
class
|
|
38
|
+
class REP0 < Socket
|
|
37
39
|
# Blocks until the next request arrives. Returns the request body.
|
|
38
40
|
def receive
|
|
39
41
|
Reactor.run { @engine.routing.receive }
|
|
@@ -42,12 +44,14 @@ module NNQ
|
|
|
42
44
|
|
|
43
45
|
# Routes +body+ back to the pipe the most recent #receive came from.
|
|
44
46
|
def send_reply(body)
|
|
47
|
+
body = frozen_binary(body)
|
|
45
48
|
Reactor.run { @engine.routing.send_reply(body) }
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
private
|
|
50
53
|
|
|
54
|
+
|
|
51
55
|
def protocol
|
|
52
56
|
Protocol::SP::Protocols::REP_V0
|
|
53
57
|
end
|
|
@@ -57,4 +61,8 @@ module NNQ
|
|
|
57
61
|
Routing::Rep.new(engine)
|
|
58
62
|
end
|
|
59
63
|
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
REQ = REQ0
|
|
67
|
+
REP = REP0
|
|
60
68
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/queue"
|
|
5
|
+
require "async/limited_queue"
|
|
6
|
+
|
|
7
|
+
module NNQ
|
|
8
|
+
module Routing
|
|
9
|
+
# BUS0: best-effort bidirectional mesh.
|
|
10
|
+
#
|
|
11
|
+
# Send side: fan-out to all connected peers. Each peer gets its own
|
|
12
|
+
# bounded send queue and pump fiber — a slow peer drops messages
|
|
13
|
+
# instead of blocking fast ones (same as PUB). Send never blocks.
|
|
14
|
+
#
|
|
15
|
+
# Recv side: all incoming messages are pushed into a shared
|
|
16
|
+
# unbounded queue (same as PULL).
|
|
17
|
+
#
|
|
18
|
+
# No SP headers in cooked mode — body on the wire is the user
|
|
19
|
+
# payload.
|
|
20
|
+
#
|
|
21
|
+
class Bus
|
|
22
|
+
def initialize(engine)
|
|
23
|
+
@engine = engine
|
|
24
|
+
@queues = {} # conn => Async::LimitedQueue
|
|
25
|
+
@pump_tasks = {} # conn => Async::Task
|
|
26
|
+
@recv_queue = Async::Queue.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Broadcasts +body+ to every connected peer. Non-blocking per
|
|
31
|
+
# peer: drops when a peer's queue is at HWM.
|
|
32
|
+
#
|
|
33
|
+
# @param body [String]
|
|
34
|
+
def send(body)
|
|
35
|
+
@queues.each_value do |queue|
|
|
36
|
+
queue.enqueue(body) unless queue.limited?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Called by the engine recv loop with each received message.
|
|
42
|
+
def enqueue(body, _conn = nil)
|
|
43
|
+
@recv_queue.enqueue(body)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# @return [String, nil] message body, or nil once the socket is closed
|
|
48
|
+
def receive
|
|
49
|
+
@recv_queue.dequeue
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def connection_added(conn)
|
|
54
|
+
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
55
|
+
@queues[conn] = queue
|
|
56
|
+
@pump_tasks[conn] = spawn_pump(conn, queue)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def connection_removed(conn)
|
|
61
|
+
@queues.delete(conn)
|
|
62
|
+
task = @pump_tasks.delete(conn)
|
|
63
|
+
return unless task
|
|
64
|
+
return if task == Async::Task.current
|
|
65
|
+
task.stop
|
|
66
|
+
rescue IOError, Errno::EPIPE
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def send_queue_drained?
|
|
71
|
+
@queues.each_value.all? { |q| q.empty? }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def close
|
|
76
|
+
@pump_tasks.each_value(&:stop)
|
|
77
|
+
@pump_tasks.clear
|
|
78
|
+
@queues.clear
|
|
79
|
+
@recv_queue.enqueue(nil)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def close_read
|
|
84
|
+
@recv_queue.enqueue(nil)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def spawn_pump(conn, queue)
|
|
92
|
+
annotation = "nnq bus pump #{conn.endpoint}"
|
|
93
|
+
parent = @engine.connections[conn]&.barrier || @engine.barrier
|
|
94
|
+
|
|
95
|
+
@engine.spawn_task(annotation:, parent:) do
|
|
96
|
+
loop do
|
|
97
|
+
body = queue.dequeue
|
|
98
|
+
conn.send_message(body)
|
|
99
|
+
@engine.emit_verbose_msg_sent(body)
|
|
100
|
+
rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
101
|
+
break
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/nnq/routing/pair.rb
CHANGED
|
@@ -22,6 +22,7 @@ module NNQ
|
|
|
22
22
|
class Pair
|
|
23
23
|
include SendPump
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
def initialize(engine)
|
|
26
27
|
init_send_pump(engine)
|
|
27
28
|
@recv_queue = Async::Queue.new
|
|
@@ -41,7 +42,7 @@ module NNQ
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
# Called by the recv loop with each
|
|
45
|
+
# Called by the recv loop with each message off the wire.
|
|
45
46
|
def enqueue(body, _conn = nil)
|
|
46
47
|
@recv_queue.enqueue(body)
|
|
47
48
|
end
|
|
@@ -52,6 +53,7 @@ module NNQ
|
|
|
52
53
|
# without ever exposing it to pumps.
|
|
53
54
|
def connection_added(conn)
|
|
54
55
|
raise ConnectionRejected, "PAIR socket already has a peer" if @peer
|
|
56
|
+
|
|
55
57
|
@peer = conn
|
|
56
58
|
spawn_send_pump_for(conn)
|
|
57
59
|
end
|
|
@@ -73,6 +75,7 @@ module NNQ
|
|
|
73
75
|
def close_read
|
|
74
76
|
@recv_queue.enqueue(nil)
|
|
75
77
|
end
|
|
78
|
+
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
end
|
data/lib/nnq/routing/pub.rb
CHANGED
|
@@ -60,7 +60,7 @@ module NNQ
|
|
|
60
60
|
|
|
61
61
|
# True once every peer's queue is empty. Engine linger polls this.
|
|
62
62
|
def send_queue_drained?
|
|
63
|
-
@queues.each_value.all?
|
|
63
|
+
@queues.each_value.all? { |q| q.empty? }
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
|
|
@@ -70,21 +70,25 @@ module NNQ
|
|
|
70
70
|
@queues.clear
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
|
|
73
74
|
private
|
|
74
75
|
|
|
76
|
+
|
|
75
77
|
def spawn_pump(conn, queue)
|
|
76
|
-
|
|
78
|
+
annotation = "nnq pub pump #{conn.endpoint}"
|
|
79
|
+
parent = @engine.connections[conn]&.barrier || @engine.barrier
|
|
80
|
+
|
|
81
|
+
@engine.spawn_task(annotation:, parent:) do
|
|
77
82
|
loop do
|
|
78
83
|
body = queue.dequeue
|
|
79
84
|
conn.send_message(body)
|
|
80
|
-
@engine.
|
|
85
|
+
@engine.emit_verbose_msg_sent(body)
|
|
81
86
|
rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
82
87
|
break
|
|
83
88
|
end
|
|
84
|
-
ensure
|
|
85
|
-
@engine.handle_connection_lost(conn)
|
|
86
89
|
end
|
|
87
90
|
end
|
|
91
|
+
|
|
88
92
|
end
|
|
89
93
|
end
|
|
90
94
|
end
|
data/lib/nnq/routing/pull.rb
CHANGED
|
@@ -6,7 +6,7 @@ module NNQ
|
|
|
6
6
|
module Routing
|
|
7
7
|
# PULL side: an unbounded queue of received messages. Per-connection
|
|
8
8
|
# recv fibers (spawned by the Engine when each pipe is established)
|
|
9
|
-
# call {#enqueue} on each
|
|
9
|
+
# call {#enqueue} on each message; user code calls {#receive}.
|
|
10
10
|
#
|
|
11
11
|
# No HWM, no prefetch buffer — TCP throttles the senders directly
|
|
12
12
|
# via the kernel buffer.
|
|
@@ -41,6 +41,7 @@ module NNQ
|
|
|
41
41
|
def close_read
|
|
42
42
|
@queue.enqueue(nil)
|
|
43
43
|
end
|
|
44
|
+
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
end
|
data/lib/nnq/routing/push.rb
CHANGED
data/lib/nnq/routing/rep.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "async/queue"
|
|
4
|
+
require_relative "backtrace"
|
|
4
5
|
|
|
5
6
|
module NNQ
|
|
6
7
|
module Routing
|
|
@@ -27,7 +28,8 @@ module NNQ
|
|
|
27
28
|
# - TTL cap on the backtrace stack: 8 hops, matching nng's default.
|
|
28
29
|
#
|
|
29
30
|
class Rep
|
|
30
|
-
|
|
31
|
+
include Backtrace
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
def initialize(engine)
|
|
33
35
|
@engine = engine
|
|
@@ -46,7 +48,9 @@ module NNQ
|
|
|
46
48
|
# again without replying is how users drop unwanted requests.
|
|
47
49
|
@mutex.synchronize { @pending = nil }
|
|
48
50
|
item = @recv_queue.dequeue
|
|
51
|
+
|
|
49
52
|
return nil if item.nil?
|
|
53
|
+
|
|
50
54
|
conn, btrace, body = item
|
|
51
55
|
@mutex.synchronize { @pending = [conn, btrace] }
|
|
52
56
|
body
|
|
@@ -69,7 +73,7 @@ module NNQ
|
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
|
|
72
|
-
# Called by the engine recv loop with each received
|
|
76
|
+
# Called by the engine recv loop with each received message.
|
|
73
77
|
def enqueue(body, conn)
|
|
74
78
|
btrace, payload = parse_backtrace(body)
|
|
75
79
|
return unless btrace # malformed/over-TTL — drop
|
|
@@ -94,25 +98,6 @@ module NNQ
|
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
# Reads 4-byte BE words off the front of +body+, stopping at the
|
|
100
|
-
# first one whose top byte has its high bit set. Returns
|
|
101
|
-
# [backtrace_bytes, remaining_payload] or nil on malformed input.
|
|
102
|
-
def parse_backtrace(body)
|
|
103
|
-
offset = 0
|
|
104
|
-
hops = 0
|
|
105
|
-
while hops < MAX_HOPS
|
|
106
|
-
return nil if body.bytesize - offset < 4
|
|
107
|
-
word = body.byteslice(offset, 4)
|
|
108
|
-
offset += 4
|
|
109
|
-
hops += 1
|
|
110
|
-
if word.getbyte(0) & 0x80 != 0
|
|
111
|
-
return [body.byteslice(0, offset), body.byteslice(offset..)]
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
nil # exceeded TTL without finding terminator
|
|
115
|
-
end
|
|
116
101
|
end
|
|
117
102
|
end
|
|
118
103
|
end
|
data/lib/nnq/routing/req.rb
CHANGED
|
@@ -10,7 +10,7 @@ module NNQ
|
|
|
10
10
|
# Wire format: each message body on the wire is `[4-byte BE
|
|
11
11
|
# request_id][user_payload]`. The request id has the high bit set
|
|
12
12
|
# (`0x80000000..0xFFFFFFFF`) — that's nng's marker for "this is the
|
|
13
|
-
# last (deepest)
|
|
13
|
+
# last (deepest) word on the backtrace stack". Direct REQ→REP has
|
|
14
14
|
# exactly one id.
|
|
15
15
|
#
|
|
16
16
|
# Semantics (cooked mode, what this implements):
|
|
@@ -66,7 +66,7 @@ module NNQ
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
# Called by the engine recv loop with each received
|
|
69
|
+
# Called by the engine recv loop with each received message.
|
|
70
70
|
def enqueue(body, _conn)
|
|
71
71
|
return if body.bytesize < 4
|
|
72
72
|
id = body.unpack1("N")
|
|
@@ -91,17 +91,21 @@ module NNQ
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
+
|
|
94
95
|
def pick_peer
|
|
95
96
|
loop do
|
|
96
97
|
conns = @engine.connections.keys
|
|
98
|
+
|
|
97
99
|
if conns.empty?
|
|
98
100
|
@engine.new_pipe.wait
|
|
99
101
|
next
|
|
100
102
|
end
|
|
103
|
+
|
|
101
104
|
@next_idx = (@next_idx + 1) % conns.size
|
|
102
105
|
return conns[@next_idx]
|
|
103
106
|
end
|
|
104
107
|
end
|
|
108
|
+
|
|
105
109
|
end
|
|
106
110
|
end
|
|
107
111
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/queue"
|
|
4
|
+
require_relative "backtrace"
|
|
5
|
+
|
|
6
|
+
module NNQ
|
|
7
|
+
module Routing
|
|
8
|
+
# RESPONDENT: reply side of the survey0 pattern.
|
|
9
|
+
#
|
|
10
|
+
# Semantics mirror REP: strict alternation of #receive then
|
|
11
|
+
# #send_reply. The backtrace (survey ID + any hop IDs) is stripped
|
|
12
|
+
# on receive and echoed verbatim on reply.
|
|
13
|
+
#
|
|
14
|
+
class Respondent
|
|
15
|
+
include Backtrace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def initialize(engine)
|
|
19
|
+
@engine = engine
|
|
20
|
+
@recv_queue = Async::Queue.new
|
|
21
|
+
@pending = nil
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Receives the next survey body. Stashes the backtrace +
|
|
27
|
+
# originating connection for the subsequent #send_reply.
|
|
28
|
+
#
|
|
29
|
+
# @return [String, nil] survey body, or nil if the socket was closed
|
|
30
|
+
def receive
|
|
31
|
+
@mutex.synchronize { @pending = nil }
|
|
32
|
+
item = @recv_queue.dequeue
|
|
33
|
+
|
|
34
|
+
return nil if item.nil?
|
|
35
|
+
|
|
36
|
+
conn, btrace, body = item
|
|
37
|
+
@mutex.synchronize { @pending = [conn, btrace] }
|
|
38
|
+
body
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Sends +body+ as the reply to the most recently received survey.
|
|
43
|
+
#
|
|
44
|
+
# @param body [String]
|
|
45
|
+
def send_reply(body)
|
|
46
|
+
conn, btrace = @mutex.synchronize do
|
|
47
|
+
raise Error, "RESPONDENT socket has no pending survey to reply to" unless @pending
|
|
48
|
+
taken = @pending
|
|
49
|
+
@pending = nil
|
|
50
|
+
taken
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return if conn.closed?
|
|
54
|
+
conn.send_message(btrace + body)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Called by the engine recv loop with each received message.
|
|
59
|
+
def enqueue(body, conn)
|
|
60
|
+
btrace, payload = parse_backtrace(body)
|
|
61
|
+
return unless btrace # malformed/over-TTL — drop
|
|
62
|
+
@recv_queue.enqueue([conn, btrace, payload])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def connection_removed(conn)
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@pending = nil if @pending && @pending[0] == conn
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def close
|
|
74
|
+
@recv_queue.enqueue(nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def close_read
|
|
79
|
+
@recv_queue.enqueue(nil)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|