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
|
@@ -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
|
|
@@ -65,11 +69,11 @@ module NNQ
|
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
return if conn.closed?
|
|
68
|
-
conn.send_message(
|
|
72
|
+
conn.send_message(body, header: btrace)
|
|
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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/limited_queue"
|
|
4
|
+
require_relative "backtrace"
|
|
5
|
+
|
|
6
|
+
module NNQ
|
|
7
|
+
module Routing
|
|
8
|
+
# Raw REP: bypasses the cooked state machine. The incoming
|
|
9
|
+
# backtrace header is split off once at parse time and handed to
|
|
10
|
+
# the caller alongside the live Connection as `[pipe, header, body]`.
|
|
11
|
+
# Replies go back via `send(body, to:, header:)` which writes the
|
|
12
|
+
# caller-supplied header verbatim — no cooked pending/echo logic,
|
|
13
|
+
# no single-in-flight constraint.
|
|
14
|
+
#
|
|
15
|
+
class RepRaw
|
|
16
|
+
include Backtrace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def initialize(engine)
|
|
20
|
+
@engine = engine
|
|
21
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# @return [Array, nil] [pipe, header, body] or nil on close
|
|
26
|
+
def receive
|
|
27
|
+
@recv_queue.dequeue
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Sends +body+ with the caller-supplied +header+ back to +to+
|
|
32
|
+
# (a Connection handed out by a prior #receive). Silent drop
|
|
33
|
+
# if the target is closed or the header would push total hops
|
|
34
|
+
# over MAX_HOPS.
|
|
35
|
+
def send(body, to:, header:)
|
|
36
|
+
return if to.closed?
|
|
37
|
+
return if Backtrace.too_many_hops?(header)
|
|
38
|
+
to.send_message(body, header: header)
|
|
39
|
+
rescue ClosedError
|
|
40
|
+
# peer went away between receive and send — drop
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Called by the engine recv loop.
|
|
45
|
+
def enqueue(wire_bytes, conn)
|
|
46
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
47
|
+
return unless header # malformed / over-TTL — drop
|
|
48
|
+
@recv_queue.enqueue([conn, header, payload])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def close
|
|
53
|
+
@recv_queue.enqueue(nil)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def close_read
|
|
58
|
+
@recv_queue.enqueue(nil)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
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):
|
|
@@ -55,7 +55,7 @@ module NNQ
|
|
|
55
55
|
|
|
56
56
|
conn = pick_peer
|
|
57
57
|
header = [id].pack("N")
|
|
58
|
-
conn.send_message(header
|
|
58
|
+
conn.send_message(body, header: header)
|
|
59
59
|
promise.wait
|
|
60
60
|
ensure
|
|
61
61
|
@mutex.synchronize do
|
|
@@ -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,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/limited_queue"
|
|
4
|
+
require_relative "backtrace"
|
|
5
|
+
|
|
6
|
+
module NNQ
|
|
7
|
+
module Routing
|
|
8
|
+
# Raw REQ: bypasses the cooked single-in-flight request-id
|
|
9
|
+
# state machine. Sends are fire-and-forget round-robin with a
|
|
10
|
+
# caller-supplied header (typically `[id | 0x80000000].pack("N")`);
|
|
11
|
+
# replies land in a bounded queue and are delivered as
|
|
12
|
+
# `[pipe, header, body]` tuples so the app can correlate by
|
|
13
|
+
# header verbatim without ever parsing or slicing bytes.
|
|
14
|
+
#
|
|
15
|
+
class ReqRaw
|
|
16
|
+
include Backtrace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def initialize(engine)
|
|
20
|
+
@engine = engine
|
|
21
|
+
@next_idx = 0
|
|
22
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def send(body, header:)
|
|
27
|
+
conn = pick_peer
|
|
28
|
+
conn.send_message(body, header: header)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def receive
|
|
33
|
+
@recv_queue.dequeue
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def enqueue(wire_bytes, conn)
|
|
38
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
39
|
+
return unless header
|
|
40
|
+
@recv_queue.enqueue([conn, header, payload])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def close
|
|
45
|
+
@recv_queue.enqueue(nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def close_read
|
|
50
|
+
@recv_queue.enqueue(nil)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pick_peer
|
|
58
|
+
loop do
|
|
59
|
+
conns = @engine.connections.keys
|
|
60
|
+
|
|
61
|
+
if conns.empty?
|
|
62
|
+
@engine.new_pipe.wait
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@next_idx = (@next_idx + 1) % conns.size
|
|
67
|
+
return conns[@next_idx]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
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(body, header: btrace)
|
|
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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/limited_queue"
|
|
4
|
+
require_relative "backtrace"
|
|
5
|
+
|
|
6
|
+
module NNQ
|
|
7
|
+
module Routing
|
|
8
|
+
# Raw RESPONDENT: mirror of {RepRaw} for the survey pattern.
|
|
9
|
+
# No survey-window state, no pending slot — the app receives
|
|
10
|
+
# `[pipe, header, body]` tuples and chooses whether (and when)
|
|
11
|
+
# to reply via `send(body, to:, header:)`.
|
|
12
|
+
#
|
|
13
|
+
class RespondentRaw
|
|
14
|
+
include Backtrace
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def initialize(engine)
|
|
18
|
+
@engine = engine
|
|
19
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def receive
|
|
24
|
+
@recv_queue.dequeue
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def send(body, to:, header:)
|
|
29
|
+
return if to.closed?
|
|
30
|
+
return if Backtrace.too_many_hops?(header)
|
|
31
|
+
to.send_message(body, header: header)
|
|
32
|
+
rescue ClosedError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def enqueue(wire_bytes, conn)
|
|
37
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
38
|
+
return unless header
|
|
39
|
+
@recv_queue.enqueue([conn, header, payload])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def close
|
|
44
|
+
@recv_queue.enqueue(nil)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def close_read
|
|
49
|
+
@recv_queue.enqueue(nil)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -21,7 +21,13 @@ module NNQ
|
|
|
21
21
|
# {#spawn_send_pump_for} from their #connection_added hook.
|
|
22
22
|
#
|
|
23
23
|
module SendPump
|
|
24
|
+
# Max messages a single pump will drain from the shared queue in
|
|
25
|
+
# one batch before yielding. Bounds the worst-case latency other
|
|
26
|
+
# pumps wait when the queue is under sustained pressure.
|
|
24
27
|
BATCH_MSG_CAP = 256
|
|
28
|
+
|
|
29
|
+
# Max cumulative body bytes in one batch. Keeps a single pump
|
|
30
|
+
# from monopolising the writer mutex on huge payloads.
|
|
25
31
|
BATCH_BYTE_CAP = 256 * 1024
|
|
26
32
|
|
|
27
33
|
|
|
@@ -56,6 +62,7 @@ module NNQ
|
|
|
56
62
|
|
|
57
63
|
private
|
|
58
64
|
|
|
65
|
+
|
|
59
66
|
# @param engine [Engine]
|
|
60
67
|
def init_send_pump(engine)
|
|
61
68
|
@engine = engine
|
|
@@ -79,11 +86,16 @@ module NNQ
|
|
|
79
86
|
#
|
|
80
87
|
# @param conn [Connection]
|
|
81
88
|
def spawn_send_pump_for(conn)
|
|
82
|
-
|
|
89
|
+
annotation = "nnq send pump #{conn.endpoint}"
|
|
90
|
+
parent = @engine.connections[conn]&.barrier || @engine.barrier
|
|
91
|
+
|
|
92
|
+
task = @engine.spawn_task(annotation:, parent:) do
|
|
83
93
|
loop do
|
|
84
94
|
first = @send_queue.dequeue
|
|
85
95
|
break if first.nil? # queue closed
|
|
96
|
+
|
|
86
97
|
@in_flight += 1
|
|
98
|
+
|
|
87
99
|
begin
|
|
88
100
|
batch = [first]
|
|
89
101
|
drain_capped(batch)
|
|
@@ -91,22 +103,26 @@ module NNQ
|
|
|
91
103
|
ensure
|
|
92
104
|
@in_flight -= 1
|
|
93
105
|
end
|
|
106
|
+
|
|
107
|
+
Async::Task.current.yield
|
|
94
108
|
rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
95
109
|
# Peer died mid-flush. In-flight batch dropped.
|
|
96
110
|
break
|
|
97
111
|
end
|
|
98
|
-
ensure
|
|
99
|
-
@engine.handle_connection_lost(conn)
|
|
100
112
|
end
|
|
113
|
+
|
|
101
114
|
@pumps[conn] = task
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
|
|
105
118
|
def drain_capped(batch)
|
|
106
|
-
bytes = batch
|
|
119
|
+
bytes = batch.first.bytesize
|
|
120
|
+
|
|
107
121
|
while batch.size < BATCH_MSG_CAP && bytes < BATCH_BYTE_CAP
|
|
108
122
|
msg = @send_queue.dequeue(timeout: 0)
|
|
123
|
+
|
|
109
124
|
break unless msg
|
|
125
|
+
|
|
110
126
|
batch << msg
|
|
111
127
|
bytes += msg.bytesize
|
|
112
128
|
end
|
|
@@ -115,16 +131,21 @@ module NNQ
|
|
|
115
131
|
|
|
116
132
|
def write_batch(conn, batch)
|
|
117
133
|
if batch.size == 1
|
|
118
|
-
conn.write_message(batch
|
|
134
|
+
conn.write_message(batch.first)
|
|
119
135
|
else
|
|
120
136
|
# Single mutex acquisition for the whole batch (batches run
|
|
121
137
|
# up to BATCH_MSG_CAP messages). The per-message pump loop
|
|
122
138
|
# would otherwise lock/unlock the SP mutex N times.
|
|
123
139
|
conn.write_messages(batch)
|
|
124
140
|
end
|
|
141
|
+
|
|
125
142
|
conn.flush
|
|
126
|
-
|
|
143
|
+
|
|
144
|
+
if @engine.verbose_monitor
|
|
145
|
+
batch.each { |body| @engine.emit_verbose_msg_sent(body) }
|
|
146
|
+
end
|
|
127
147
|
end
|
|
148
|
+
|
|
128
149
|
end
|
|
129
150
|
end
|
|
130
151
|
end
|