nnq 0.7.0 → 0.8.2
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 +45 -9
- data/lib/nnq/bus.rb +1 -1
- data/lib/nnq/engine/connection_lifecycle.rb +5 -4
- data/lib/nnq/engine.rb +9 -6
- data/lib/nnq/pair.rb +1 -1
- data/lib/nnq/pub_sub.rb +1 -1
- data/lib/nnq/push_pull.rb +1 -1
- data/lib/nnq/req_rep.rb +4 -4
- data/lib/nnq/routing/backtrace.rb +1 -1
- data/lib/nnq/routing/req.rb +1 -1
- data/lib/nnq/routing/surveyor.rb +2 -2
- data/lib/nnq/socket.rb +14 -9
- data/lib/nnq/surveyor_respondent.rb +4 -4
- data/lib/nnq/transport/inproc/pipe.rb +12 -1
- data/lib/nnq/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 559ecdf415aadfb477c89de0594cbaa5260a567664196c218c9315d2ad93e42c
|
|
4
|
+
data.tar.gz: a732f598f7558f0e174a30b2f146bed2a9287883bb0614418fdd9d60b0271df5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 511582392bd0d3a18b032158ab13215fe33f469e0b46d1d2ddc03b9f10cfdf433b37cb24dc1ba40a42721d34a774844a3a05e8eb68d463bfa9a8fde5204ae0ca
|
|
7
|
+
data.tar.gz: 9d40868d84ae3ccb1fd78aba9bf88c0f0fb3f4646a330b56a424e31d89d57358d686a3feb8df4e1f761519e80cd0c5ab3170a0c7e06019715e8ee817eb9d0b27
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2 — 2026-04-20
|
|
4
|
+
|
|
5
|
+
- **`:message_received` verbose events carry `wire_size:`.** The
|
|
6
|
+
engine recv loop now reads `conn.last_wire_size_in` (duck-typed,
|
|
7
|
+
nil-safe) and passes it through `emit_verbose_msg_received` into
|
|
8
|
+
the monitor event detail, so compression decorators like nnq-zstd
|
|
9
|
+
can surface the compressed on-the-wire byte count to `-vvv`
|
|
10
|
+
traces. Plain transports are unaffected (`wire_size: nil`).
|
|
11
|
+
|
|
12
|
+
## 0.8.1 — 2026-04-19
|
|
13
|
+
|
|
14
|
+
- **Fix close-race in `ConnectionLifecycle#tear_down!`.** The fd was
|
|
15
|
+
closed before sibling pumps were cancelled, which woke recv fibers
|
|
16
|
+
parked in `io_wait` with `IOError: stream closed in another thread`.
|
|
17
|
+
`@barrier.stop` now runs before `@conn.close`, so blocking reads
|
|
18
|
+
unwind via `Async::Stop` before the fd goes away.
|
|
19
|
+
|
|
20
|
+
## 0.8.0 — 2026-04-19
|
|
21
|
+
|
|
22
|
+
- **Uniform frozen + `BINARY` message contract across transports.**
|
|
23
|
+
`Socket#coerce_binary` replaces the old `frozen_binary` + `.b.freeze`
|
|
24
|
+
copy on the hot send path. Every send method runs its body through
|
|
25
|
+
`coerce_binary`, which:
|
|
26
|
+
- coerces non-String bodies via `#to_str` (nil / `42` / `:foo` raise
|
|
27
|
+
`NoMethodError` instead of producing a zero-byte frame);
|
|
28
|
+
- re-tags unfrozen non-BINARY bodies to `Encoding::BINARY` in place —
|
|
29
|
+
a flag flip, no copy;
|
|
30
|
+
- freezes the body.
|
|
31
|
+
|
|
32
|
+
Receivers always see a frozen BINARY-tagged body: TCP/IPC get it via
|
|
33
|
+
the recv-pump freeze, inproc gets it via `Pipe#send_message`, which
|
|
34
|
+
only allocates for the pathological case of a frozen non-BINARY body
|
|
35
|
+
(the typical `# frozen_string_literal: true` UTF-8 literal). Bodies
|
|
36
|
+
returned by REP/REQ/SURVEYOR/RESPONDENT (cooked and raw) are frozen
|
|
37
|
+
by `parse_backtrace` and the REQ/SURVEYOR id-parsing paths. Mutation
|
|
38
|
+
bugs surface as `FrozenError` instead of silently corrupting a shared
|
|
39
|
+
reference on the inproc fast path. Inproc throughput pays ~20-30%
|
|
40
|
+
for the contract; TCP/IPC unaffected.
|
|
41
|
+
|
|
42
|
+
- **Benchmarks send fresh strings per iteration.** `BenchHelper.run`
|
|
43
|
+
passes an unfrozen `"x" * size` through to the burst closure; the
|
|
44
|
+
`measure` / `measure_roundtrip` bursts `.dup` it before each send.
|
|
45
|
+
More realistic than reusing one frozen payload and hitting every
|
|
46
|
+
fast path in `coerce_binary` + `Pipe#send_message`.
|
|
47
|
+
|
|
3
48
|
## 0.7.0 — 2026-04-18
|
|
4
49
|
|
|
5
50
|
- **Inproc transport now uses a queue-based `Inproc::Pipe`** instead
|
|
@@ -20,15 +65,6 @@
|
|
|
20
65
|
PAIR, SUB, REP, RESPONDENT, SURVEYOR, and the `*_raw` variants all
|
|
21
66
|
implement the hook; REQ (promise-based) stays on the fiber path.
|
|
22
67
|
Cuts three fiber hops to one on the steady-state recv path.
|
|
23
|
-
|
|
24
|
-
Inproc PUSH/PULL single-peer throughput (Ruby 4.0.2):
|
|
25
|
-
|
|
26
|
-
| Size | Before (no JIT) | After (no JIT) | After (+YJIT) |
|
|
27
|
-
|---|---|---|---|
|
|
28
|
-
| 128 B | 122k msg/s | 350k msg/s | 1,226k msg/s |
|
|
29
|
-
| 2 KiB | 87k msg/s | 360k msg/s | 1,458k msg/s |
|
|
30
|
-
| 32 KiB | 21k msg/s | 261k msg/s | 887k msg/s |
|
|
31
|
-
|
|
32
68
|
- **Routing pumps shed their `@pump_tasks` bookkeeping.** `bus`, `pub`,
|
|
33
69
|
`surveyor`, and `surveyor_raw` no longer track per-connection pump
|
|
34
70
|
tasks in a hash. Pumps are spawned under
|
data/lib/nnq/bus.rb
CHANGED
|
@@ -151,6 +151,11 @@ module NNQ
|
|
|
151
151
|
def tear_down!(reconnect: false)
|
|
152
152
|
return if @state == :closed
|
|
153
153
|
transition!(:closed)
|
|
154
|
+
# Cancel sibling pumps BEFORE closing the fd. If we close first,
|
|
155
|
+
# any pump still parked in io_wait wakes up with
|
|
156
|
+
# `IOError: stream closed in another thread`. The caller is the
|
|
157
|
+
# supervisor task, which is NOT in the barrier — no self-stop.
|
|
158
|
+
@barrier.stop
|
|
154
159
|
if @conn
|
|
155
160
|
@engine.connections.delete(@conn)
|
|
156
161
|
@engine.routing.connection_removed(@conn) if @engine.routing.respond_to?(:connection_removed)
|
|
@@ -159,10 +164,6 @@ module NNQ
|
|
|
159
164
|
@engine.resolve_all_peers_gone_if_empty
|
|
160
165
|
end
|
|
161
166
|
@engine.maybe_reconnect(@endpoint) if reconnect
|
|
162
|
-
# Cancel every sibling pump of this connection. The caller is
|
|
163
|
-
# the supervisor task, which is NOT in the barrier — so there
|
|
164
|
-
# is no self-stop risk.
|
|
165
|
-
@barrier.stop
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
|
data/lib/nnq/engine.rb
CHANGED
|
@@ -118,10 +118,12 @@ module NNQ
|
|
|
118
118
|
|
|
119
119
|
|
|
120
120
|
# Emits a :message_received verbose event. Same early-return
|
|
121
|
-
# discipline as {#emit_verbose_msg_sent}.
|
|
122
|
-
|
|
121
|
+
# discipline as {#emit_verbose_msg_sent}. +wire_size+ is the
|
|
122
|
+
# on-the-wire byte count when the connection is wrapped by a
|
|
123
|
+
# compression decorator (nnq-zstd); nil for plain transports.
|
|
124
|
+
def emit_verbose_msg_received(body, wire_size: nil)
|
|
123
125
|
return unless @verbose_monitor
|
|
124
|
-
emit_monitor_event(:message_received, detail: { body: body })
|
|
126
|
+
emit_monitor_event(:message_received, detail: { body: body, wire_size: wire_size })
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
|
|
@@ -382,10 +384,11 @@ module NNQ
|
|
|
382
384
|
|
|
383
385
|
@connections[conn].barrier.async(annotation: "nnq recv #{conn.endpoint}") do
|
|
384
386
|
loop do
|
|
385
|
-
body = conn.receive_message
|
|
387
|
+
body = conn.receive_message.freeze
|
|
386
388
|
if @verbose_monitor
|
|
387
|
-
preview
|
|
388
|
-
|
|
389
|
+
preview = @routing.respond_to?(:preview_body) ? @routing.preview_body(body) : body
|
|
390
|
+
wire_size = conn.respond_to?(:last_wire_size_in) ? conn.last_wire_size_in : nil
|
|
391
|
+
emit_verbose_msg_received(preview, wire_size: wire_size)
|
|
389
392
|
end
|
|
390
393
|
@routing.enqueue(body, conn)
|
|
391
394
|
rescue *CONNECTION_LOST, Async::Stop
|
data/lib/nnq/pair.rb
CHANGED
data/lib/nnq/pub_sub.rb
CHANGED
data/lib/nnq/push_pull.rb
CHANGED
data/lib/nnq/req_rep.rb
CHANGED
|
@@ -18,7 +18,7 @@ module NNQ
|
|
|
18
18
|
# raw mode — use {#send} / {#receive} there.
|
|
19
19
|
def send_request(body)
|
|
20
20
|
raise Error, "REQ#send_request not available in raw mode" if raw?
|
|
21
|
-
body =
|
|
21
|
+
body = coerce_binary(body)
|
|
22
22
|
Reactor.run { @engine.routing.send_request(body) }
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -29,7 +29,7 @@ module NNQ
|
|
|
29
29
|
# cooked mode.
|
|
30
30
|
def send(body, header:)
|
|
31
31
|
raise Error, "REQ#send not available in cooked mode" unless raw?
|
|
32
|
-
body =
|
|
32
|
+
body = coerce_binary(body)
|
|
33
33
|
Reactor.run { @engine.routing.send(body, header: header) }
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -74,7 +74,7 @@ module NNQ
|
|
|
74
74
|
# came from. Raises in raw mode.
|
|
75
75
|
def send_reply(body)
|
|
76
76
|
raise Error, "REP#send_reply not available in raw mode" if raw?
|
|
77
|
-
body =
|
|
77
|
+
body = coerce_binary(body)
|
|
78
78
|
Reactor.run { @engine.routing.send_reply(body) }
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -84,7 +84,7 @@ module NNQ
|
|
|
84
84
|
# tuple). Silent drop if +to+ is closed. Raises in cooked mode.
|
|
85
85
|
def send(body, to:, header:)
|
|
86
86
|
raise Error, "REP#send not available in cooked mode" unless raw?
|
|
87
|
-
body =
|
|
87
|
+
body = coerce_binary(body)
|
|
88
88
|
Reactor.run { @engine.routing.send(body, to: to, header: header) }
|
|
89
89
|
end
|
|
90
90
|
|
data/lib/nnq/routing/req.rb
CHANGED
data/lib/nnq/routing/surveyor.rb
CHANGED
|
@@ -67,7 +67,7 @@ module NNQ
|
|
|
67
67
|
return if body.bytesize < 4
|
|
68
68
|
|
|
69
69
|
id = body.unpack1("N")
|
|
70
|
-
payload = body.byteslice(4..)
|
|
70
|
+
payload = body.byteslice(4..).freeze
|
|
71
71
|
|
|
72
72
|
@mutex.synchronize do
|
|
73
73
|
return unless @current_id == id
|
|
@@ -84,7 +84,7 @@ module NNQ
|
|
|
84
84
|
transform = lambda do |body|
|
|
85
85
|
next nil if body.bytesize < 4
|
|
86
86
|
id = body.unpack1("N")
|
|
87
|
-
payload = body.byteslice(4..)
|
|
87
|
+
payload = body.byteslice(4..).freeze
|
|
88
88
|
match = mutex.synchronize { @current_id == id }
|
|
89
89
|
match ? payload : nil
|
|
90
90
|
end
|
data/lib/nnq/socket.rb
CHANGED
|
@@ -135,16 +135,21 @@ module NNQ
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
|
|
138
|
-
# Coerces +body+ to a frozen
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
138
|
+
# Coerces +body+ to a frozen `Encoding::BINARY`-tagged String and
|
|
139
|
+
# returns it. Every send method runs its body through this so the
|
|
140
|
+
# receiver sees a uniform frozen+BINARY contract across transports
|
|
141
|
+
# (mutation bugs raise `FrozenError` instead of silently corrupting
|
|
142
|
+
# a shared reference on the inproc fast path).
|
|
143
143
|
#
|
|
144
|
-
# Fast-path:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
# Fast-path: unfrozen non-BINARY strings are re-tagged in place
|
|
145
|
+
# (force_encoding is a flag flip, no copy). The pathological case
|
|
146
|
+
# of a frozen non-BINARY body (e.g. a `# frozen_string_literal: true`
|
|
147
|
+
# literal) can't be re-tagged in place — the inproc {Pipe} handles
|
|
148
|
+
# that with a copy so the receive contract stays uniform.
|
|
149
|
+
def coerce_binary(body)
|
|
150
|
+
body = body.to_str unless body.is_a?(String)
|
|
151
|
+
body.force_encoding(Encoding::BINARY) unless body.frozen? || body.encoding == Encoding::BINARY
|
|
152
|
+
body.freeze
|
|
148
153
|
end
|
|
149
154
|
|
|
150
155
|
|
|
@@ -16,7 +16,7 @@ module NNQ
|
|
|
16
16
|
# Cooked: broadcasts +body+ as a survey to all connected respondents.
|
|
17
17
|
def send_survey(body)
|
|
18
18
|
raise Error, "SURVEYOR#send_survey not available in raw mode" if raw?
|
|
19
|
-
body =
|
|
19
|
+
body = coerce_binary(body)
|
|
20
20
|
Reactor.run { @engine.routing.send_survey(body) }
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ module NNQ
|
|
|
26
26
|
# protocol-sp header kwarg — no concat). Raises in cooked mode.
|
|
27
27
|
def send(body, header:)
|
|
28
28
|
raise Error, "SURVEYOR#send not available in cooked mode" unless raw?
|
|
29
|
-
body =
|
|
29
|
+
body = coerce_binary(body)
|
|
30
30
|
Reactor.run { @engine.routing.send(body, header: header) }
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -70,7 +70,7 @@ module NNQ
|
|
|
70
70
|
# recent survey. Raises in raw mode.
|
|
71
71
|
def send_reply(body)
|
|
72
72
|
raise Error, "RESPONDENT#send_reply not available in raw mode" if raw?
|
|
73
|
-
body =
|
|
73
|
+
body = coerce_binary(body)
|
|
74
74
|
Reactor.run { @engine.routing.send_reply(body) }
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -78,7 +78,7 @@ module NNQ
|
|
|
78
78
|
# Raw: writes +body+ with +header+ back to +to+. Raises in cooked mode.
|
|
79
79
|
def send(body, to:, header:)
|
|
80
80
|
raise Error, "RESPONDENT#send not available in cooked mode" unless raw?
|
|
81
|
-
body =
|
|
81
|
+
body = coerce_binary(body)
|
|
82
82
|
Reactor.run { @engine.routing.send(body, to: to, header: header) }
|
|
83
83
|
end
|
|
84
84
|
|
|
@@ -69,7 +69,14 @@ module NNQ
|
|
|
69
69
|
|
|
70
70
|
def send_message(body, header: nil)
|
|
71
71
|
raise ClosedError, "connection closed" if @closed
|
|
72
|
-
|
|
72
|
+
|
|
73
|
+
# Socket#coerce_binary tags mutable bodies BINARY in place;
|
|
74
|
+
# the pathological case of a frozen non-BINARY body (e.g. a
|
|
75
|
+
# `# frozen_string_literal: true` literal) can't be re-tagged
|
|
76
|
+
# in place, so copy it here to keep the receiver contract
|
|
77
|
+
# uniform with TCP/IPC.
|
|
78
|
+
body = body.b.freeze if body.encoding != Encoding::BINARY
|
|
79
|
+
wire = header ? (header + body).freeze : body
|
|
73
80
|
|
|
74
81
|
if (q = @direct_recv_queue)
|
|
75
82
|
item = @direct_recv_transform ? @direct_recv_transform.call(wire) : wire
|
|
@@ -86,6 +93,10 @@ module NNQ
|
|
|
86
93
|
def write_messages(bodies)
|
|
87
94
|
raise ClosedError, "connection closed" if @closed
|
|
88
95
|
|
|
96
|
+
if bodies.any? { |b| b.encoding != Encoding::BINARY }
|
|
97
|
+
bodies = bodies.map { |b| b.encoding == Encoding::BINARY ? b : b.b.freeze }
|
|
98
|
+
end
|
|
99
|
+
|
|
89
100
|
if (q = @direct_recv_queue)
|
|
90
101
|
transform = @direct_recv_transform
|
|
91
102
|
bodies.each do |body|
|
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.8.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -41,16 +41,16 @@ dependencies:
|
|
|
41
41
|
name: protocol-sp
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.4'
|
|
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.4'
|
|
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.
|