nnq 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 +37 -0
- data/lib/nnq/connection.rb +7 -4
- data/lib/nnq/options.rb +3 -1
- data/lib/nnq/req_rep.rb +51 -11
- data/lib/nnq/routing/backtrace.rb +8 -0
- data/lib/nnq/routing/rep.rb +1 -1
- data/lib/nnq/routing/rep_raw.rb +63 -0
- data/lib/nnq/routing/req.rb +1 -1
- data/lib/nnq/routing/req_raw.rb +73 -0
- data/lib/nnq/routing/respondent.rb +1 -1
- data/lib/nnq/routing/respondent_raw.rb +54 -0
- data/lib/nnq/routing/surveyor_raw.rb +107 -0
- data/lib/nnq/socket.rb +2 -2
- data/lib/nnq/surveyor_respondent.rb +38 -18
- data/lib/nnq/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a336ac1e24bc6210ddeac6731163baab6f8980d9eebbe3235fac13157416fcf
|
|
4
|
+
data.tar.gz: f049bf9038235487966cae1c8920b452abf9984e3776b2b93cbbd95e067fb485
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dcef45943a41f1bc53bbcf8ecc0c34c2779e4a16dea04e8f78854cf6a9debe11168596b47b764728e49393f61c319d41e2d79e489b37dec809adc59cbc0741f0
|
|
7
|
+
data.tar.gz: fd7aaa30c57d5b8fbfd6279aba8b96423e201837ca4ef0144a315ec020da5e0183aaede0c7327fc49bf1bd318894099e4fe02657a12aebf98c1d895582583c62
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-04-15
|
|
4
|
+
|
|
5
|
+
- **NNG-style raw mode for REQ/REP and SURVEYOR/RESPONDENT.** Constructing
|
|
6
|
+
any of the four with `raw: true` bypasses the cooked state machine
|
|
7
|
+
(request-id tracking, pending-reply slot, survey window) and exposes
|
|
8
|
+
the full SP backtrace header as an opaque, caller-supplied handle.
|
|
9
|
+
- `#receive` returns `[pipe, header, body]` where `pipe` is the live
|
|
10
|
+
`NNQ::Connection` that delivered the message (idiomatic Ruby handle
|
|
11
|
+
— no opaque pipe_id token, no lookup registry), `header` is the
|
|
12
|
+
parsed backtrace bytes, and `body` is the payload.
|
|
13
|
+
- Raw REQ/SURVEYOR send: `send(body, header:)` — fans round-robin /
|
|
14
|
+
fans out.
|
|
15
|
+
- Raw REP/RESPONDENT send: `send(body, to:, header:)` — routes
|
|
16
|
+
directly to a prior `pipe` with the stored `header` written
|
|
17
|
+
verbatim, so the cooked peer matches the reply. Closed peer or
|
|
18
|
+
over-TTL header → silent drop (matches NNG behavior).
|
|
19
|
+
- Cooked-mode methods (`send_request`, `send_reply`, `send_survey`)
|
|
20
|
+
raise `NNQ::Error` in raw mode and vice versa.
|
|
21
|
+
- Unblocks proxy/device-style use cases (forwarders, request routers)
|
|
22
|
+
without touching the cooked code paths. `lib/nnq/routing/{req,rep,
|
|
23
|
+
surveyor,respondent}_raw.rb` live alongside their cooked siblings;
|
|
24
|
+
`build_routing` branches on `@raw` inside REQ0/REP0/SURVEYOR0/
|
|
25
|
+
RESPONDENT0. PUB/SUB and PUSH/PULL raw are still out of scope.
|
|
26
|
+
- **Zero-alloc cooked send paths via protocol-sp `header:` kwarg.**
|
|
27
|
+
`Connection#send_message` / `#write_message` grow an optional
|
|
28
|
+
`header:` kwarg that protocol-sp writes between the SP length prefix
|
|
29
|
+
and the body as a third buffered write (coalesced into a single
|
|
30
|
+
`writev`). Cooked `Req#send_request`, `Rep#send_reply`, and
|
|
31
|
+
`Respondent#send_reply` no longer allocate the `header + body`
|
|
32
|
+
intermediate String on every send — the savings apply to every
|
|
33
|
+
REQ/REP round trip regardless of whether raw mode is used.
|
|
34
|
+
Requires `protocol-sp >= 0.3`.
|
|
35
|
+
- **`Options#recv_hwm`** — new option, defaults to `Options::DEFAULT_HWM`
|
|
36
|
+
(same as `send_hwm`). Bounds the raw routing strategies' receive
|
|
37
|
+
queues; the cooked paths still use their existing (unbounded) state
|
|
38
|
+
and are unaffected.
|
|
39
|
+
|
|
3
40
|
## 0.5.0 — 2026-04-15
|
|
4
41
|
|
|
5
42
|
- **Send-path freezes the body** — every public send method (PUSH,
|
data/lib/nnq/connection.rb
CHANGED
|
@@ -35,10 +35,12 @@ module NNQ
|
|
|
35
35
|
# Writes one message into the SP connection's send buffer (no flush).
|
|
36
36
|
#
|
|
37
37
|
# @param body [String]
|
|
38
|
+
# @param header [String, nil] optional binary prefix written between
|
|
39
|
+
# the SP length prefix and body (see Protocol::SP::Connection)
|
|
38
40
|
# @return [void]
|
|
39
|
-
def write_message(body)
|
|
41
|
+
def write_message(body, header: nil)
|
|
40
42
|
raise ClosedError, "connection closed" if @closed
|
|
41
|
-
@sp.write_message(body)
|
|
43
|
+
@sp.write_message(body, header: header)
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
|
|
@@ -57,10 +59,11 @@ module NNQ
|
|
|
57
59
|
# each call is request-paced and there's nothing to batch.
|
|
58
60
|
#
|
|
59
61
|
# @param body [String]
|
|
62
|
+
# @param header [String, nil] optional binary prefix
|
|
60
63
|
# @return [void]
|
|
61
|
-
def send_message(body)
|
|
64
|
+
def send_message(body, header: nil)
|
|
62
65
|
raise ClosedError, "connection closed" if @closed
|
|
63
|
-
@sp.send_message(body)
|
|
66
|
+
@sp.send_message(body, header: header)
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
|
data/lib/nnq/options.rb
CHANGED
|
@@ -18,19 +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 :recv_hwm
|
|
21
22
|
attr_accessor :survey_time
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# @param linger [Numeric] linger period in seconds on close
|
|
25
26
|
# (default Float::INFINITY = wait forever, matching libzmq).
|
|
26
27
|
# Pass 0 for immediate drop-on-close.
|
|
27
|
-
def initialize(linger: Float::INFINITY, send_hwm: DEFAULT_HWM)
|
|
28
|
+
def initialize(linger: Float::INFINITY, send_hwm: DEFAULT_HWM, recv_hwm: DEFAULT_HWM)
|
|
28
29
|
@linger = linger
|
|
29
30
|
@read_timeout = nil
|
|
30
31
|
@write_timeout = nil
|
|
31
32
|
@reconnect_interval = 0.1
|
|
32
33
|
@max_message_size = nil
|
|
33
34
|
@send_hwm = send_hwm
|
|
35
|
+
@recv_hwm = recv_hwm
|
|
34
36
|
@survey_time = 1.0
|
|
35
37
|
end
|
|
36
38
|
|
data/lib/nnq/req_rep.rb
CHANGED
|
@@ -3,21 +3,45 @@
|
|
|
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
15
|
class REQ0 < Socket
|
|
13
|
-
#
|
|
14
|
-
# arrives. Returns the reply body (without the id header).
|
|
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?
|
|
16
21
|
body = frozen_binary(body)
|
|
17
22
|
Reactor.run { @engine.routing.send_request(body) }
|
|
18
23
|
end
|
|
19
24
|
|
|
20
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
|
+
|
|
21
45
|
private
|
|
22
46
|
|
|
23
47
|
|
|
@@ -27,28 +51,44 @@ module NNQ
|
|
|
27
51
|
|
|
28
52
|
|
|
29
53
|
def build_routing(engine)
|
|
30
|
-
Routing::Req.new(engine)
|
|
54
|
+
raw? ? Routing::ReqRaw.new(engine) : Routing::Req.new(engine)
|
|
31
55
|
end
|
|
32
56
|
end
|
|
33
57
|
|
|
34
58
|
|
|
35
|
-
# REP (nng rep0): server side of request/reply.
|
|
36
|
-
#
|
|
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).
|
|
37
64
|
#
|
|
38
65
|
class REP0 < Socket
|
|
39
|
-
#
|
|
66
|
+
# Cooked: returns the next request body. Raw: returns
|
|
67
|
+
# `[pipe, header, body]`.
|
|
40
68
|
def receive
|
|
41
69
|
Reactor.run { @engine.routing.receive }
|
|
42
70
|
end
|
|
43
71
|
|
|
44
72
|
|
|
45
|
-
#
|
|
73
|
+
# Cooked: routes +body+ back to the pipe the most recent #receive
|
|
74
|
+
# came from. Raises in raw mode.
|
|
46
75
|
def send_reply(body)
|
|
76
|
+
raise Error, "REP#send_reply not available in raw mode" if raw?
|
|
47
77
|
body = frozen_binary(body)
|
|
48
78
|
Reactor.run { @engine.routing.send_reply(body) }
|
|
49
79
|
end
|
|
50
80
|
|
|
51
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
|
+
|
|
52
92
|
private
|
|
53
93
|
|
|
54
94
|
|
|
@@ -58,7 +98,7 @@ module NNQ
|
|
|
58
98
|
|
|
59
99
|
|
|
60
100
|
def build_routing(engine)
|
|
61
|
-
Routing::Rep.new(engine)
|
|
101
|
+
raw? ? Routing::RepRaw.new(engine) : Routing::Rep.new(engine)
|
|
62
102
|
end
|
|
63
103
|
end
|
|
64
104
|
|
|
@@ -34,6 +34,14 @@ module NNQ
|
|
|
34
34
|
nil # exceeded TTL without finding terminator
|
|
35
35
|
end
|
|
36
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
|
+
|
|
37
45
|
end
|
|
38
46
|
end
|
|
39
47
|
end
|
data/lib/nnq/routing/rep.rb
CHANGED
|
@@ -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
|
@@ -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,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
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/limited_queue"
|
|
5
|
+
require_relative "backtrace"
|
|
6
|
+
|
|
7
|
+
module NNQ
|
|
8
|
+
module Routing
|
|
9
|
+
# Raw SURVEYOR: fans out surveys to all peers like cooked
|
|
10
|
+
# {Surveyor}, but without a survey window, survey-id matching,
|
|
11
|
+
# or timeout. Replies are delivered as `[pipe, header, body]`
|
|
12
|
+
# tuples so the app can correlate by header verbatim.
|
|
13
|
+
#
|
|
14
|
+
# Each per-conn send queue holds `[header, body]` pairs and the
|
|
15
|
+
# pump calls `conn.write_message(body, header: header)` so the
|
|
16
|
+
# protocol-sp header kwarg is threaded through the fan-out —
|
|
17
|
+
# zero concat even on the broadcast path.
|
|
18
|
+
#
|
|
19
|
+
class SurveyorRaw
|
|
20
|
+
include Backtrace
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def initialize(engine)
|
|
24
|
+
@engine = engine
|
|
25
|
+
@queues = {} # conn => Async::LimitedQueue
|
|
26
|
+
@pump_tasks = {} # conn => Async::Task
|
|
27
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def send(body, header:)
|
|
32
|
+
@queues.each_value do |q|
|
|
33
|
+
q.enqueue([header, body]) unless q.limited?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def receive
|
|
39
|
+
@recv_queue.dequeue
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def enqueue(wire_bytes, conn)
|
|
44
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
45
|
+
return unless header
|
|
46
|
+
@recv_queue.enqueue([conn, header, payload])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def connection_added(conn)
|
|
51
|
+
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
52
|
+
@queues[conn] = queue
|
|
53
|
+
@pump_tasks[conn] = spawn_pump(conn, queue)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def connection_removed(conn)
|
|
58
|
+
@queues.delete(conn)
|
|
59
|
+
task = @pump_tasks.delete(conn)
|
|
60
|
+
|
|
61
|
+
return unless task
|
|
62
|
+
return if task == Async::Task.current
|
|
63
|
+
|
|
64
|
+
task.stop
|
|
65
|
+
rescue IOError, Errno::EPIPE
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def send_queue_drained?
|
|
70
|
+
@queues.each_value.all? { |q| q.empty? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def close
|
|
75
|
+
@pump_tasks.each_value(&:stop)
|
|
76
|
+
@pump_tasks.clear
|
|
77
|
+
@queues.clear
|
|
78
|
+
@recv_queue.enqueue(nil)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def close_read
|
|
83
|
+
@recv_queue.enqueue(nil)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def spawn_pump(conn, queue)
|
|
91
|
+
annotation = "nnq surveyor_raw pump #{conn.endpoint}"
|
|
92
|
+
parent = @engine.connections[conn]&.barrier || @engine.barrier
|
|
93
|
+
|
|
94
|
+
@engine.spawn_task(annotation:, parent:) do
|
|
95
|
+
loop do
|
|
96
|
+
header, body = queue.dequeue
|
|
97
|
+
conn.send_message(body, header: header)
|
|
98
|
+
@engine.emit_verbose_msg_sent(body)
|
|
99
|
+
rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
100
|
+
break
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/nnq/socket.rb
CHANGED
|
@@ -32,9 +32,9 @@ module NNQ
|
|
|
32
32
|
|
|
33
33
|
# @yieldparam [self] the socket; when a block is passed the socket
|
|
34
34
|
# is {#close}d when the block returns (or raises), File.open-style.
|
|
35
|
-
def initialize(raw: false, linger: Float::INFINITY, send_hwm: Options::DEFAULT_HWM)
|
|
35
|
+
def initialize(raw: false, linger: Float::INFINITY, send_hwm: Options::DEFAULT_HWM, recv_hwm: Options::DEFAULT_HWM)
|
|
36
36
|
@raw = raw
|
|
37
|
-
@options = Options.new(linger: linger, send_hwm: send_hwm)
|
|
37
|
+
@options = Options.new(linger: linger, send_hwm: send_hwm, recv_hwm: recv_hwm)
|
|
38
38
|
@engine = Engine.new(protocol: protocol, options: @options) { |engine| build_routing(engine) }
|
|
39
39
|
|
|
40
40
|
begin
|
|
@@ -3,27 +3,37 @@
|
|
|
3
3
|
require_relative "socket"
|
|
4
4
|
require_relative "routing/surveyor"
|
|
5
5
|
require_relative "routing/respondent"
|
|
6
|
+
require_relative "routing/surveyor_raw"
|
|
7
|
+
require_relative "routing/respondent_raw"
|
|
6
8
|
|
|
7
9
|
module NNQ
|
|
8
10
|
# SURVEYOR (nng surveyor0): broadcast side of the survey pattern.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# Only one outstanding survey at a time — sending a new survey
|
|
13
|
-
# abandons the previous one. Respondents are not obliged to reply.
|
|
11
|
+
# Cooked mode enforces a timed survey window and matches replies by
|
|
12
|
+
# survey id; raw mode fans out with a caller-supplied +header+ and
|
|
13
|
+
# delivers replies as `[pipe, header, body]` with no timer.
|
|
14
14
|
#
|
|
15
15
|
class SURVEYOR0 < Socket
|
|
16
|
-
#
|
|
16
|
+
# Cooked: broadcasts +body+ as a survey to all connected respondents.
|
|
17
17
|
def send_survey(body)
|
|
18
|
+
raise Error, "SURVEYOR#send_survey not available in raw mode" if raw?
|
|
18
19
|
body = frozen_binary(body)
|
|
19
20
|
Reactor.run { @engine.routing.send_survey(body) }
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
24
|
+
# Raw: broadcasts +body+ with +header+ to all connected respondents
|
|
25
|
+
# via per-conn send pumps (header is threaded through the
|
|
26
|
+
# protocol-sp header kwarg — no concat). Raises in cooked mode.
|
|
27
|
+
def send(body, header:)
|
|
28
|
+
raise Error, "SURVEYOR#send not available in cooked mode" unless raw?
|
|
29
|
+
body = frozen_binary(body)
|
|
30
|
+
Reactor.run { @engine.routing.send(body, header: header) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Cooked: receives the next reply within the survey window, raises
|
|
35
|
+
# {NNQ::TimedOut} on window expiry. Raw: returns `[pipe, header, body]`
|
|
36
|
+
# and blocks indefinitely (no survey window).
|
|
27
37
|
def receive
|
|
28
38
|
Reactor.run { @engine.routing.receive }
|
|
29
39
|
end
|
|
@@ -38,31 +48,41 @@ module NNQ
|
|
|
38
48
|
|
|
39
49
|
|
|
40
50
|
def build_routing(engine)
|
|
41
|
-
Routing::Surveyor.new(engine)
|
|
51
|
+
raw? ? Routing::SurveyorRaw.new(engine) : Routing::Surveyor.new(engine)
|
|
42
52
|
end
|
|
43
53
|
end
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
# RESPONDENT (nng respondent0): reply side of the survey pattern.
|
|
47
|
-
#
|
|
48
|
-
#
|
|
57
|
+
# Cooked mode strictly alternates #receive / #send_reply; raw mode
|
|
58
|
+
# exposes the backtrace as an opaque +header+ and the originating
|
|
59
|
+
# surveyor pipe as a live Connection.
|
|
49
60
|
#
|
|
50
61
|
class RESPONDENT0 < Socket
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
# @return [String, nil] survey body, or nil if the socket was closed
|
|
62
|
+
# Cooked: blocks until the next survey arrives. Raw: returns
|
|
63
|
+
# `[pipe, header, body]`.
|
|
54
64
|
def receive
|
|
55
65
|
Reactor.run { @engine.routing.receive }
|
|
56
66
|
end
|
|
57
67
|
|
|
58
68
|
|
|
59
|
-
#
|
|
69
|
+
# Cooked: routes +body+ back to the surveyor that sent the most
|
|
70
|
+
# recent survey. Raises in raw mode.
|
|
60
71
|
def send_reply(body)
|
|
72
|
+
raise Error, "RESPONDENT#send_reply not available in raw mode" if raw?
|
|
61
73
|
body = frozen_binary(body)
|
|
62
74
|
Reactor.run { @engine.routing.send_reply(body) }
|
|
63
75
|
end
|
|
64
76
|
|
|
65
77
|
|
|
78
|
+
# Raw: writes +body+ with +header+ back to +to+. Raises in cooked mode.
|
|
79
|
+
def send(body, to:, header:)
|
|
80
|
+
raise Error, "RESPONDENT#send not available in cooked mode" unless raw?
|
|
81
|
+
body = frozen_binary(body)
|
|
82
|
+
Reactor.run { @engine.routing.send(body, to: to, header: header) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
66
86
|
private
|
|
67
87
|
|
|
68
88
|
|
|
@@ -72,7 +92,7 @@ module NNQ
|
|
|
72
92
|
|
|
73
93
|
|
|
74
94
|
def build_routing(engine)
|
|
75
|
-
Routing::Respondent.new(engine)
|
|
95
|
+
raw? ? Routing::RespondentRaw.new(engine) : Routing::Respondent.new(engine)
|
|
76
96
|
end
|
|
77
97
|
end
|
|
78
98
|
end
|
data/lib/nnq/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nnq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.3'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.3'
|
|
54
54
|
description: Pure Ruby implementation of nanomsg's Scalability Protocols (SP) on top
|
|
55
55
|
of async + io-stream. Per-socket HWM, opportunistic batching, wire-compatible with
|
|
56
56
|
libnng over inproc/ipc/tcp.
|
|
@@ -85,11 +85,15 @@ files:
|
|
|
85
85
|
- lib/nnq/routing/pull.rb
|
|
86
86
|
- lib/nnq/routing/push.rb
|
|
87
87
|
- lib/nnq/routing/rep.rb
|
|
88
|
+
- lib/nnq/routing/rep_raw.rb
|
|
88
89
|
- lib/nnq/routing/req.rb
|
|
90
|
+
- lib/nnq/routing/req_raw.rb
|
|
89
91
|
- lib/nnq/routing/respondent.rb
|
|
92
|
+
- lib/nnq/routing/respondent_raw.rb
|
|
90
93
|
- lib/nnq/routing/send_pump.rb
|
|
91
94
|
- lib/nnq/routing/sub.rb
|
|
92
95
|
- lib/nnq/routing/surveyor.rb
|
|
96
|
+
- lib/nnq/routing/surveyor_raw.rb
|
|
93
97
|
- lib/nnq/socket.rb
|
|
94
98
|
- lib/nnq/surveyor_respondent.rb
|
|
95
99
|
- lib/nnq/transport/inproc.rb
|