nnq-cli 0.2.0 → 0.4.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 +71 -0
- data/lib/nnq/cli/base_runner.rb +68 -28
- data/lib/nnq/cli/cli_parser.rb +52 -47
- data/lib/nnq/cli/expression_evaluator.rb +41 -50
- data/lib/nnq/cli/formatter.rb +23 -56
- data/lib/nnq/cli/pipe.rb +9 -9
- data/lib/nnq/cli/pipe_worker.rb +10 -16
- data/lib/nnq/cli/req_rep.rb +22 -17
- data/lib/nnq/cli/socket_setup.rb +27 -10
- data/lib/nnq/cli/surveyor_respondent.rb +8 -11
- data/lib/nnq/cli/term.rb +2 -2
- data/lib/nnq/cli/version.rb +1 -1
- data/lib/nnq/cli.rb +13 -2
- metadata +12 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfc34e5b7774c4c3288d440d5172391b098055fd2c376d3827e8a5944dd085d7
|
|
4
|
+
data.tar.gz: '0836095278d158e9b52234060cbfe111abcdc282a31659480ff2eb9db0bad5e8'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d34031ee7de234d6f1f88305b8cbd7699932216bec86bb6f05191e6001608e899ff29160934cc9c9a34b9c8fc71c7bff2d74014904a85753bd63d947bf88f78b
|
|
7
|
+
data.tar.gz: 84a46b47822df0538eb9a875142b01893370c3222b22728ede2dfb1578354cd21775f22bc343daaded4bc60ba56f99af6a44dd0edb40f4181cab44040cc45fc7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-04-16
|
|
4
|
+
|
|
5
|
+
- **`-P0` auto-sizes to `nproc`** (capped at 16). `nnq pipe -P 0`
|
|
6
|
+
now spawns one Ractor worker per online processor instead of
|
|
7
|
+
erroring. Explicit values still clamp to `1..16`.
|
|
8
|
+
|
|
9
|
+
## 0.3.2 — 2026-04-16
|
|
10
|
+
|
|
11
|
+
- **REQ generator mode (`-E`/`-e` with no stdin).** `nnq req` now
|
|
12
|
+
produces each request from the send-eval alone when no `-d`/`-F`
|
|
13
|
+
is given and stdin is not piped, matching the existing PUSH/PUB
|
|
14
|
+
generator behaviour. Bounded by `-n` or paced by `-i` like the
|
|
15
|
+
other generator-capable runners.
|
|
16
|
+
- **System tests split into themed files under `test/system/`.**
|
|
17
|
+
Replaces the monolithic `test/system_test.sh` with 10 standalone
|
|
18
|
+
files sharing `test/system/support.sh` helpers.
|
|
19
|
+
`test/system/run_all.sh` chains them; each file also runs
|
|
20
|
+
standalone. New `rake test:system` task invokes `run_all.sh`.
|
|
21
|
+
- **System tests for REQ/PUSH/PUB `-E` generator mode.** REQ fires
|
|
22
|
+
`-E'"foo"' -n 3` against a REP running `-e 'it.upcase' -n 3` and
|
|
23
|
+
verifies three `FOO` replies round-trip; PUSH and PUB get the
|
|
24
|
+
same treatment.
|
|
25
|
+
|
|
26
|
+
## 0.3.1 — 2026-04-15
|
|
27
|
+
|
|
28
|
+
- **Messages are single `String`s, not 1-element arrays.** Every
|
|
29
|
+
runner, the formatter, and the expression evaluator used to wrap
|
|
30
|
+
each message body in a one-element array as a historical
|
|
31
|
+
artifact of multipart thinking. That's gone now: `Formatter#pack`/
|
|
32
|
+
`#unpack` take and return a `String`, `Formatter.preview` takes a
|
|
33
|
+
`String`, and REQ/REP/SURVEYOR/RESPONDENT/pipe runners pass the
|
|
34
|
+
body straight through without `.first` or array literals. Fixes
|
|
35
|
+
an `undefined method 'first' for an instance of String` crash in
|
|
36
|
+
`ReqRunner#request_and_receive` that was masked by the tests.
|
|
37
|
+
- **`-e` / `-E` expressions use Ruby 3.4's `it` default block
|
|
38
|
+
variable** (and accept explicit `|msg|` block-parameter syntax
|
|
39
|
+
via `proc { |msg| ... }`). The old implicit `$_`/`$F` aliasing is
|
|
40
|
+
gone; expressions are compiled as plain `proc { EXPR }` and
|
|
41
|
+
evaluated via `instance_exec(msg, &block)`, so `it.upcase` and
|
|
42
|
+
`|msg| msg.upcase` both work. Cleaner, faster, and no global
|
|
43
|
+
state.
|
|
44
|
+
- **Requires nnq >= 0.6.1** for the `-vvv` trace fixes — cooked
|
|
45
|
+
REQ/REP/RESPONDENT now emit `>>` lines and recv previews strip
|
|
46
|
+
the SP backtrace header so you see the actual payload.
|
|
47
|
+
- **`test/system_test.sh`** — new shell-based system test suite
|
|
48
|
+
modeled on omq-cli's, covering REQ/REP (basic, `--echo`, `-e`),
|
|
49
|
+
the ported `-vvv` REQ/REP verbose-trace assertion from omq-cli
|
|
50
|
+
commit `950890911de078`, PUSH/PULL, PUB/SUB, and the `@name`
|
|
51
|
+
abstract-namespace shortcut for both `-b` and `-c`.
|
|
52
|
+
|
|
53
|
+
## 0.3.0 — 2026-04-15
|
|
54
|
+
|
|
55
|
+
- **Compression switched from LZ4 to Zstd** via the new
|
|
56
|
+
[nnq-zstd](https://github.com/paddor/nnq-zstd) gem. `-z`
|
|
57
|
+
(fast, level −3) and `-Z` (balanced, level 3) are mutually
|
|
58
|
+
exclusive and map to transparent `NNQ::Zstd.wrap` decorators
|
|
59
|
+
around each socket. Sender-side dictionary training and in-band
|
|
60
|
+
dict shipping mean compression ratios on streams of similar
|
|
61
|
+
small messages are now dramatically better than the old
|
|
62
|
+
stateless LZ4 path. Both peers still have to pass `-z`/`-Z`;
|
|
63
|
+
there is no negotiation.
|
|
64
|
+
- **`-Z` / `--compress-high`** flag for balanced Zstd.
|
|
65
|
+
- **Formatter no longer knows about compression.** All per-message
|
|
66
|
+
compression state and logic moved out of `Formatter`; compression
|
|
67
|
+
is applied transparently at the socket boundary via the new
|
|
68
|
+
wrapper. Fewer code paths in runners.
|
|
69
|
+
- **`rlz4` dependency dropped** in favor of `nnq-zstd ~> 0.1`.
|
|
70
|
+
- **Lazy loading.** `nnq/zstd` (and therefore `rzstd` and the Rust
|
|
71
|
+
extension) is only required when `-z`/`-Z` is actually used,
|
|
72
|
+
keeping startup cost unchanged for non-compressing runs.
|
|
73
|
+
|
|
3
74
|
## 0.2.0 — 2026-04-15
|
|
4
75
|
|
|
5
76
|
- **Peer wait for bind-mode bounded senders** — `nnq push -b ... -d
|
data/lib/nnq/cli/base_runner.rb
CHANGED
|
@@ -5,14 +5,15 @@ module NNQ
|
|
|
5
5
|
# Template runner base class for all socket-type CLI runners.
|
|
6
6
|
# Subclasses override {#run_loop} to implement socket-specific behaviour.
|
|
7
7
|
#
|
|
8
|
-
# nnq carries one String body per message (no multipart).
|
|
9
|
-
#
|
|
10
|
-
# expressions using `$F`/`$_` work the same way as in omq-cli, and
|
|
11
|
-
# unwraps to a bare String at the send boundary.
|
|
8
|
+
# nnq carries one String body per message (no multipart).
|
|
9
|
+
#
|
|
12
10
|
class BaseRunner
|
|
13
11
|
# @return [Config] frozen CLI configuration
|
|
12
|
+
attr_reader :config
|
|
13
|
+
|
|
14
|
+
|
|
14
15
|
# @return [Object] the NNQ socket instance
|
|
15
|
-
attr_reader :
|
|
16
|
+
attr_reader :sock
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
# @param config [Config] frozen CLI configuration
|
|
@@ -20,7 +21,7 @@ module NNQ
|
|
|
20
21
|
def initialize(config, socket_class)
|
|
21
22
|
@config = config
|
|
22
23
|
@klass = socket_class
|
|
23
|
-
@fmt = Formatter.new(config.format
|
|
24
|
+
@fmt = Formatter.new(config.format)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
|
|
@@ -64,7 +65,8 @@ module NNQ
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def create_socket
|
|
67
|
-
SocketSetup.build(@klass, config)
|
|
68
|
+
sock = SocketSetup.build(@klass, config)
|
|
69
|
+
SocketSetup.maybe_wrap_zstd(sock, config.compress)
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
|
|
@@ -144,6 +146,7 @@ module NNQ
|
|
|
144
146
|
# latecomers finish their handshake before we start sending.
|
|
145
147
|
def apply_grace_period
|
|
146
148
|
return unless config.binds.any? || config.connects.size > 1
|
|
149
|
+
|
|
147
150
|
ri = @sock.options.reconnect_interval
|
|
148
151
|
sleep(ri.is_a?(Range) ? ri.begin : ri)
|
|
149
152
|
end
|
|
@@ -162,7 +165,9 @@ module NNQ
|
|
|
162
165
|
|
|
163
166
|
def run_send_logic
|
|
164
167
|
n = config.count
|
|
165
|
-
|
|
168
|
+
|
|
169
|
+
sleep config.delay if config.delay
|
|
170
|
+
|
|
166
171
|
if config.interval
|
|
167
172
|
run_interval_send(n)
|
|
168
173
|
elsif config.data || config.file
|
|
@@ -188,21 +193,30 @@ module NNQ
|
|
|
188
193
|
|
|
189
194
|
def run_interval_send(n)
|
|
190
195
|
i = send_tick
|
|
191
|
-
|
|
196
|
+
|
|
197
|
+
if @send_tick_eof || (n && n > 0 && i >= n)
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
192
201
|
Async::Loop.quantized(interval: config.interval) do
|
|
193
202
|
i += send_tick
|
|
194
|
-
|
|
203
|
+
|
|
204
|
+
if @send_tick_eof || (n && n > 0 && i >= n)
|
|
205
|
+
break
|
|
206
|
+
end
|
|
195
207
|
end
|
|
196
208
|
end
|
|
197
209
|
|
|
198
210
|
|
|
199
211
|
def run_stdin_send(n)
|
|
200
212
|
i = 0
|
|
213
|
+
|
|
201
214
|
loop do
|
|
202
|
-
msg = read_next
|
|
203
|
-
break unless msg
|
|
215
|
+
msg = read_next or break
|
|
204
216
|
msg = eval_send_expr(msg)
|
|
217
|
+
|
|
205
218
|
send_msg(msg) if msg
|
|
219
|
+
|
|
206
220
|
i += 1
|
|
207
221
|
break if n && n > 0 && i >= n
|
|
208
222
|
end
|
|
@@ -211,6 +225,7 @@ module NNQ
|
|
|
211
225
|
|
|
212
226
|
def send_tick
|
|
213
227
|
raw = read_next_or_nil
|
|
228
|
+
|
|
214
229
|
if raw.nil?
|
|
215
230
|
if @send_eval_proc && !@stdin_ready
|
|
216
231
|
# Pure generator mode: no stdin, eval produces output from nothing.
|
|
@@ -218,9 +233,11 @@ module NNQ
|
|
|
218
233
|
send_msg(msg) if msg
|
|
219
234
|
return 1
|
|
220
235
|
end
|
|
236
|
+
|
|
221
237
|
@send_tick_eof = true
|
|
222
238
|
return 0
|
|
223
239
|
end
|
|
240
|
+
|
|
224
241
|
msg = eval_send_expr(raw)
|
|
225
242
|
send_msg(msg) if msg
|
|
226
243
|
1
|
|
@@ -230,14 +247,16 @@ module NNQ
|
|
|
230
247
|
def run_recv_logic
|
|
231
248
|
n = config.count
|
|
232
249
|
i = 0
|
|
250
|
+
|
|
233
251
|
if config.interval
|
|
234
252
|
run_interval_recv(n)
|
|
235
253
|
else
|
|
236
254
|
loop do
|
|
237
|
-
msg = recv_msg
|
|
238
|
-
break if msg.nil?
|
|
255
|
+
msg = recv_msg or break
|
|
239
256
|
msg = eval_recv_expr(msg)
|
|
257
|
+
|
|
240
258
|
output(msg)
|
|
259
|
+
|
|
241
260
|
i += 1
|
|
242
261
|
break if n && n > 0 && i >= n
|
|
243
262
|
end
|
|
@@ -247,21 +266,28 @@ module NNQ
|
|
|
247
266
|
|
|
248
267
|
def run_interval_recv(n)
|
|
249
268
|
i = recv_tick
|
|
269
|
+
|
|
250
270
|
return if i == 0
|
|
251
271
|
return if n && n > 0 && i >= n
|
|
272
|
+
|
|
252
273
|
Async::Loop.quantized(interval: config.interval) do
|
|
253
274
|
i += recv_tick
|
|
254
|
-
|
|
275
|
+
|
|
276
|
+
if @recv_tick_eof || (n && n > 0 && i >= n)
|
|
277
|
+
break
|
|
278
|
+
end
|
|
255
279
|
end
|
|
256
280
|
end
|
|
257
281
|
|
|
258
282
|
|
|
259
283
|
def recv_tick
|
|
260
284
|
msg = recv_msg
|
|
285
|
+
|
|
261
286
|
if msg.nil?
|
|
262
287
|
@recv_tick_eof = true
|
|
263
288
|
return 0
|
|
264
289
|
end
|
|
290
|
+
|
|
265
291
|
msg = eval_recv_expr(msg)
|
|
266
292
|
output(msg)
|
|
267
293
|
1
|
|
@@ -285,22 +311,30 @@ module NNQ
|
|
|
285
311
|
# -- Message I/O -------------------------------------------------
|
|
286
312
|
|
|
287
313
|
|
|
288
|
-
# msg
|
|
314
|
+
# @param msg [String]
|
|
315
|
+
#
|
|
289
316
|
def send_msg(msg)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
317
|
+
body = msg
|
|
318
|
+
|
|
319
|
+
case config.format
|
|
320
|
+
when :marshal
|
|
321
|
+
body = Marshal.dump(msg)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
@sock.send(body)
|
|
294
325
|
transient_ready!
|
|
295
326
|
end
|
|
296
327
|
|
|
297
328
|
|
|
298
|
-
# @return [
|
|
329
|
+
# @return [String, nil] message body, or nil on close.
|
|
299
330
|
def recv_msg
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
msg = @sock.receive or return
|
|
332
|
+
|
|
333
|
+
case config.format
|
|
334
|
+
when :marshal
|
|
335
|
+
msg = Marshal.load msg
|
|
336
|
+
end
|
|
337
|
+
|
|
304
338
|
transient_ready!
|
|
305
339
|
msg
|
|
306
340
|
end
|
|
@@ -329,7 +363,7 @@ module NNQ
|
|
|
329
363
|
@fmt.decode_marshal($stdin)
|
|
330
364
|
when :raw
|
|
331
365
|
data = $stdin.read
|
|
332
|
-
data.nil? || data.empty? ? nil :
|
|
366
|
+
data.nil? || data.empty? ? nil : data
|
|
333
367
|
else
|
|
334
368
|
line = $stdin.gets
|
|
335
369
|
line.nil? ? nil : @fmt.decode(line)
|
|
@@ -360,6 +394,7 @@ module NNQ
|
|
|
360
394
|
|
|
361
395
|
def output(msg)
|
|
362
396
|
return if config.quiet || msg.nil?
|
|
397
|
+
|
|
363
398
|
$stdout.write(@fmt.encode(msg))
|
|
364
399
|
$stdout.flush
|
|
365
400
|
end
|
|
@@ -415,11 +450,13 @@ module NNQ
|
|
|
415
450
|
def set_process_title(endpoints: nil)
|
|
416
451
|
eps = endpoints || config.endpoints
|
|
417
452
|
title = ["nnq", config.type_name]
|
|
418
|
-
title << "-z" if config.compress
|
|
453
|
+
title << (config.compress == :balanced ? "-Z" : "-z") if config.compress
|
|
419
454
|
title << "-P#{config.parallel}" if config.parallel
|
|
455
|
+
|
|
420
456
|
eps.each do |ep|
|
|
421
457
|
title << (ep.respond_to?(:url) ? ep.url : ep.to_s)
|
|
422
458
|
end
|
|
459
|
+
|
|
423
460
|
Process.setproctitle(title.join(" "))
|
|
424
461
|
end
|
|
425
462
|
|
|
@@ -429,6 +466,7 @@ module NNQ
|
|
|
429
466
|
|
|
430
467
|
def log(msg)
|
|
431
468
|
return unless config.verbose >= 1
|
|
469
|
+
|
|
432
470
|
$stderr.write("#{Term.log_prefix(config.verbose)}nnq: #{msg}\n")
|
|
433
471
|
end
|
|
434
472
|
|
|
@@ -438,11 +476,13 @@ module NNQ
|
|
|
438
476
|
# -vvvv: prepend ISO8601 timestamps
|
|
439
477
|
def start_event_monitor
|
|
440
478
|
verbose = config.verbose >= 3
|
|
441
|
-
v
|
|
479
|
+
v = config.verbose
|
|
480
|
+
|
|
442
481
|
@sock.monitor(verbose: verbose) do |event|
|
|
443
482
|
CLI::Term.write_event(event, v)
|
|
444
483
|
end
|
|
445
484
|
end
|
|
485
|
+
|
|
446
486
|
end
|
|
447
487
|
end
|
|
448
488
|
end
|
data/lib/nnq/cli/cli_parser.rb
CHANGED
|
@@ -10,13 +10,13 @@ module NNQ
|
|
|
10
10
|
EXAMPLES = <<~'TEXT'
|
|
11
11
|
-- Request / Reply ------------------------------------------
|
|
12
12
|
|
|
13
|
-
+-----+
|
|
13
|
+
+-----+ "hello" +-----+
|
|
14
14
|
| REQ |------------->| REP |
|
|
15
15
|
| |<-------------| |
|
|
16
|
-
+-----+
|
|
16
|
+
+-----+ "HELLO" +-----+
|
|
17
17
|
|
|
18
18
|
# terminal 1: echo server
|
|
19
|
-
nnq rep --bind tcp://:5555 --recv-eval '
|
|
19
|
+
nnq rep --bind tcp://:5555 --recv-eval 'it.upcase'
|
|
20
20
|
|
|
21
21
|
# terminal 2: send a request
|
|
22
22
|
echo "hello" | nnq req --connect tcp://localhost:5555
|
|
@@ -27,9 +27,9 @@ module NNQ
|
|
|
27
27
|
|
|
28
28
|
-- Publish / Subscribe --------------------------------------
|
|
29
29
|
|
|
30
|
-
+-----+
|
|
30
|
+
+-----+ "weather.nyc 72F" +-----+
|
|
31
31
|
| PUB |--------------------->| SUB | --subscribe "weather."
|
|
32
|
-
+-----+
|
|
32
|
+
+-----+ +-----+
|
|
33
33
|
|
|
34
34
|
# terminal 1: subscriber (all topics by default)
|
|
35
35
|
nnq sub --bind tcp://:5556
|
|
@@ -39,9 +39,9 @@ module NNQ
|
|
|
39
39
|
|
|
40
40
|
-- Periodic Publish -------------------------------------------
|
|
41
41
|
|
|
42
|
-
+-----+
|
|
42
|
+
+-----+ "tick 1" +-----+
|
|
43
43
|
| PUB |--(every 1s)-->| SUB |
|
|
44
|
-
+-----+
|
|
44
|
+
+-----+ +-----+
|
|
45
45
|
|
|
46
46
|
# terminal 1: subscriber
|
|
47
47
|
nnq sub --bind tcp://:5556
|
|
@@ -56,9 +56,9 @@ module NNQ
|
|
|
56
56
|
|
|
57
57
|
-- Pipeline -------------------------------------------------
|
|
58
58
|
|
|
59
|
-
+------+
|
|
59
|
+
+------+ +------+
|
|
60
60
|
| PUSH |----------->| PULL |
|
|
61
|
-
+------+
|
|
61
|
+
+------+ +------+
|
|
62
62
|
|
|
63
63
|
# terminal 1: worker
|
|
64
64
|
nnq pull --bind tcp://:5557
|
|
@@ -72,30 +72,30 @@ module NNQ
|
|
|
72
72
|
|
|
73
73
|
-- Pipe (PULL -> eval -> PUSH) --------------------------------
|
|
74
74
|
|
|
75
|
-
+------+
|
|
75
|
+
+------+ +------+ +------+
|
|
76
76
|
| PUSH |--------->| pipe |--------->| PULL |
|
|
77
|
-
+------+
|
|
77
|
+
+------+ +------+ +------+
|
|
78
78
|
|
|
79
79
|
# terminal 1: producer
|
|
80
80
|
echo -e "hello\nworld" | nnq push --bind ipc://@work
|
|
81
81
|
|
|
82
82
|
# terminal 2: worker -- uppercase each message
|
|
83
|
-
nnq pipe -c ipc://@work -c ipc://@sink -e '
|
|
83
|
+
nnq pipe -c ipc://@work -c ipc://@sink -e 'it.upcase'
|
|
84
84
|
# terminal 3: collector
|
|
85
85
|
nnq pull --bind ipc://@sink
|
|
86
86
|
|
|
87
87
|
# 4 Ractor workers in a single process (-P)
|
|
88
|
-
nnq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer(
|
|
88
|
+
nnq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer(it)).to_s'
|
|
89
89
|
|
|
90
90
|
# exit when producer disconnects (--transient)
|
|
91
|
-
nnq pipe -c ipc://@work -c ipc://@sink --transient -e '
|
|
91
|
+
nnq pipe -c ipc://@work -c ipc://@sink --transient -e 'it.upcase'
|
|
92
92
|
|
|
93
93
|
# fan-in: multiple sources -> one sink
|
|
94
94
|
nnq pipe --in -c ipc://@work1 -c ipc://@work2 \
|
|
95
|
-
--out -c ipc://@sink -e '
|
|
95
|
+
--out -c ipc://@sink -e 'it.upcase'
|
|
96
96
|
|
|
97
97
|
# fan-out: one source -> multiple sinks (round-robin)
|
|
98
|
-
nnq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '
|
|
98
|
+
nnq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e 'it'
|
|
99
99
|
|
|
100
100
|
-- Formats --------------------------------------------------
|
|
101
101
|
|
|
@@ -105,10 +105,6 @@ module NNQ
|
|
|
105
105
|
# quoted -- lossless, round-trippable (uses String#dump escaping)
|
|
106
106
|
nnq pull --bind tcp://:5557 --quoted
|
|
107
107
|
|
|
108
|
-
# JSON Lines -- single-element arrays (nnq is single-body)
|
|
109
|
-
echo '["payload"]' | nnq push --connect tcp://localhost:5557 --jsonl
|
|
110
|
-
nnq pull --bind tcp://:5557 --jsonl
|
|
111
|
-
|
|
112
108
|
# raw -- emit the body verbatim (no framing, no newline)
|
|
113
109
|
nnq pull --bind tcp://:5557 --raw
|
|
114
110
|
|
|
@@ -121,25 +117,25 @@ module NNQ
|
|
|
121
117
|
-- Ruby Eval ------------------------------------------------
|
|
122
118
|
|
|
123
119
|
# filter incoming: only pass messages containing "error"
|
|
124
|
-
nnq pull -b tcp://:5557 --recv-eval '
|
|
120
|
+
nnq pull -b tcp://:5557 --recv-eval 'it.include?("error") ? it : nil'
|
|
125
121
|
|
|
126
122
|
# transform incoming with gems
|
|
127
|
-
nnq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(
|
|
123
|
+
nnq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(it)["temperature"]'
|
|
128
124
|
|
|
129
125
|
# require a local file, use its methods
|
|
130
|
-
nnq rep --bind tcp://:5555 --require ./transform.rb -e '
|
|
126
|
+
nnq rep --bind tcp://:5555 --require ./transform.rb -e 'transform(it)'
|
|
131
127
|
|
|
132
|
-
# next skips, break stops
|
|
133
|
-
nnq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/;
|
|
128
|
+
# next skips, break stops
|
|
129
|
+
nnq pull -b tcp://:5557 -e 'next if /^#/; break if it =~ /quit/; it'
|
|
134
130
|
|
|
135
131
|
# BEGIN/END blocks (like awk) -- accumulate and summarize
|
|
136
|
-
nnq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer(
|
|
132
|
+
nnq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer(it); nil END{puts @sum}'
|
|
137
133
|
|
|
138
|
-
# transform outgoing messages
|
|
139
|
-
echo hello | nnq push -c tcp://localhost:5557 --send-eval '
|
|
134
|
+
# transform outgoing messages (with explicit block variable msg)
|
|
135
|
+
echo hello | nnq push -c tcp://localhost:5557 --send-eval '|msg| msg.upcase'
|
|
140
136
|
|
|
141
137
|
# REQ: transform request and reply independently
|
|
142
|
-
echo hello | nnq req -c tcp://localhost:5555 -E '
|
|
138
|
+
echo hello | nnq req -c tcp://localhost:5555 -E 'it.upcase' -e 'it'
|
|
143
139
|
|
|
144
140
|
-- Script Handlers (-r) ------------------------------------
|
|
145
141
|
|
|
@@ -150,7 +146,7 @@ module NNQ
|
|
|
150
146
|
nnq pull --bind tcp://:5557 -r./handler.rb
|
|
151
147
|
|
|
152
148
|
# combine script handlers with inline eval
|
|
153
|
-
nnq req -c tcp://localhost:5555 -r./handler.rb -E '
|
|
149
|
+
nnq req -c tcp://localhost:5555 -r./handler.rb -E 'it.upcase'
|
|
154
150
|
|
|
155
151
|
# NNQ.outgoing { |msg| ... } -- registered outgoing transform
|
|
156
152
|
# NNQ.incoming { |msg| ... } -- registered incoming transform
|
|
@@ -178,9 +174,9 @@ module NNQ
|
|
|
178
174
|
send_hwm: nil,
|
|
179
175
|
sndbuf: nil,
|
|
180
176
|
rcvbuf: nil,
|
|
181
|
-
compress:
|
|
182
|
-
compress_in:
|
|
183
|
-
compress_out:
|
|
177
|
+
compress: nil,
|
|
178
|
+
compress_in: nil,
|
|
179
|
+
compress_out: nil,
|
|
184
180
|
send_expr: nil,
|
|
185
181
|
recv_expr: nil,
|
|
186
182
|
parallel: nil,
|
|
@@ -268,7 +264,6 @@ module NNQ
|
|
|
268
264
|
o.on("-A", "--ascii", "Safe ASCII, non-printable as dots (default)") { opts[:format] = :ascii }
|
|
269
265
|
o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
|
|
270
266
|
o.on( "--raw", "Raw binary body, no framing, no newline") { opts[:format] = :raw }
|
|
271
|
-
o.on("-J", "--jsonl", "JSON Lines (single-element array per line)") { opts[:format] = :jsonl }
|
|
272
267
|
o.on( "--msgpack", "MessagePack (binary stream)") { require "msgpack"; opts[:format] = :msgpack }
|
|
273
268
|
o.on("-M", "--marshal", "Ruby Marshal stream (binary)") { opts[:format] = :marshal }
|
|
274
269
|
|
|
@@ -295,27 +290,37 @@ module NNQ
|
|
|
295
290
|
o.on("--rcvbuf N", "SO_RCVBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:rcvbuf] = parse_byte_size(v) }
|
|
296
291
|
|
|
297
292
|
o.separator "\nCompression:"
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
293
|
+
load_zstd = -> { require "nnq/zstd" }
|
|
294
|
+
set_compress = lambda do |sym|
|
|
295
|
+
load_zstd.call
|
|
296
|
+
target = case pipe_side
|
|
297
|
+
when :in then :compress_in
|
|
298
|
+
when :out then :compress_out
|
|
299
|
+
else :compress
|
|
300
|
+
end
|
|
301
|
+
if opts[target] && opts[target] != sym
|
|
302
|
+
abort "nnq: -z and -Z are mutually exclusive"
|
|
307
303
|
end
|
|
304
|
+
opts[target] = sym
|
|
305
|
+
end
|
|
306
|
+
o.on("-z", "--compress", "Zstd compression (fast, level -3; modal with --in/--out)") do
|
|
307
|
+
set_compress.call(:fast)
|
|
308
|
+
end
|
|
309
|
+
o.on("-Z", "--compress-high", "Zstd compression (balanced, level 3; modal with --in/--out)") do
|
|
310
|
+
set_compress.call(:balanced)
|
|
308
311
|
end
|
|
309
312
|
|
|
310
313
|
o.separator "\nProcessing (-e = incoming, -E = outgoing):"
|
|
311
|
-
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message (
|
|
312
|
-
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message (
|
|
314
|
+
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message (it = msg)") { |v| opts[:recv_expr] = v }
|
|
315
|
+
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message (it = msg)") { |v| opts[:send_expr] = v }
|
|
313
316
|
o.on("-r", "--require LIB", "Require lib/file in Async context; use '-' for stdin. Scripts can register NNQ.outgoing/incoming") { |v|
|
|
314
317
|
require "nnq" unless defined?(NNQ::VERSION)
|
|
315
318
|
opts[:scripts] << (v == "-" ? :stdin : (v.start_with?("./", "../") ? File.expand_path(v) : v))
|
|
316
319
|
}
|
|
317
|
-
o.on("-P", "--parallel N", Integer, "Parallel Ractor workers (
|
|
318
|
-
|
|
320
|
+
o.on("-P", "--parallel N", Integer, "Parallel Ractor workers, 1..16 (0 = nproc, capped at 16)") { |v|
|
|
321
|
+
require "etc"
|
|
322
|
+
resolved = v.zero? ? Etc.nprocessors : v
|
|
323
|
+
opts[:parallel] = [resolved, 16].min
|
|
319
324
|
}
|
|
320
325
|
|
|
321
326
|
o.separator "\nOther:"
|
|
@@ -8,8 +8,10 @@ module NNQ
|
|
|
8
8
|
#
|
|
9
9
|
# One instance per direction (send or recv).
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# The expression sees the message body via the default block
|
|
12
|
+
# variable `it`, or callers can declare an explicit parameter with
|
|
13
|
+
# block-literal syntax: `-e '|msg| msg.upcase'` compiles to
|
|
14
|
+
# `proc { |msg| msg.upcase }`.
|
|
13
15
|
#
|
|
14
16
|
class ExpressionEvaluator
|
|
15
17
|
attr_reader :begin_proc, :end_proc, :eval_proc
|
|
@@ -28,34 +30,30 @@ module NNQ
|
|
|
28
30
|
|
|
29
31
|
if src
|
|
30
32
|
expr, begin_body, end_body = extract_blocks(src)
|
|
31
|
-
@begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
32
|
-
@end_proc = eval("proc { #{end_body} }") if end_body
|
|
33
|
+
@begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
34
|
+
@end_proc = eval("proc { #{end_body} }") if end_body
|
|
35
|
+
|
|
33
36
|
if expr && !expr.strip.empty?
|
|
34
|
-
@eval_proc = eval("proc {
|
|
37
|
+
@eval_proc = eval("proc { #{expr} }")
|
|
35
38
|
end
|
|
36
39
|
elsif fallback_proc
|
|
37
|
-
@eval_proc = proc { |msg|
|
|
38
|
-
body = msg&.first
|
|
39
|
-
$_ = body
|
|
40
|
-
fallback_proc.call(body)
|
|
41
|
-
}
|
|
40
|
+
@eval_proc = proc { |msg| fallback_proc.call(msg) }
|
|
42
41
|
end
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
# Runs the eval proc against +msg+ using +context+ as self.
|
|
47
|
-
# Returns the normalised result
|
|
46
|
+
# Returns the normalised result (as String), nil (filter/skip), or SENT.
|
|
47
|
+
#
|
|
48
48
|
def call(msg, context)
|
|
49
49
|
return msg unless @eval_proc
|
|
50
50
|
|
|
51
|
-
$F = msg
|
|
52
51
|
result = context.instance_exec(msg, &@eval_proc)
|
|
53
52
|
return nil if result.nil?
|
|
54
53
|
return SENT if result.equal?(context)
|
|
55
|
-
return
|
|
54
|
+
return result if @format == :marshal
|
|
56
55
|
|
|
57
|
-
result
|
|
58
|
-
result.map!(&:to_s)
|
|
56
|
+
result.to_s
|
|
59
57
|
rescue => e
|
|
60
58
|
$stderr.puts "nnq: eval error: #{e.message} (#{e.class})"
|
|
61
59
|
exit 3
|
|
@@ -66,8 +64,7 @@ module NNQ
|
|
|
66
64
|
# Used inside Ractor worker blocks.
|
|
67
65
|
def self.normalize_result(result)
|
|
68
66
|
return nil if result.nil?
|
|
69
|
-
result
|
|
70
|
-
result.map!(&:to_s)
|
|
67
|
+
result.to_s
|
|
71
68
|
end
|
|
72
69
|
|
|
73
70
|
|
|
@@ -77,52 +74,34 @@ module NNQ
|
|
|
77
74
|
def self.compile_inside_ractor(src)
|
|
78
75
|
return [nil, nil, nil] unless src
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return [expr, nil] unless s
|
|
83
|
-
ci = expr.index("{", s)
|
|
84
|
-
depth = 1
|
|
85
|
-
j = ci + 1
|
|
86
|
-
while j < expr.length && depth > 0
|
|
87
|
-
depth += 1 if expr[j] == "{"
|
|
88
|
-
depth -= 1 if expr[j] == "}"
|
|
89
|
-
j += 1
|
|
90
|
-
end
|
|
91
|
-
[expr[0...s] + expr[j..], expr[(ci + 1)..(j - 2)]]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
expr, begin_body = extract.(src, "BEGIN")
|
|
95
|
-
expr, end_body = extract.(expr, "END")
|
|
77
|
+
expr, begin_body = extract_block(src, "BEGIN")
|
|
78
|
+
expr, end_body = extract_block(expr, "END")
|
|
96
79
|
|
|
97
|
-
begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
98
|
-
end_proc = eval("proc { #{end_body} }") if end_body
|
|
80
|
+
begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
81
|
+
end_proc = eval("proc { #{end_body} }") if end_body
|
|
99
82
|
eval_proc = nil
|
|
83
|
+
|
|
100
84
|
if expr && !expr.strip.empty?
|
|
101
|
-
|
|
102
|
-
eval_proc = eval("proc { |__F| $_ = __F&.first; #{ractor_expr} }") # rubocop:disable Security/Eval
|
|
85
|
+
eval_proc = eval("proc { #{expr} }")
|
|
103
86
|
end
|
|
104
87
|
|
|
105
88
|
[begin_proc, end_proc, eval_proc]
|
|
106
89
|
end
|
|
107
90
|
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def extract_block(expr, keyword)
|
|
120
|
-
start = expr.index(/#{keyword}\s*\{/)
|
|
121
|
-
return [expr, nil] unless start
|
|
92
|
+
# Strips a +BEGIN {...}+ or +END {...}+ block from +expr+ and
|
|
93
|
+
# returns +[trimmed_expr, block_body_or_nil]+. Brace-matched scan,
|
|
94
|
+
# so nested `{}` inside the block body are handled. Shared by
|
|
95
|
+
# instance and Ractor compile paths, so must be a class method
|
|
96
|
+
# (Ractors cannot call back into instance state).
|
|
97
|
+
#
|
|
98
|
+
def self.extract_block(expr, keyword)
|
|
99
|
+
start = expr.index(/#{keyword}\s*\{/) or return [expr, nil]
|
|
122
100
|
|
|
123
101
|
i = expr.index("{", start)
|
|
124
102
|
depth = 1
|
|
125
103
|
j = i + 1
|
|
104
|
+
|
|
126
105
|
while j < expr.length && depth > 0
|
|
127
106
|
case expr[j]
|
|
128
107
|
when "{"
|
|
@@ -130,6 +109,7 @@ module NNQ
|
|
|
130
109
|
when "}"
|
|
131
110
|
depth -= 1
|
|
132
111
|
end
|
|
112
|
+
|
|
133
113
|
j += 1
|
|
134
114
|
end
|
|
135
115
|
|
|
@@ -137,6 +117,17 @@ module NNQ
|
|
|
137
117
|
trimmed = expr[0...start] + expr[j..]
|
|
138
118
|
[trimmed, body]
|
|
139
119
|
end
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def extract_blocks(expr)
|
|
126
|
+
expr, begin_body = self.class.extract_block(expr, "BEGIN")
|
|
127
|
+
expr, end_body = self.class.extract_block(expr, "END")
|
|
128
|
+
[expr, begin_body, end_body]
|
|
129
|
+
end
|
|
130
|
+
|
|
140
131
|
end
|
|
141
132
|
end
|
|
142
133
|
end
|
data/lib/nnq/cli/formatter.rb
CHANGED
|
@@ -2,66 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
module NNQ
|
|
4
4
|
module CLI
|
|
5
|
-
# Raised when LZ4 decompression fails.
|
|
6
|
-
class DecompressError < RuntimeError; end
|
|
7
|
-
|
|
8
5
|
# Handles encoding/decoding a single-body message in the configured
|
|
9
|
-
# format
|
|
6
|
+
# format. Compression is handled by the NNQ::Zstd decorator around
|
|
7
|
+
# the socket, not by the formatter.
|
|
10
8
|
#
|
|
11
9
|
# Unlike omq-cli's Formatter, nnq messages are not multipart — one
|
|
12
|
-
# `String` body per message. The API
|
|
13
|
-
#
|
|
14
|
-
# way.
|
|
10
|
+
# `String` body per message. The API takes and returns a plain
|
|
11
|
+
# `String`.
|
|
15
12
|
class Formatter
|
|
16
|
-
# @param format [Symbol] wire format (:ascii, :quoted, :raw, :
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@format = format
|
|
20
|
-
@compress = compress
|
|
13
|
+
# @param format [Symbol] wire format (:ascii, :quoted, :raw, :msgpack, :marshal)
|
|
14
|
+
def initialize(format)
|
|
15
|
+
@format = format
|
|
21
16
|
end
|
|
22
17
|
|
|
23
18
|
|
|
24
19
|
# Encodes a message body into a printable string for output.
|
|
25
20
|
#
|
|
26
|
-
# @param msg [
|
|
21
|
+
# @param msg [String] message body
|
|
27
22
|
# @return [String] formatted output line
|
|
23
|
+
#
|
|
28
24
|
def encode(msg)
|
|
29
|
-
body = msg.first.to_s
|
|
30
25
|
case @format
|
|
31
26
|
when :ascii
|
|
32
|
-
|
|
27
|
+
msg.b.gsub(/[^[:print:]\t]/, ".") << "\n"
|
|
33
28
|
when :quoted
|
|
34
|
-
|
|
29
|
+
msg.b.dump[1..-2] << "\n"
|
|
35
30
|
when :raw
|
|
36
|
-
|
|
37
|
-
when :jsonl
|
|
38
|
-
JSON.generate([body]) + "\n"
|
|
31
|
+
msg # FIXME: are these really the wire bytes?
|
|
39
32
|
when :msgpack
|
|
40
|
-
MessagePack.pack(
|
|
33
|
+
MessagePack.pack(msg)
|
|
41
34
|
when :marshal
|
|
42
|
-
|
|
35
|
+
msg.inspect << "\n"
|
|
43
36
|
end
|
|
44
37
|
end
|
|
45
38
|
|
|
46
39
|
|
|
47
|
-
# Decodes a formatted input line into a
|
|
40
|
+
# Decodes a formatted input line into a message body.
|
|
48
41
|
#
|
|
49
42
|
# @param line [String] input line (newline-terminated)
|
|
50
|
-
# @return [
|
|
43
|
+
# @return [String] message
|
|
44
|
+
#
|
|
51
45
|
def decode(line)
|
|
52
46
|
case @format
|
|
53
47
|
when :ascii, :marshal
|
|
54
|
-
|
|
48
|
+
line.chomp
|
|
55
49
|
when :quoted
|
|
56
|
-
|
|
50
|
+
"\"#{line.chomp}\"".undump
|
|
57
51
|
when :raw
|
|
58
|
-
|
|
59
|
-
when :jsonl
|
|
60
|
-
arr = JSON.parse(line.chomp)
|
|
61
|
-
unless arr.is_a?(Array) && arr.all? { |e| e.is_a?(String) }
|
|
62
|
-
abort "JSON Lines input must be an array of strings"
|
|
63
|
-
end
|
|
64
|
-
arr.first(1)
|
|
52
|
+
line
|
|
65
53
|
end
|
|
66
54
|
end
|
|
67
55
|
|
|
@@ -89,33 +77,12 @@ module NNQ
|
|
|
89
77
|
end
|
|
90
78
|
|
|
91
79
|
|
|
92
|
-
# Compresses the body with LZ4 if compression is enabled.
|
|
93
|
-
#
|
|
94
|
-
# @param msg [Array<String>] single-element array
|
|
95
|
-
# @return [Array<String>] optionally compressed
|
|
96
|
-
def compress(msg)
|
|
97
|
-
@compress ? msg.map { |p| RLZ4.compress(p) if p } : msg
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Decompresses the body with LZ4 if compression is enabled.
|
|
102
|
-
# nil/empty bodies pass through.
|
|
103
|
-
#
|
|
104
|
-
# @param msg [Array<String>] possibly compressed single-element array
|
|
105
|
-
# @return [Array<String>] decompressed
|
|
106
|
-
def decompress(msg)
|
|
107
|
-
@compress ? msg.map { |p| p && !p.empty? ? RLZ4.decompress(p) : p } : msg
|
|
108
|
-
rescue RLZ4::DecompressError
|
|
109
|
-
raise DecompressError, "decompression failed (did the sender use --compress?)"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
|
|
113
80
|
# Formats a message body for human-readable preview (logging).
|
|
114
81
|
#
|
|
115
|
-
# @param
|
|
82
|
+
# @param body [String] message body
|
|
116
83
|
# @return [String] truncated preview
|
|
117
|
-
def self.preview(
|
|
118
|
-
body =
|
|
84
|
+
def self.preview(body)
|
|
85
|
+
body = body.to_s
|
|
119
86
|
"(#{body.bytesize}B) #{preview_body(body)}"
|
|
120
87
|
end
|
|
121
88
|
|
data/lib/nnq/cli/pipe.rb
CHANGED
|
@@ -12,8 +12,8 @@ module NNQ
|
|
|
12
12
|
# @param config [Config] frozen CLI configuration
|
|
13
13
|
def initialize(config)
|
|
14
14
|
@config = config
|
|
15
|
-
@fmt_in = Formatter.new(config.format
|
|
16
|
-
@fmt_out = Formatter.new(config.format
|
|
15
|
+
@fmt_in = Formatter.new(config.format)
|
|
16
|
+
@fmt_out = Formatter.new(config.format)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
|
|
@@ -68,6 +68,8 @@ module NNQ
|
|
|
68
68
|
push = SocketSetup.build(NNQ::PUSH0, config)
|
|
69
69
|
SocketSetup.attach_endpoints(pull, in_eps, verbose: config.verbose)
|
|
70
70
|
SocketSetup.attach_endpoints(push, out_eps, verbose: config.verbose)
|
|
71
|
+
pull = SocketSetup.maybe_wrap_zstd(pull, config.compress_in || config.compress)
|
|
72
|
+
push = SocketSetup.maybe_wrap_zstd(push, config.compress_out || config.compress)
|
|
71
73
|
[pull, push]
|
|
72
74
|
end
|
|
73
75
|
|
|
@@ -103,13 +105,9 @@ module NNQ
|
|
|
103
105
|
loop do
|
|
104
106
|
body = @pull.receive
|
|
105
107
|
break if body.nil?
|
|
106
|
-
msg =
|
|
107
|
-
msg = eval_recv_expr(msg)
|
|
108
|
+
msg = eval_recv_expr(body)
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
out = @fmt_out.compress(msg)
|
|
111
|
-
@push.send(out.first)
|
|
112
|
-
end
|
|
110
|
+
@push.send(msg) if msg
|
|
113
111
|
|
|
114
112
|
# Yield after send so send-pump fibers can drain the queue
|
|
115
113
|
# before the next message is enqueued. Without this, one pump
|
|
@@ -163,7 +161,9 @@ module NNQ
|
|
|
163
161
|
def set_pipe_process_title
|
|
164
162
|
in_eps, out_eps = resolve_endpoints
|
|
165
163
|
title = ["nnq pipe"]
|
|
166
|
-
|
|
164
|
+
if (m = config.compress || config.compress_in || config.compress_out)
|
|
165
|
+
title << (m == :balanced ? "-Z" : "-z")
|
|
166
|
+
end
|
|
167
167
|
title << "-P#{config.parallel}" if config.parallel
|
|
168
168
|
title.concat(in_eps.map(&:url))
|
|
169
169
|
title << "->"
|
data/lib/nnq/cli/pipe_worker.rb
CHANGED
|
@@ -24,8 +24,8 @@ module NNQ
|
|
|
24
24
|
compile_expr
|
|
25
25
|
run_message_loop
|
|
26
26
|
run_end_block
|
|
27
|
-
rescue NNQ::
|
|
28
|
-
@error_port&.send(e.message)
|
|
27
|
+
rescue NNQ::Zstd::ProtocolError => e
|
|
28
|
+
@error_port&.send("zstd protocol error: #{e.message}")
|
|
29
29
|
ensure
|
|
30
30
|
@pull&.close
|
|
31
31
|
@push&.close
|
|
@@ -41,6 +41,8 @@ module NNQ
|
|
|
41
41
|
@push = NNQ::CLI::SocketSetup.build(NNQ::PUSH0, @config)
|
|
42
42
|
NNQ::CLI::SocketSetup.attach_endpoints(@pull, @in_eps, verbose: 0)
|
|
43
43
|
NNQ::CLI::SocketSetup.attach_endpoints(@push, @out_eps, verbose: 0)
|
|
44
|
+
@pull = NNQ::CLI::SocketSetup.maybe_wrap_zstd(@pull, @config.compress_in || @config.compress)
|
|
45
|
+
@push = NNQ::CLI::SocketSetup.maybe_wrap_zstd(@push, @config.compress_out || @config.compress)
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
|
|
@@ -63,9 +65,9 @@ module NNQ
|
|
|
63
65
|
def format_event(event)
|
|
64
66
|
case event.type
|
|
65
67
|
when :message_sent
|
|
66
|
-
"nnq: >> #{NNQ::CLI::Formatter.preview(
|
|
68
|
+
"nnq: >> #{NNQ::CLI::Formatter.preview(event.detail[:body])}"
|
|
67
69
|
when :message_received
|
|
68
|
-
"nnq: << #{NNQ::CLI::Formatter.preview(
|
|
70
|
+
"nnq: << #{NNQ::CLI::Formatter.preview(event.detail[:body])}"
|
|
69
71
|
else
|
|
70
72
|
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
71
73
|
detail = event.detail ? " #{event.detail}" : ""
|
|
@@ -86,8 +88,6 @@ module NNQ
|
|
|
86
88
|
def compile_expr
|
|
87
89
|
@begin_proc, @end_proc, @eval_proc =
|
|
88
90
|
NNQ::CLI::ExpressionEvaluator.compile_inside_ractor(@config.recv_expr)
|
|
89
|
-
@fmt_in = NNQ::CLI::Formatter.new(@config.format, compress: @config.compress_in || @config.compress)
|
|
90
|
-
@fmt_out = NNQ::CLI::Formatter.new(@config.format, compress: @config.compress_out || @config.compress)
|
|
91
91
|
@ctx = Object.new
|
|
92
92
|
@ctx.instance_exec(&@begin_proc) if @begin_proc
|
|
93
93
|
end
|
|
@@ -100,12 +100,9 @@ module NNQ
|
|
|
100
100
|
body = @pull.receive
|
|
101
101
|
break if body.nil?
|
|
102
102
|
msg = NNQ::CLI::ExpressionEvaluator.normalize_result(
|
|
103
|
-
@ctx.instance_exec(
|
|
103
|
+
@ctx.instance_exec(body, &@eval_proc)
|
|
104
104
|
)
|
|
105
|
-
|
|
106
|
-
out = @fmt_out.compress(msg)
|
|
107
|
-
@push.send(out.first)
|
|
108
|
-
end
|
|
105
|
+
@push.send(msg) if msg
|
|
109
106
|
n -= 1 if n && n > 0
|
|
110
107
|
break if n == 0
|
|
111
108
|
end
|
|
@@ -113,8 +110,7 @@ module NNQ
|
|
|
113
110
|
loop do
|
|
114
111
|
body = @pull.receive
|
|
115
112
|
break if body.nil?
|
|
116
|
-
|
|
117
|
-
@push.send(out.first)
|
|
113
|
+
@push.send(body)
|
|
118
114
|
n -= 1 if n && n > 0
|
|
119
115
|
break if n == 0
|
|
120
116
|
end
|
|
@@ -129,9 +125,7 @@ module NNQ
|
|
|
129
125
|
out = NNQ::CLI::ExpressionEvaluator.normalize_result(
|
|
130
126
|
@ctx.instance_exec(&@end_proc)
|
|
131
127
|
)
|
|
132
|
-
|
|
133
|
-
@push.send(@fmt_out.compress(out).first)
|
|
134
|
-
end
|
|
128
|
+
@push.send(out) if out
|
|
135
129
|
end
|
|
136
130
|
end
|
|
137
131
|
end
|
data/lib/nnq/cli/req_rep.rb
CHANGED
|
@@ -13,33 +13,39 @@ module NNQ
|
|
|
13
13
|
def run_loop(task)
|
|
14
14
|
n = config.count
|
|
15
15
|
i = 0
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
sleep config.delay if config.delay
|
|
18
|
+
generator = @send_eval_proc && !config.data && !config.file && !stdin_ready?
|
|
19
|
+
|
|
17
20
|
loop do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
if generator
|
|
22
|
+
msg = eval_send_expr(nil)
|
|
23
|
+
else
|
|
24
|
+
msg = read_next
|
|
25
|
+
break unless msg
|
|
26
|
+
msg = eval_send_expr(msg)
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
next unless msg
|
|
30
|
+
|
|
22
31
|
reply = request_and_receive(msg)
|
|
23
32
|
break if reply.nil?
|
|
24
33
|
output(eval_recv_expr(reply))
|
|
25
34
|
i += 1
|
|
26
35
|
break if n && n > 0 && i >= n
|
|
27
|
-
break if !config.interval && (config.data || config.file)
|
|
36
|
+
break if !config.interval && (generator || config.data || config.file) && !(n && n > 0)
|
|
28
37
|
wait_for_interval if config.interval
|
|
29
38
|
end
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
|
|
33
42
|
def request_and_receive(msg)
|
|
34
|
-
return nil if msg.empty?
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
reply_body = @sock.send_request(msg.first)
|
|
43
|
+
return nil if msg.nil? || msg.empty?
|
|
44
|
+
body = config.format == :marshal ? Marshal.dump(msg) : msg
|
|
45
|
+
reply_body = @sock.send_request(body)
|
|
38
46
|
transient_ready!
|
|
39
47
|
return nil if reply_body.nil?
|
|
40
|
-
|
|
41
|
-
reply = [Marshal.load(reply.first)] if config.format == :marshal
|
|
42
|
-
reply
|
|
48
|
+
config.format == :marshal ? Marshal.load(reply_body) : reply_body
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
|
|
@@ -76,7 +82,7 @@ module NNQ
|
|
|
76
82
|
reply = eval_recv_expr(msg)
|
|
77
83
|
unless reply.equal?(SENT)
|
|
78
84
|
output(reply)
|
|
79
|
-
send_reply(reply ||
|
|
85
|
+
send_reply(reply || "")
|
|
80
86
|
end
|
|
81
87
|
elsif config.echo
|
|
82
88
|
output(msg)
|
|
@@ -94,10 +100,9 @@ module NNQ
|
|
|
94
100
|
|
|
95
101
|
|
|
96
102
|
def send_reply(msg)
|
|
97
|
-
return if msg.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@sock.send_reply(msg.first)
|
|
103
|
+
return if msg.nil?
|
|
104
|
+
body = config.format == :marshal ? Marshal.dump(msg) : msg
|
|
105
|
+
@sock.send_reply(body)
|
|
101
106
|
transient_ready!
|
|
102
107
|
end
|
|
103
108
|
end
|
data/lib/nnq/cli/socket_setup.rb
CHANGED
|
@@ -10,12 +10,14 @@ module NNQ
|
|
|
10
10
|
# (one batch exactly fills a full queue).
|
|
11
11
|
DEFAULT_HWM = 64
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
# Default max inbound message size (1 MiB) so a misconfigured or
|
|
14
15
|
# malicious peer can't force arbitrary memory allocation on a
|
|
15
16
|
# terminal user. Users can raise it with --recv-maxsz N, or
|
|
16
17
|
# disable it entirely with --recv-maxsz 0.
|
|
17
18
|
DEFAULT_RECV_MAXSZ = 1 << 20
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
# Apply post-construction socket options from +config+ to +sock+.
|
|
20
22
|
# send_hwm and linger are construction-time kwargs (see {.build});
|
|
21
23
|
# the rest of the options are set here and read later by the
|
|
@@ -24,12 +26,15 @@ module NNQ
|
|
|
24
26
|
sock.options.read_timeout = config.timeout if config.timeout
|
|
25
27
|
sock.options.write_timeout = config.timeout if config.timeout
|
|
26
28
|
sock.options.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
|
|
30
|
+
case config.recv_maxsz
|
|
31
|
+
when nil
|
|
32
|
+
sock.options.max_message_size = DEFAULT_RECV_MAXSZ
|
|
33
|
+
when 0
|
|
34
|
+
sock.options.max_message_size = nil
|
|
35
|
+
else
|
|
36
|
+
config.recv_maxsz
|
|
37
|
+
end
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
|
|
@@ -38,10 +43,10 @@ module NNQ
|
|
|
38
43
|
# (send_hwm is captured during routing init and can't be changed
|
|
39
44
|
# after the fact), so we pass them there.
|
|
40
45
|
def self.build(klass, config)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
linger = config.linger
|
|
47
|
+
send_hwm = config.send_hwm || DEFAULT_HWM
|
|
48
|
+
sock = klass.new(linger:, send_hwm:)
|
|
49
|
+
|
|
45
50
|
apply_options(sock, config)
|
|
46
51
|
sock
|
|
47
52
|
end
|
|
@@ -54,6 +59,7 @@ module NNQ
|
|
|
54
59
|
sock.bind(url)
|
|
55
60
|
CLI::Term.write_attach(:bind, sock.last_endpoint, verbose) if verbose >= 1
|
|
56
61
|
end
|
|
62
|
+
|
|
57
63
|
config.connects.each do |url|
|
|
58
64
|
sock.connect(url)
|
|
59
65
|
CLI::Term.write_attach(:connect, url, verbose) if verbose >= 1
|
|
@@ -77,6 +83,16 @@ module NNQ
|
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
|
|
86
|
+
# Wrap +sock+ with NNQ::Zstd if +mode+ is :fast or :balanced.
|
|
87
|
+
# Returns the wrapper (decorator) or +sock+ unchanged.
|
|
88
|
+
def self.maybe_wrap_zstd(sock, mode)
|
|
89
|
+
return sock unless mode
|
|
90
|
+
require "nnq/zstd"
|
|
91
|
+
level = mode == :balanced ? 3 : -3
|
|
92
|
+
NNQ::Zstd.wrap(sock, level: level)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
80
96
|
# Subscribe to prefixes on a SUB socket.
|
|
81
97
|
#
|
|
82
98
|
# Unlike ZeroMQ, nng's sub0 starts with an empty subscription set,
|
|
@@ -88,6 +104,7 @@ module NNQ
|
|
|
88
104
|
prefixes = config.subscribes.empty? ? [""] : config.subscribes
|
|
89
105
|
prefixes.each { |p| sock.subscribe(p) }
|
|
90
106
|
end
|
|
107
|
+
|
|
91
108
|
end
|
|
92
109
|
end
|
|
93
110
|
end
|
|
@@ -29,10 +29,9 @@ module NNQ
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def survey_and_collect(msg)
|
|
32
|
-
return if msg.empty?
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@sock.send_survey(msg.first)
|
|
32
|
+
return if msg.nil? || msg.empty?
|
|
33
|
+
body = config.format == :marshal ? Marshal.dump(msg) : msg
|
|
34
|
+
@sock.send_survey(body)
|
|
36
35
|
transient_ready!
|
|
37
36
|
collect_replies
|
|
38
37
|
end
|
|
@@ -42,8 +41,7 @@ module NNQ
|
|
|
42
41
|
loop do
|
|
43
42
|
body = @sock.receive
|
|
44
43
|
break if body.nil?
|
|
45
|
-
reply =
|
|
46
|
-
reply = [Marshal.load(reply.first)] if config.format == :marshal
|
|
44
|
+
reply = config.format == :marshal ? Marshal.load(body) : body
|
|
47
45
|
output(eval_recv_expr(reply))
|
|
48
46
|
rescue NNQ::TimedOut
|
|
49
47
|
break
|
|
@@ -83,7 +81,7 @@ module NNQ
|
|
|
83
81
|
reply = eval_recv_expr(msg)
|
|
84
82
|
unless reply.equal?(SENT)
|
|
85
83
|
output(reply)
|
|
86
|
-
send_reply(reply ||
|
|
84
|
+
send_reply(reply || "")
|
|
87
85
|
end
|
|
88
86
|
elsif config.echo
|
|
89
87
|
output(msg)
|
|
@@ -101,10 +99,9 @@ module NNQ
|
|
|
101
99
|
|
|
102
100
|
|
|
103
101
|
def send_reply(msg)
|
|
104
|
-
return if msg.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@sock.send_reply(msg.first)
|
|
102
|
+
return if msg.nil?
|
|
103
|
+
body = config.format == :marshal ? Marshal.dump(msg) : msg
|
|
104
|
+
@sock.send_reply(body)
|
|
108
105
|
transient_ready!
|
|
109
106
|
end
|
|
110
107
|
end
|
data/lib/nnq/cli/term.rb
CHANGED
|
@@ -36,9 +36,9 @@ module NNQ
|
|
|
36
36
|
prefix = log_prefix(verbose)
|
|
37
37
|
case event.type
|
|
38
38
|
when :message_sent
|
|
39
|
-
"#{prefix}nnq: >> #{Formatter.preview(
|
|
39
|
+
"#{prefix}nnq: >> #{Formatter.preview(event.detail[:body])}"
|
|
40
40
|
when :message_received
|
|
41
|
-
"#{prefix}nnq: << #{Formatter.preview(
|
|
41
|
+
"#{prefix}nnq: << #{Formatter.preview(event.detail[:body])}"
|
|
42
42
|
else
|
|
43
43
|
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
44
44
|
detail = event.detail ? " #{event.detail}" : ""
|
data/lib/nnq/cli/version.rb
CHANGED
data/lib/nnq/cli.rb
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
|
+
|
|
5
|
+
# Forward-declare NNQ::Zstd::ProtocolError so the rescue clause below
|
|
6
|
+
# resolves even when compression wasn't requested and `nnq/zstd` was
|
|
7
|
+
# never required. The real class is defined in nnq-zstd; re-opening it
|
|
8
|
+
# here with the same StandardError superclass is benign.
|
|
9
|
+
module NNQ
|
|
10
|
+
module Zstd
|
|
11
|
+
class ProtocolError < StandardError; end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
4
15
|
require_relative "cli/version"
|
|
5
16
|
require_relative "cli/config"
|
|
6
17
|
require_relative "cli/cli_parser"
|
|
@@ -153,8 +164,8 @@ module NNQ
|
|
|
153
164
|
runner_class.new(config)
|
|
154
165
|
end
|
|
155
166
|
runner.call(task)
|
|
156
|
-
rescue
|
|
157
|
-
$stderr.puts "nnq: #{e.message}"
|
|
167
|
+
rescue NNQ::Zstd::ProtocolError => e
|
|
168
|
+
$stderr.puts "nnq: zstd protocol error: #{e.message}"
|
|
158
169
|
exit 1
|
|
159
170
|
rescue IO::TimeoutError, Async::TimeoutError
|
|
160
171
|
$stderr.puts "nnq: timeout" unless config.quiet
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nnq-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -24,37 +24,37 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.5'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: nnq-zstd
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
32
|
+
version: '0.1'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
39
|
+
version: '0.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
41
|
+
name: msgpack
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
|
-
- - "
|
|
44
|
+
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0
|
|
46
|
+
version: '0'
|
|
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'
|
|
54
54
|
description: Command-line tool for sending and receiving nanomsg SP messages on any
|
|
55
55
|
NNQ socket type (REQ/REP, PUB/SUB, PUSH/PULL, PAIR). Supports Ruby eval (-e/-E),
|
|
56
56
|
script handlers (-r), the virtual `pipe` socket with optional Ractor parallelism,
|
|
57
|
-
multiple formats (ASCII, JSON Lines, msgpack, Marshal), and
|
|
57
|
+
multiple formats (ASCII, JSON Lines, msgpack, Marshal), and Zstd compression. Like
|
|
58
58
|
nngcat from libnng, but with Ruby superpowers.
|
|
59
59
|
email:
|
|
60
60
|
- paddor@gmail.com
|