nnq 0.6.1 → 0.7.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 +63 -0
- data/lib/nnq/constants.rb +58 -0
- data/lib/nnq/engine/connection_lifecycle.rb +23 -0
- data/lib/nnq/engine/reconnect.rb +1 -26
- data/lib/nnq/engine.rb +52 -17
- data/lib/nnq/routing/bus.rb +9 -10
- data/lib/nnq/routing/pair.rb +7 -0
- data/lib/nnq/routing/pub.rb +4 -13
- data/lib/nnq/routing/pull.rb +8 -0
- data/lib/nnq/routing/rep.rb +11 -0
- data/lib/nnq/routing/rep_raw.rb +10 -0
- data/lib/nnq/routing/req_raw.rb +10 -0
- data/lib/nnq/routing/respondent.rb +10 -0
- data/lib/nnq/routing/respondent_raw.rb +10 -0
- data/lib/nnq/routing/sub.rb +7 -0
- data/lib/nnq/routing/surveyor.rb +18 -13
- data/lib/nnq/routing/surveyor_raw.rb +13 -13
- data/lib/nnq/socket.rb +4 -9
- data/lib/nnq/transport/inproc/pipe.rb +131 -0
- data/lib/nnq/transport/inproc.rb +27 -25
- data/lib/nnq/transport/ipc.rb +5 -2
- data/lib/nnq/transport/tcp.rb +5 -2
- data/lib/nnq/version.rb +1 -1
- data/lib/nnq.rb +12 -11
- metadata +3 -2
- data/lib/nnq/monitor_event.rb +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c55340c77dffd3e4fdbc8419fb463e608a6e635adc577582e295b8af20fdc3e0
|
|
4
|
+
data.tar.gz: 6bb76f07c3732a4e69ad9a8e93a6a852f5ca04aac40fedc0df346ec9a2d74297
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1cb190f96ace0a94363ea6a0b24b9d8b563363fbb3fb84e0bb4927a8ef818949270f20dec984828405bd15068400ace40aa5a9017d52321e2f9e10aa5235d973
|
|
7
|
+
data.tar.gz: 036a2a8203a7a26afad8d67fd23d9606d4947561ec0d8d31893b6efb116b62f623560afc6a4293fdbbd8846c39fa76aa9885eb47c9ab11900d552f5bc4377ecd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 — 2026-04-18
|
|
4
|
+
|
|
5
|
+
- **Inproc transport now uses a queue-based `Inproc::Pipe`** instead
|
|
6
|
+
of a Unix `socketpair(2)` running the full SP protocol.
|
|
7
|
+
`NNQ::Transport::Inproc::Pipe` duck-types `NNQ::Connection` and
|
|
8
|
+
transfers frozen Strings through a pair of `Async::Queue`s (one
|
|
9
|
+
per direction). No framing, no handshake, no kernel buffer copy.
|
|
10
|
+
When a routing strategy supplies an SP backtrace header
|
|
11
|
+
(REQ/REP/SURVEYOR), it's prepended before enqueue so the receive
|
|
12
|
+
side sees the same layout as the TCP/IPC path and `parse_backtrace`
|
|
13
|
+
keeps working unchanged. The new `Engine#connection_ready(conn,
|
|
14
|
+
endpoint:)` and `ConnectionLifecycle#ready_direct!` entry points
|
|
15
|
+
register a pipe as ready without the SP handshake phase.
|
|
16
|
+
- **Inproc direct-recv fast path.** When a routing strategy exposes a
|
|
17
|
+
`#direct_recv_for(conn)` hook, the peer pipe enqueues directly into
|
|
18
|
+
the routing recv queue via `Pipe#wire_direct_recv`, bypassing both
|
|
19
|
+
the intermediate pipe queue and the recv pump fiber. PULL, BUS,
|
|
20
|
+
PAIR, SUB, REP, RESPONDENT, SURVEYOR, and the `*_raw` variants all
|
|
21
|
+
implement the hook; REQ (promise-based) stays on the fiber path.
|
|
22
|
+
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
|
+
- **Routing pumps shed their `@pump_tasks` bookkeeping.** `bus`, `pub`,
|
|
33
|
+
`surveyor`, and `surveyor_raw` no longer track per-connection pump
|
|
34
|
+
tasks in a hash. Pumps are spawned under
|
|
35
|
+
`@engine.connections[conn].barrier`, so `ConnectionLifecycle#tear_down!`
|
|
36
|
+
already cascade-cancels them on `barrier.stop` — the hash was dead
|
|
37
|
+
weight.
|
|
38
|
+
- **Transport registry is pluggable.** `NNQ::Engine.transports` is now a
|
|
39
|
+
mutable class-level `Hash` instead of a frozen constant; each built-in
|
|
40
|
+
transport (`tcp`, `ipc`, `inproc`) self-registers at load with
|
|
41
|
+
`Engine.transports["…"] = self`. External transports (e.g. `nnq-zstd`'s
|
|
42
|
+
`zstd+tcp://`) can register themselves the same way.
|
|
43
|
+
- **`ConnectionLifecycle` calls `transport.wrap_connection(conn, engine)`
|
|
44
|
+
after handshake.** Transports that implement the hook can return a
|
|
45
|
+
delegating wrapper that layers compression / TLS / instrumentation
|
|
46
|
+
over the raw `NNQ::Connection` without the engine caring. Transports
|
|
47
|
+
without the hook (tcp/ipc/inproc) pass through unchanged.
|
|
48
|
+
- **`lib/nnq.rb` restructured to mirror `lib/omq.rb`.** Requires split
|
|
49
|
+
into Core / Transport / Socket-types sections. New
|
|
50
|
+
`lib/nnq/constants.rb` owns `MonitorEvent`, the `CONNECTION_LOST` /
|
|
51
|
+
`CONNECTION_FAILED` error arrays, and `NNQ.freeze_for_ractors!` — all
|
|
52
|
+
previously scattered across `engine.rb`, `reconnect.rb`,
|
|
53
|
+
`monitor_event.rb`, and the top-level `nnq.rb`. `monitor_event.rb` is
|
|
54
|
+
removed (absorbed into constants).
|
|
55
|
+
- **Benchmarks: richer scaffolding, measured via `Async::Clock`.**
|
|
56
|
+
`BenchHelper` gains `NNQ_BENCH_SIZES` / `NNQ_BENCH_TRANSPORTS` /
|
|
57
|
+
`NNQ_BENCH_PEERS` env overrides, a `measure_roundtrip` helper for
|
|
58
|
+
REQ/REP-style patterns, and a `wait_subscribed` helper that closes
|
|
59
|
+
the gap between TCP connect and SUBSCRIBE propagation. All elapsed
|
|
60
|
+
measurements use `Async::Clock.measure { … }` blocks instead of
|
|
61
|
+
`Process.clock_gettime`. `bench/report.rb --update-readme` now
|
|
62
|
+
falls back to the most recent row per cell across all history, so a
|
|
63
|
+
partial bench run refreshes only the cells it covers instead of
|
|
64
|
+
clobbering untouched cells with "—".
|
|
65
|
+
|
|
3
66
|
## 0.6.1 — 2026-04-15
|
|
4
67
|
|
|
5
68
|
- **Verbose trace (`-vvv`) now fires for cooked REQ/REP/RESPONDENT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "io/stream"
|
|
5
|
+
|
|
6
|
+
module NNQ
|
|
7
|
+
# Lifecycle event emitted by {Socket#monitor}.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute [r] type
|
|
10
|
+
# @return [Symbol] event type (:listening, :connected, :disconnected, ...)
|
|
11
|
+
# @!attribute [r] endpoint
|
|
12
|
+
# @return [String, nil] the endpoint involved
|
|
13
|
+
# @!attribute [r] detail
|
|
14
|
+
# @return [Hash, nil] extra context
|
|
15
|
+
#
|
|
16
|
+
MonitorEvent = Data.define(:type, :endpoint, :detail) do
|
|
17
|
+
def initialize(type:, endpoint: nil, detail: nil)
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Errors that indicate an established connection went away. Used by
|
|
24
|
+
# the recv loop, routing pumps, and connection lifecycle to silently
|
|
25
|
+
# terminate (the connection lifecycle's #lost! handler decides
|
|
26
|
+
# whether to reconnect). Not frozen at load time — transport plugins
|
|
27
|
+
# append to this before the first bind/connect, which freezes both
|
|
28
|
+
# arrays.
|
|
29
|
+
CONNECTION_LOST = [
|
|
30
|
+
EOFError,
|
|
31
|
+
IOError,
|
|
32
|
+
Errno::ECONNRESET,
|
|
33
|
+
Errno::EPIPE,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Errors raised when a peer cannot be reached. Triggers a reconnect
|
|
38
|
+
# retry rather than propagating.
|
|
39
|
+
CONNECTION_FAILED = [
|
|
40
|
+
Errno::ECONNREFUSED,
|
|
41
|
+
Errno::EHOSTUNREACH,
|
|
42
|
+
Errno::ENETUNREACH,
|
|
43
|
+
Errno::ENOENT,
|
|
44
|
+
Errno::EPIPE,
|
|
45
|
+
Errno::ETIMEDOUT,
|
|
46
|
+
Socket::ResolutionError,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Freezes module-level state so NNQ sockets can be used inside Ractors.
|
|
51
|
+
# Call this once before spawning any Ractors that create NNQ sockets.
|
|
52
|
+
#
|
|
53
|
+
def self.freeze_for_ractors!
|
|
54
|
+
CONNECTION_LOST.freeze
|
|
55
|
+
CONNECTION_FAILED.freeze
|
|
56
|
+
Engine.transports.freeze
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -104,6 +104,15 @@ module NNQ
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
# Registers an already-ready connection object (e.g. an
|
|
108
|
+
# {Transport::Inproc::Pipe}) without running the SP handshake.
|
|
109
|
+
# Transports that frame on the wire should call {#handshake!}
|
|
110
|
+
# instead.
|
|
111
|
+
def ready_direct!(conn)
|
|
112
|
+
ready!(conn)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
|
|
107
116
|
# Starts a supervisor for this connection. Must be called after
|
|
108
117
|
# all per-connection pumps (recv loop, send pump) have been
|
|
109
118
|
# spawned on the connection barrier. The supervisor blocks until
|
|
@@ -121,6 +130,7 @@ module NNQ
|
|
|
121
130
|
private
|
|
122
131
|
|
|
123
132
|
def ready!(conn)
|
|
133
|
+
conn = wrap_connection(conn)
|
|
124
134
|
@conn = conn
|
|
125
135
|
@engine.connections[conn] = self
|
|
126
136
|
transition!(:ready)
|
|
@@ -170,6 +180,19 @@ module NNQ
|
|
|
170
180
|
end
|
|
171
181
|
|
|
172
182
|
|
|
183
|
+
# Post-handshake transport wrap. A transport that implements
|
|
184
|
+
# `wrap_connection(conn)` (e.g. nnq-zstd's zstd+tcp) returns a
|
|
185
|
+
# delegating wrapper that adds a layer (compression, TLS, …)
|
|
186
|
+
# without the engine caring. Unknown or hook-less transports pass
|
|
187
|
+
# through unchanged.
|
|
188
|
+
def wrap_connection(conn)
|
|
189
|
+
return conn unless @endpoint
|
|
190
|
+
transport = @engine.transport_for(@endpoint)
|
|
191
|
+
return conn unless transport.respond_to?(:wrap_connection)
|
|
192
|
+
transport.wrap_connection(conn, @engine)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
|
|
173
196
|
# Handshake timeout: same logic as TCP.connect_timeout — derived
|
|
174
197
|
# from reconnect_interval (floor 0.5s). Prevents a hang when the
|
|
175
198
|
# peer accepts the TCP connection but never sends an SP greeting.
|
data/lib/nnq/engine/reconnect.rb
CHANGED
|
@@ -2,31 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module NNQ
|
|
4
4
|
class Engine
|
|
5
|
-
# Connection errors that should trigger a reconnect retry rather
|
|
6
|
-
# than propagate. Mutable at load time so plugins (e.g. a future
|
|
7
|
-
# TLS transport) can append their own error classes; frozen on
|
|
8
|
-
# first {Engine#connect}.
|
|
9
|
-
CONNECTION_FAILED = [
|
|
10
|
-
Errno::ECONNREFUSED,
|
|
11
|
-
Errno::EHOSTUNREACH,
|
|
12
|
-
Errno::ENETUNREACH,
|
|
13
|
-
Errno::ENOENT,
|
|
14
|
-
Errno::EPIPE,
|
|
15
|
-
Errno::ETIMEDOUT,
|
|
16
|
-
Socket::ResolutionError,
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
# Errors that indicate an established connection went away. Used
|
|
20
|
-
# by the recv loop and pumps to silently terminate (the connection
|
|
21
|
-
# lifecycle's #lost! handler decides whether to reconnect).
|
|
22
|
-
CONNECTION_LOST = [
|
|
23
|
-
EOFError,
|
|
24
|
-
IOError,
|
|
25
|
-
Errno::ECONNRESET,
|
|
26
|
-
Errno::EPIPE,
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
|
|
30
5
|
# Schedules reconnect attempts with exponential back-off.
|
|
31
6
|
#
|
|
32
7
|
# Runs a background task that loops until a connection is
|
|
@@ -61,7 +36,7 @@ module NNQ
|
|
|
61
36
|
sleep quantized_wait(delay) if delay > 0
|
|
62
37
|
break if @engine.closed?
|
|
63
38
|
begin
|
|
64
|
-
@engine.transport_for(@endpoint).connect(@endpoint, @engine)
|
|
39
|
+
@engine.transport_for(@endpoint).connect(@endpoint, @engine, **@engine.dial_opts_for(@endpoint))
|
|
65
40
|
break
|
|
66
41
|
rescue *CONNECTION_FAILED, *CONNECTION_LOST => e
|
|
67
42
|
delay = next_delay(delay, max_delay)
|
data/lib/nnq/engine.rb
CHANGED
|
@@ -4,16 +4,9 @@ require "async"
|
|
|
4
4
|
require "async/clock"
|
|
5
5
|
require "set"
|
|
6
6
|
require "protocol/sp"
|
|
7
|
-
require_relative "error"
|
|
8
|
-
require_relative "connection"
|
|
9
|
-
require_relative "monitor_event"
|
|
10
|
-
require_relative "reactor"
|
|
11
7
|
require_relative "engine/socket_lifecycle"
|
|
12
8
|
require_relative "engine/connection_lifecycle"
|
|
13
9
|
require_relative "engine/reconnect"
|
|
14
|
-
require_relative "transport/tcp"
|
|
15
|
-
require_relative "transport/ipc"
|
|
16
|
-
require_relative "transport/inproc"
|
|
17
10
|
|
|
18
11
|
module NNQ
|
|
19
12
|
# Per-socket orchestrator. Owns the listener set, the connection map
|
|
@@ -25,11 +18,18 @@ module NNQ
|
|
|
25
18
|
# no HWM bookkeeping, no mechanisms, no heartbeat, no monitor queue.
|
|
26
19
|
#
|
|
27
20
|
class Engine
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
# Scheme → transport module registry. Each transport file
|
|
22
|
+
# self-registers on require; plugins (e.g. nnq-zstd) add more:
|
|
23
|
+
#
|
|
24
|
+
# NNQ::Engine.transports["zstd+tcp"] = NNQ::Transport::ZstdTcp
|
|
25
|
+
#
|
|
26
|
+
@transports = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# @return [Hash{String => Module}] registered transports
|
|
31
|
+
attr_reader :transports
|
|
32
|
+
end
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
# @return [Integer] our SP protocol id (e.g. Protocols::PUSH_V0)
|
|
@@ -95,6 +95,7 @@ module NNQ
|
|
|
95
95
|
@monitor_queue = nil
|
|
96
96
|
@verbose_monitor = false
|
|
97
97
|
@dialed = Set.new
|
|
98
|
+
@dial_opts = {} # endpoint => kwargs for transport.connect on reconnect
|
|
98
99
|
@routing = yield(self)
|
|
99
100
|
end
|
|
100
101
|
|
|
@@ -191,9 +192,9 @@ module NNQ
|
|
|
191
192
|
|
|
192
193
|
|
|
193
194
|
# Binds to +endpoint+. Synchronous: errors propagate.
|
|
194
|
-
def bind(endpoint)
|
|
195
|
+
def bind(endpoint, **opts)
|
|
195
196
|
transport = transport_for(endpoint)
|
|
196
|
-
listener = transport.bind(endpoint, self)
|
|
197
|
+
listener = transport.bind(endpoint, self, **opts)
|
|
197
198
|
listener.start_accept_loop(@lifecycle.barrier) do |io, framing = :tcp|
|
|
198
199
|
handle_accepted(io, endpoint: endpoint, framing: framing)
|
|
199
200
|
end
|
|
@@ -207,12 +208,13 @@ module NNQ
|
|
|
207
208
|
# actual dial happens inside a background reconnect task that
|
|
208
209
|
# retries with exponential back-off until the peer becomes
|
|
209
210
|
# reachable. Inproc connect is synchronous and instant.
|
|
210
|
-
def connect(endpoint)
|
|
211
|
+
def connect(endpoint, **opts)
|
|
211
212
|
@dialed << endpoint
|
|
213
|
+
@dial_opts[endpoint] = opts unless opts.empty?
|
|
212
214
|
@last_endpoint = endpoint
|
|
213
215
|
|
|
214
216
|
if endpoint.start_with?("inproc://")
|
|
215
|
-
transport_for(endpoint).connect(endpoint, self)
|
|
217
|
+
transport_for(endpoint).connect(endpoint, self, **opts)
|
|
216
218
|
else
|
|
217
219
|
emit_monitor_event(:connect_delayed, endpoint: endpoint)
|
|
218
220
|
Reconnect.schedule(endpoint, @options, @lifecycle.barrier, self, delay: 0)
|
|
@@ -220,6 +222,14 @@ module NNQ
|
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
|
|
225
|
+
# Transport options captured from {#connect} for +endpoint+. Used by
|
|
226
|
+
# {Reconnect} to re-dial with the original kwargs. Empty hash for
|
|
227
|
+
# endpoints connected without extra options.
|
|
228
|
+
def dial_opts_for(endpoint)
|
|
229
|
+
@dial_opts[endpoint] || {}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
|
|
223
233
|
# Schedules a reconnect for +endpoint+ if auto-reconnect is enabled
|
|
224
234
|
# and the endpoint is still in the dialed set. Called from the
|
|
225
235
|
# connection lifecycle's `lost!` path.
|
|
@@ -235,7 +245,7 @@ module NNQ
|
|
|
235
245
|
# transport from the URL each iteration.
|
|
236
246
|
def transport_for(endpoint)
|
|
237
247
|
scheme = endpoint[/\A([a-z+]+):\/\//i, 1] or raise Error, "no scheme: #{endpoint}"
|
|
238
|
-
|
|
248
|
+
Engine.transports[scheme] or raise Error, "unsupported transport: #{scheme}"
|
|
239
249
|
end
|
|
240
250
|
|
|
241
251
|
|
|
@@ -263,6 +273,19 @@ module NNQ
|
|
|
263
273
|
end
|
|
264
274
|
|
|
265
275
|
|
|
276
|
+
# Registers an already-connected, framing-free pipe (inproc). Skips
|
|
277
|
+
# the SP handshake entirely — {Transport::Inproc::Pipe} is a Ruby
|
|
278
|
+
# duck-type for {NNQ::Connection} and has no wire protocol.
|
|
279
|
+
def connection_ready(conn, endpoint:)
|
|
280
|
+
lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint, framing: :inproc)
|
|
281
|
+
lifecycle.ready_direct!(conn)
|
|
282
|
+
spawn_recv_loop(conn) if @routing.respond_to?(:enqueue) && @connections.key?(conn)
|
|
283
|
+
lifecycle.start_supervisor!
|
|
284
|
+
rescue ConnectionRejected
|
|
285
|
+
# routing rejected this peer (e.g. PAIR already bonded)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
|
|
266
289
|
# Spawns a task under the given parent barrier (defaults to the
|
|
267
290
|
# socket-level barrier). Used by routing strategies (e.g. PUSH send
|
|
268
291
|
# pump) to attach long-lived fibers to the engine's lifecycle. The
|
|
@@ -345,6 +368,18 @@ module NNQ
|
|
|
345
368
|
|
|
346
369
|
|
|
347
370
|
def spawn_recv_loop(conn)
|
|
371
|
+
# Inproc fast-path: wire the peer pipe to enqueue directly into
|
|
372
|
+
# the routing recv queue, skipping both the recv pump fiber and
|
|
373
|
+
# the intermediate pipe queue. Cuts three fiber hops to one on
|
|
374
|
+
# PUSH/PULL and peers.
|
|
375
|
+
if conn.is_a?(Transport::Inproc::Pipe) && conn.peer && @routing.respond_to?(:direct_recv_for)
|
|
376
|
+
queue, transform = @routing.direct_recv_for(conn)
|
|
377
|
+
if queue
|
|
378
|
+
conn.peer.wire_direct_recv(queue, transform)
|
|
379
|
+
return
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
348
383
|
@connections[conn].barrier.async(annotation: "nnq recv #{conn.endpoint}") do
|
|
349
384
|
loop do
|
|
350
385
|
body = conn.receive_message
|
data/lib/nnq/routing/bus.rb
CHANGED
|
@@ -22,7 +22,6 @@ module NNQ
|
|
|
22
22
|
def initialize(engine)
|
|
23
23
|
@engine = engine
|
|
24
24
|
@queues = {} # conn => Async::LimitedQueue
|
|
25
|
-
@pump_tasks = {} # conn => Async::Task
|
|
26
25
|
@recv_queue = Async::Queue.new
|
|
27
26
|
end
|
|
28
27
|
|
|
@@ -44,6 +43,13 @@ module NNQ
|
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
|
|
46
|
+
# Inproc fast-path hook: peer pipe enqueues directly into the
|
|
47
|
+
# shared recv queue — identity transform, no backtrace or filter.
|
|
48
|
+
def direct_recv_for(_conn)
|
|
49
|
+
[@recv_queue, nil]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
47
53
|
# @return [String, nil] message body, or nil once the socket is closed
|
|
48
54
|
def receive
|
|
49
55
|
@recv_queue.dequeue
|
|
@@ -52,18 +58,13 @@ module NNQ
|
|
|
52
58
|
|
|
53
59
|
def connection_added(conn)
|
|
54
60
|
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
55
|
-
@queues[conn]
|
|
56
|
-
|
|
61
|
+
@queues[conn] = queue
|
|
62
|
+
spawn_pump(conn, queue)
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
def connection_removed(conn)
|
|
61
67
|
@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
68
|
end
|
|
68
69
|
|
|
69
70
|
|
|
@@ -73,8 +74,6 @@ module NNQ
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def close
|
|
76
|
-
@pump_tasks.each_value(&:stop)
|
|
77
|
-
@pump_tasks.clear
|
|
78
77
|
@queues.clear
|
|
79
78
|
@recv_queue.enqueue(nil)
|
|
80
79
|
end
|
data/lib/nnq/routing/pair.rb
CHANGED
|
@@ -48,6 +48,13 @@ module NNQ
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
# Inproc fast-path hook: peer pipe enqueues straight into the
|
|
52
|
+
# local recv queue.
|
|
53
|
+
def direct_recv_for(_conn)
|
|
54
|
+
[@recv_queue, nil]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
|
|
51
58
|
# First-pipe-wins. Raising {ConnectionRejected} tells the
|
|
52
59
|
# ConnectionLifecycle to tear down the just-registered connection
|
|
53
60
|
# without ever exposing it to pumps.
|
data/lib/nnq/routing/pub.rb
CHANGED
|
@@ -19,9 +19,8 @@ module NNQ
|
|
|
19
19
|
#
|
|
20
20
|
class Pub
|
|
21
21
|
def initialize(engine)
|
|
22
|
-
@engine
|
|
23
|
-
@queues
|
|
24
|
-
@pump_tasks = {} # conn => Async::Task
|
|
22
|
+
@engine = engine
|
|
23
|
+
@queues = {} # conn => Async::LimitedQueue
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
|
|
@@ -42,19 +41,13 @@ module NNQ
|
|
|
42
41
|
# control into the new task body, which parks on queue.dequeue;
|
|
43
42
|
# at that park the publisher fiber can run and must already see
|
|
44
43
|
# this peer's queue.
|
|
45
|
-
@queues[conn]
|
|
46
|
-
|
|
44
|
+
@queues[conn] = queue
|
|
45
|
+
spawn_pump(conn, queue)
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
|
|
50
49
|
def connection_removed(conn)
|
|
51
50
|
@queues.delete(conn)
|
|
52
|
-
task = @pump_tasks.delete(conn)
|
|
53
|
-
return unless task
|
|
54
|
-
return if task == Async::Task.current
|
|
55
|
-
task.stop
|
|
56
|
-
rescue IOError, Errno::EPIPE
|
|
57
|
-
# pump was mid-flush; already unwinding
|
|
58
51
|
end
|
|
59
52
|
|
|
60
53
|
|
|
@@ -65,8 +58,6 @@ module NNQ
|
|
|
65
58
|
|
|
66
59
|
|
|
67
60
|
def close
|
|
68
|
-
@pump_tasks.each_value(&:stop)
|
|
69
|
-
@pump_tasks.clear
|
|
70
61
|
@queues.clear
|
|
71
62
|
end
|
|
72
63
|
|
data/lib/nnq/routing/pull.rb
CHANGED
|
@@ -22,6 +22,14 @@ module NNQ
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# Inproc fast-path hook: return the routing recv queue so the
|
|
26
|
+
# peer pipe can enqueue directly, skipping the recv pump fiber.
|
|
27
|
+
# Identity transform — PULL bodies are the user payload already.
|
|
28
|
+
def direct_recv_for(_conn)
|
|
29
|
+
[@queue, nil]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
25
33
|
# @return [String, nil] message body, or nil if the queue was closed
|
|
26
34
|
def receive
|
|
27
35
|
@queue.dequeue
|
data/lib/nnq/routing/rep.rb
CHANGED
|
@@ -89,6 +89,17 @@ module NNQ
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
# Inproc fast-path hook: peer pipe parses the backtrace and
|
|
93
|
+
# enqueues the same [conn, btrace, payload] tuple the pump would.
|
|
94
|
+
def direct_recv_for(conn)
|
|
95
|
+
transform = lambda do |body|
|
|
96
|
+
btrace, payload = parse_backtrace(body)
|
|
97
|
+
btrace ? [conn, btrace, payload] : nil
|
|
98
|
+
end
|
|
99
|
+
[@recv_queue, transform]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
|
|
92
103
|
def connection_removed(conn)
|
|
93
104
|
@mutex.synchronize do
|
|
94
105
|
@pending = nil if @pending && @pending[0] == conn
|
data/lib/nnq/routing/rep_raw.rb
CHANGED
|
@@ -56,6 +56,16 @@ module NNQ
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
# Inproc fast-path hook.
|
|
60
|
+
def direct_recv_for(conn)
|
|
61
|
+
transform = lambda do |wire_bytes|
|
|
62
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
63
|
+
header ? [conn, header, payload] : nil
|
|
64
|
+
end
|
|
65
|
+
[@recv_queue, transform]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
59
69
|
def close
|
|
60
70
|
@recv_queue.enqueue(nil)
|
|
61
71
|
end
|
data/lib/nnq/routing/req_raw.rb
CHANGED
|
@@ -48,6 +48,16 @@ module NNQ
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
# Inproc fast-path hook.
|
|
52
|
+
def direct_recv_for(conn)
|
|
53
|
+
transform = lambda do |wire_bytes|
|
|
54
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
55
|
+
header ? [conn, header, payload] : nil
|
|
56
|
+
end
|
|
57
|
+
[@recv_queue, transform]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
51
61
|
def close
|
|
52
62
|
@recv_queue.enqueue(nil)
|
|
53
63
|
end
|
|
@@ -71,6 +71,16 @@ module NNQ
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
# Inproc fast-path hook.
|
|
75
|
+
def direct_recv_for(conn)
|
|
76
|
+
transform = lambda do |body|
|
|
77
|
+
btrace, payload = parse_backtrace(body)
|
|
78
|
+
btrace ? [conn, btrace, payload] : nil
|
|
79
|
+
end
|
|
80
|
+
[@recv_queue, transform]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
74
84
|
def connection_removed(conn)
|
|
75
85
|
@mutex.synchronize do
|
|
76
86
|
@pending = nil if @pending && @pending[0] == conn
|
|
@@ -47,6 +47,16 @@ module NNQ
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
# Inproc fast-path hook.
|
|
51
|
+
def direct_recv_for(conn)
|
|
52
|
+
transform = lambda do |wire_bytes|
|
|
53
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
54
|
+
header ? [conn, header, payload] : nil
|
|
55
|
+
end
|
|
56
|
+
[@recv_queue, transform]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
|
|
50
60
|
def close
|
|
51
61
|
@recv_queue.enqueue(nil)
|
|
52
62
|
end
|
data/lib/nnq/routing/sub.rb
CHANGED
|
@@ -38,6 +38,13 @@ module NNQ
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
# Inproc fast-path hook: filter via the subscription list in the
|
|
42
|
+
# transform, then enqueue only matching bodies.
|
|
43
|
+
def direct_recv_for(_conn)
|
|
44
|
+
[@queue, ->(body) { matches?(body) ? body : nil }]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
41
48
|
# @return [String, nil]
|
|
42
49
|
def receive
|
|
43
50
|
@queue.dequeue
|
data/lib/nnq/routing/surveyor.rb
CHANGED
|
@@ -24,7 +24,6 @@ module NNQ
|
|
|
24
24
|
def initialize(engine)
|
|
25
25
|
@engine = engine
|
|
26
26
|
@queues = {} # conn => Async::LimitedQueue
|
|
27
|
-
@pump_tasks = {} # conn => Async::Task
|
|
28
27
|
@recv_queue = Async::Queue.new
|
|
29
28
|
@current_id = nil
|
|
30
29
|
@mutex = Mutex.new
|
|
@@ -78,6 +77,21 @@ module NNQ
|
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
|
|
80
|
+
# Inproc fast-path hook. Transform filters replies by current
|
|
81
|
+
# survey id and strips the 4-byte header, mirroring #enqueue.
|
|
82
|
+
def direct_recv_for(_conn)
|
|
83
|
+
mutex = @mutex
|
|
84
|
+
transform = lambda do |body|
|
|
85
|
+
next nil if body.bytesize < 4
|
|
86
|
+
id = body.unpack1("N")
|
|
87
|
+
payload = body.byteslice(4..)
|
|
88
|
+
match = mutex.synchronize { @current_id == id }
|
|
89
|
+
match ? payload : nil
|
|
90
|
+
end
|
|
91
|
+
[@recv_queue, transform]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
81
95
|
# Strips the 4-byte survey id for verbose trace previews.
|
|
82
96
|
def preview_body(wire)
|
|
83
97
|
wire.byteslice(4..) || wire
|
|
@@ -85,21 +99,14 @@ module NNQ
|
|
|
85
99
|
|
|
86
100
|
|
|
87
101
|
def connection_added(conn)
|
|
88
|
-
queue
|
|
89
|
-
@queues[conn]
|
|
90
|
-
|
|
102
|
+
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
103
|
+
@queues[conn] = queue
|
|
104
|
+
spawn_pump(conn, queue)
|
|
91
105
|
end
|
|
92
106
|
|
|
93
107
|
|
|
94
108
|
def connection_removed(conn)
|
|
95
109
|
@queues.delete(conn)
|
|
96
|
-
task = @pump_tasks.delete(conn)
|
|
97
|
-
|
|
98
|
-
return unless task
|
|
99
|
-
return if task == Async::Task.current
|
|
100
|
-
|
|
101
|
-
task.stop
|
|
102
|
-
rescue IOError, Errno::EPIPE
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
|
|
@@ -109,8 +116,6 @@ module NNQ
|
|
|
109
116
|
|
|
110
117
|
|
|
111
118
|
def close
|
|
112
|
-
@pump_tasks.each_value(&:stop)
|
|
113
|
-
@pump_tasks.clear
|
|
114
119
|
@queues.clear
|
|
115
120
|
@recv_queue.enqueue(nil)
|
|
116
121
|
end
|
|
@@ -23,7 +23,6 @@ module NNQ
|
|
|
23
23
|
def initialize(engine)
|
|
24
24
|
@engine = engine
|
|
25
25
|
@queues = {} # conn => Async::LimitedQueue
|
|
26
|
-
@pump_tasks = {} # conn => Async::Task
|
|
27
26
|
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
28
27
|
end
|
|
29
28
|
|
|
@@ -47,6 +46,16 @@ module NNQ
|
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
|
|
49
|
+
# Inproc fast-path hook.
|
|
50
|
+
def direct_recv_for(conn)
|
|
51
|
+
transform = lambda do |wire_bytes|
|
|
52
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
53
|
+
header ? [conn, header, payload] : nil
|
|
54
|
+
end
|
|
55
|
+
[@recv_queue, transform]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
50
59
|
def preview_body(wire)
|
|
51
60
|
_, payload = parse_backtrace(wire)
|
|
52
61
|
payload || wire
|
|
@@ -54,21 +63,14 @@ module NNQ
|
|
|
54
63
|
|
|
55
64
|
|
|
56
65
|
def connection_added(conn)
|
|
57
|
-
queue
|
|
58
|
-
@queues[conn]
|
|
59
|
-
|
|
66
|
+
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
67
|
+
@queues[conn] = queue
|
|
68
|
+
spawn_pump(conn, queue)
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
def connection_removed(conn)
|
|
64
73
|
@queues.delete(conn)
|
|
65
|
-
task = @pump_tasks.delete(conn)
|
|
66
|
-
|
|
67
|
-
return unless task
|
|
68
|
-
return if task == Async::Task.current
|
|
69
|
-
|
|
70
|
-
task.stop
|
|
71
|
-
rescue IOError, Errno::EPIPE
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
|
|
@@ -78,8 +80,6 @@ module NNQ
|
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
def close
|
|
81
|
-
@pump_tasks.each_value(&:stop)
|
|
82
|
-
@pump_tasks.clear
|
|
83
83
|
@queues.clear
|
|
84
84
|
@recv_queue.enqueue(nil)
|
|
85
85
|
end
|
data/lib/nnq/socket.rb
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "async/queue"
|
|
4
4
|
|
|
5
|
-
require_relative "options"
|
|
6
|
-
require_relative "engine"
|
|
7
|
-
require_relative "monitor_event"
|
|
8
|
-
require_relative "reactor"
|
|
9
|
-
|
|
10
5
|
module NNQ
|
|
11
6
|
# Socket base class. Subclasses (PUSH, PULL, ...) wire up a routing
|
|
12
7
|
# strategy and the SP protocol id.
|
|
@@ -50,15 +45,15 @@ module NNQ
|
|
|
50
45
|
end
|
|
51
46
|
|
|
52
47
|
|
|
53
|
-
def bind(endpoint)
|
|
48
|
+
def bind(endpoint, **opts)
|
|
54
49
|
ensure_parent_task
|
|
55
|
-
Reactor.run { @engine.bind(endpoint) }
|
|
50
|
+
Reactor.run { @engine.bind(endpoint, **opts) }
|
|
56
51
|
end
|
|
57
52
|
|
|
58
53
|
|
|
59
|
-
def connect(endpoint)
|
|
54
|
+
def connect(endpoint, **opts)
|
|
60
55
|
ensure_parent_task
|
|
61
|
-
Reactor.run { @engine.connect(endpoint) }
|
|
56
|
+
Reactor.run { @engine.connect(endpoint, **opts) }
|
|
62
57
|
end
|
|
63
58
|
|
|
64
59
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module Transport
|
|
5
|
+
module Inproc
|
|
6
|
+
# Queue-based in-process pipe. Duck-types {NNQ::Connection} so
|
|
7
|
+
# routing strategies, the recv loop, and the send pump work
|
|
8
|
+
# against it unchanged.
|
|
9
|
+
#
|
|
10
|
+
# No wire framing: bodies are transferred as frozen Strings
|
|
11
|
+
# through a pair of {Async::Queue} (one per direction). When an
|
|
12
|
+
# SP backtrace header is supplied (REQ/REP/SURVEYOR paths), it's
|
|
13
|
+
# prepended before enqueue so {#receive_message} returns an
|
|
14
|
+
# already-prefixed body — matching the TCP/IPC framing semantic
|
|
15
|
+
# so routing's `parse_backtrace` parses the same layout either
|
|
16
|
+
# way.
|
|
17
|
+
#
|
|
18
|
+
# Direct-recv fast path: when a routing strategy calls
|
|
19
|
+
# {#wire_direct_recv} on the peer side of a pipe pair, subsequent
|
|
20
|
+
# {#send_message} calls enqueue straight into the consumer's
|
|
21
|
+
# recv queue — the intermediate pipe queue and the recv pump
|
|
22
|
+
# fiber are both skipped. Cuts three fiber hops to one and is
|
|
23
|
+
# what lets inproc PUSH/PULL clear 1M msg/s on YJIT.
|
|
24
|
+
#
|
|
25
|
+
# Wiring happens synchronously inside {Transport::Inproc.connect}
|
|
26
|
+
# (before the call returns to the caller), so there's no window
|
|
27
|
+
# in which a send can precede a wire — no pending buffer needed.
|
|
28
|
+
#
|
|
29
|
+
# Close protocol: {#close} enqueues a `nil` sentinel onto the
|
|
30
|
+
# send side (or the direct queue if wired). The peer's recv loop
|
|
31
|
+
# sees `nil`, raises `EOFError`, and unwinds via its connection
|
|
32
|
+
# supervisor.
|
|
33
|
+
class Pipe
|
|
34
|
+
# @return [String, nil] endpoint URI this pipe was established on
|
|
35
|
+
attr_reader :endpoint
|
|
36
|
+
|
|
37
|
+
# @return [Pipe, nil] the other end of the pair
|
|
38
|
+
attr_accessor :peer
|
|
39
|
+
|
|
40
|
+
# @return [Async::Queue, nil] when non-nil, {#send_message}
|
|
41
|
+
# enqueues here instead of into @send_queue.
|
|
42
|
+
attr_reader :direct_recv_queue
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def initialize(send_queue:, recv_queue:, endpoint:)
|
|
46
|
+
@send_queue = send_queue
|
|
47
|
+
@recv_queue = recv_queue
|
|
48
|
+
@endpoint = endpoint
|
|
49
|
+
@closed = false
|
|
50
|
+
@peer = nil
|
|
51
|
+
@direct_recv_queue = nil
|
|
52
|
+
@direct_recv_transform = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Wires the direct-recv fast path. After this call, messages
|
|
57
|
+
# sent on this pipe bypass the intermediate pipe queue and
|
|
58
|
+
# land directly in +queue+.
|
|
59
|
+
#
|
|
60
|
+
# @param queue [Async::Queue]
|
|
61
|
+
# @param transform [Proc, nil] optional per-message transform;
|
|
62
|
+
# return nil to drop the message (used by filter/parse
|
|
63
|
+
# strategies like SUB or REP).
|
|
64
|
+
def wire_direct_recv(queue, transform)
|
|
65
|
+
@direct_recv_transform = transform
|
|
66
|
+
@direct_recv_queue = queue
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def send_message(body, header: nil)
|
|
71
|
+
raise ClosedError, "connection closed" if @closed
|
|
72
|
+
wire = header ? header + body : body
|
|
73
|
+
|
|
74
|
+
if (q = @direct_recv_queue)
|
|
75
|
+
item = @direct_recv_transform ? @direct_recv_transform.call(wire) : wire
|
|
76
|
+
q.enqueue(item) unless item.nil?
|
|
77
|
+
else
|
|
78
|
+
@send_queue.enqueue(wire)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
alias write_message send_message
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_messages(bodies)
|
|
87
|
+
raise ClosedError, "connection closed" if @closed
|
|
88
|
+
|
|
89
|
+
if (q = @direct_recv_queue)
|
|
90
|
+
transform = @direct_recv_transform
|
|
91
|
+
bodies.each do |body|
|
|
92
|
+
item = transform ? transform.call(body) : body
|
|
93
|
+
q.enqueue(item) unless item.nil?
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
bodies.each { |body| @send_queue.enqueue(body) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# No-op — Async::Queue has no IO buffer to flush.
|
|
102
|
+
def flush
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def receive_message
|
|
108
|
+
item = @recv_queue.dequeue
|
|
109
|
+
raise EOFError, "connection closed" if item.nil?
|
|
110
|
+
item
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def closed?
|
|
115
|
+
@closed
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def close
|
|
120
|
+
return if @closed
|
|
121
|
+
@closed = true
|
|
122
|
+
# Close sentinel goes on whichever queue the peer is reading.
|
|
123
|
+
# When direct-wired, @send_queue is unused; hit the direct
|
|
124
|
+
# queue so the consumer unblocks.
|
|
125
|
+
(@direct_recv_queue || @send_queue).enqueue(nil)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/nnq/transport/inproc.rb
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
|
|
3
|
+
require "async/queue"
|
|
4
|
+
|
|
5
|
+
require_relative "inproc/pipe"
|
|
5
6
|
|
|
6
7
|
module NNQ
|
|
7
8
|
module Transport
|
|
8
9
|
# In-process transport. Both peers live in the same process and
|
|
9
|
-
# exchange
|
|
10
|
+
# exchange frozen Strings through a pair of {Async::Queue}s — no
|
|
11
|
+
# wire framing, no socketpair, no SP handshake.
|
|
10
12
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# implementation.
|
|
13
|
+
# The historical implementation ran through a Unix `socketpair(2)`
|
|
14
|
+
# and the full SP protocol, making inproc roughly as expensive as
|
|
15
|
+
# IPC. Swapping to {Inproc::Pipe} (duck-types {NNQ::Connection})
|
|
16
|
+
# drops the kernel buffer copy, the framing encode/decode, and the
|
|
17
|
+
# handshake — inproc becomes a pure in-process queue transfer.
|
|
17
18
|
#
|
|
18
19
|
module Inproc
|
|
20
|
+
Engine.transports["inproc"] = self
|
|
21
|
+
|
|
19
22
|
@registry = {}
|
|
20
23
|
@mutex = Mutex.new
|
|
21
24
|
|
|
@@ -26,7 +29,7 @@ module NNQ
|
|
|
26
29
|
# @param endpoint [String] e.g. "inproc://my-endpoint"
|
|
27
30
|
# @param engine [Engine]
|
|
28
31
|
# @return [Listener]
|
|
29
|
-
def bind(endpoint, engine)
|
|
32
|
+
def bind(endpoint, engine, **)
|
|
30
33
|
@mutex.synchronize do
|
|
31
34
|
raise Error, "inproc endpoint already bound: #{endpoint}" if @registry.key?(endpoint)
|
|
32
35
|
@registry[endpoint] = engine
|
|
@@ -36,28 +39,27 @@ module NNQ
|
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
# Connects +engine+ to a bound inproc endpoint. Creates a
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
42
|
+
# Connects +engine+ to a bound inproc endpoint. Creates a Pipe
|
|
43
|
+
# pair — one queue per direction — and registers each side with
|
|
44
|
+
# its owning engine via {Engine#connection_ready}. No handshake
|
|
45
|
+
# runs; both ends are live as soon as the pipes are wired.
|
|
43
46
|
#
|
|
44
47
|
# @param endpoint [String]
|
|
45
48
|
# @param engine [Engine]
|
|
46
49
|
# @return [void]
|
|
47
|
-
def connect(endpoint, engine)
|
|
50
|
+
def connect(endpoint, engine, **)
|
|
48
51
|
bound = @mutex.synchronize { @registry[endpoint] }
|
|
49
52
|
raise Error, "inproc endpoint not bound: #{endpoint}" unless bound
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
a_to_b = Async::Queue.new
|
|
55
|
+
b_to_a = Async::Queue.new
|
|
56
|
+
client = Pipe.new(send_queue: a_to_b, recv_queue: b_to_a, endpoint: endpoint)
|
|
57
|
+
server = Pipe.new(send_queue: b_to_a, recv_queue: a_to_b, endpoint: endpoint)
|
|
58
|
+
client.peer = server
|
|
59
|
+
server.peer = client
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# it would block on reading our greeting before we've had
|
|
56
|
-
# a chance to write it.
|
|
57
|
-
bound.spawn_task(annotation: "nnq inproc accept #{endpoint}") do
|
|
58
|
-
bound.handle_accepted(IO::Stream::Buffered.wrap(b), endpoint: endpoint)
|
|
59
|
-
end
|
|
60
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(a), endpoint: endpoint)
|
|
61
|
+
bound.connection_ready(server, endpoint: endpoint)
|
|
62
|
+
engine.connection_ready(client, endpoint: endpoint)
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
|
|
@@ -84,7 +86,7 @@ module NNQ
|
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
# No accept loop: inproc connects
|
|
89
|
+
# No accept loop: inproc connects are fully synchronous.
|
|
88
90
|
def start_accept_loop(_parent_task, &_on_accepted)
|
|
89
91
|
end
|
|
90
92
|
|
data/lib/nnq/transport/ipc.rb
CHANGED
|
@@ -13,13 +13,16 @@ module NNQ
|
|
|
13
13
|
# verbatim.
|
|
14
14
|
#
|
|
15
15
|
module IPC
|
|
16
|
+
Engine.transports["ipc"] = self
|
|
17
|
+
|
|
18
|
+
|
|
16
19
|
class << self
|
|
17
20
|
# Binds an IPC server.
|
|
18
21
|
#
|
|
19
22
|
# @param endpoint [String] e.g. "ipc:///tmp/nnq.sock" or "ipc://@abstract"
|
|
20
23
|
# @param engine [Engine]
|
|
21
24
|
# @return [Listener]
|
|
22
|
-
def bind(endpoint, engine)
|
|
25
|
+
def bind(endpoint, engine, **)
|
|
23
26
|
path = parse_path(endpoint)
|
|
24
27
|
sock_path = to_socket_path(path)
|
|
25
28
|
|
|
@@ -35,7 +38,7 @@ module NNQ
|
|
|
35
38
|
# @param endpoint [String]
|
|
36
39
|
# @param engine [Engine]
|
|
37
40
|
# @return [void]
|
|
38
|
-
def connect(endpoint, engine)
|
|
41
|
+
def connect(endpoint, engine, **)
|
|
39
42
|
path = parse_path(endpoint)
|
|
40
43
|
sock_path = to_socket_path(path)
|
|
41
44
|
sock = UNIXSocket.new(sock_path)
|
data/lib/nnq/transport/tcp.rb
CHANGED
|
@@ -11,13 +11,16 @@ module NNQ
|
|
|
11
11
|
# accept inside an Async fiber.
|
|
12
12
|
#
|
|
13
13
|
module TCP
|
|
14
|
+
Engine.transports["tcp"] = self
|
|
15
|
+
|
|
16
|
+
|
|
14
17
|
class << self
|
|
15
18
|
# Binds a TCP server to +endpoint+.
|
|
16
19
|
#
|
|
17
20
|
# @param endpoint [String] e.g. "tcp://127.0.0.1:5570" or "tcp://127.0.0.1:0"
|
|
18
21
|
# @param engine [Engine]
|
|
19
22
|
# @return [Listener]
|
|
20
|
-
def bind(endpoint, engine)
|
|
23
|
+
def bind(endpoint, engine, **)
|
|
21
24
|
host, port = parse_endpoint(endpoint)
|
|
22
25
|
host = "0.0.0.0" if host == "*"
|
|
23
26
|
server = TCPServer.new(host, port)
|
|
@@ -34,7 +37,7 @@ module NNQ
|
|
|
34
37
|
# @param endpoint [String]
|
|
35
38
|
# @param engine [Engine]
|
|
36
39
|
# @return [void]
|
|
37
|
-
def connect(endpoint, engine)
|
|
40
|
+
def connect(endpoint, engine, **)
|
|
38
41
|
host, port = parse_endpoint(endpoint)
|
|
39
42
|
sock = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
|
|
40
43
|
|
data/lib/nnq/version.rb
CHANGED
data/lib/nnq.rb
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "protocol/sp"
|
|
4
|
+
require "io/stream"
|
|
4
5
|
|
|
5
|
-
module NNQ
|
|
6
|
-
# Freezes module-level state so NNQ sockets can be used inside Ractors.
|
|
7
|
-
# Call this once before spawning any Ractors that create NNQ sockets.
|
|
8
|
-
#
|
|
9
|
-
def self.freeze_for_ractors!
|
|
10
|
-
Engine::CONNECTION_FAILED.freeze
|
|
11
|
-
Engine::CONNECTION_LOST.freeze
|
|
12
|
-
Engine::TRANSPORTS.freeze
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
6
|
|
|
7
|
+
# Core
|
|
16
8
|
require_relative "nnq/version"
|
|
17
|
-
require_relative "nnq/
|
|
9
|
+
require_relative "nnq/constants"
|
|
10
|
+
require_relative "nnq/reactor"
|
|
18
11
|
require_relative "nnq/options"
|
|
12
|
+
require_relative "nnq/error"
|
|
19
13
|
require_relative "nnq/connection"
|
|
20
14
|
require_relative "nnq/engine"
|
|
15
|
+
|
|
16
|
+
# Transport
|
|
17
|
+
require_relative "nnq/transport/inproc"
|
|
18
|
+
require_relative "nnq/transport/tcp"
|
|
19
|
+
require_relative "nnq/transport/ipc"
|
|
20
|
+
|
|
21
|
+
# Socket types
|
|
21
22
|
require_relative "nnq/socket"
|
|
22
23
|
require_relative "nnq/push_pull"
|
|
23
24
|
require_relative "nnq/pair"
|
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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -66,12 +66,12 @@ files:
|
|
|
66
66
|
- lib/nnq.rb
|
|
67
67
|
- lib/nnq/bus.rb
|
|
68
68
|
- lib/nnq/connection.rb
|
|
69
|
+
- lib/nnq/constants.rb
|
|
69
70
|
- lib/nnq/engine.rb
|
|
70
71
|
- lib/nnq/engine/connection_lifecycle.rb
|
|
71
72
|
- lib/nnq/engine/reconnect.rb
|
|
72
73
|
- lib/nnq/engine/socket_lifecycle.rb
|
|
73
74
|
- lib/nnq/error.rb
|
|
74
|
-
- lib/nnq/monitor_event.rb
|
|
75
75
|
- lib/nnq/options.rb
|
|
76
76
|
- lib/nnq/pair.rb
|
|
77
77
|
- lib/nnq/pub_sub.rb
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- lib/nnq/socket.rb
|
|
98
98
|
- lib/nnq/surveyor_respondent.rb
|
|
99
99
|
- lib/nnq/transport/inproc.rb
|
|
100
|
+
- lib/nnq/transport/inproc/pipe.rb
|
|
100
101
|
- lib/nnq/transport/ipc.rb
|
|
101
102
|
- lib/nnq/transport/tcp.rb
|
|
102
103
|
- lib/nnq/version.rb
|
data/lib/nnq/monitor_event.rb
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module NNQ
|
|
4
|
-
# Lifecycle event emitted by {Socket#monitor}.
|
|
5
|
-
#
|
|
6
|
-
# @!attribute [r] type
|
|
7
|
-
# @return [Symbol] event type (:listening, :connected, :disconnected, ...)
|
|
8
|
-
# @!attribute [r] endpoint
|
|
9
|
-
# @return [String, nil] the endpoint involved
|
|
10
|
-
# @!attribute [r] detail
|
|
11
|
-
# @return [Hash, nil] extra context
|
|
12
|
-
#
|
|
13
|
-
MonitorEvent = Data.define(:type, :endpoint, :detail) do
|
|
14
|
-
def initialize(type:, endpoint: nil, detail: nil)
|
|
15
|
-
super
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|