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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2aac6e193d793225be560b51a32d4f3e713afbab8b25652be6cd76750f789e3e
4
- data.tar.gz: a31568088dbc3ba5ad3df07f3e1f0f98543c1ad0c631718ae71f76d02b602c0a
3
+ metadata.gz: cfc34e5b7774c4c3288d440d5172391b098055fd2c376d3827e8a5944dd085d7
4
+ data.tar.gz: '0836095278d158e9b52234060cbfe111abcdc282a31659480ff2eb9db0bad5e8'
5
5
  SHA512:
6
- metadata.gz: fe5db04b1ebb441274c4b182d3eabed59229c040593fe7f0388c123972a6059572c6a2dff3633e1f72abd0a6771b3b6e2812e6b00d07b9c4d75ad9bd831b8059
7
- data.tar.gz: bba50ee147da4897cefce7297834584127183a04a6bb3a8c15525dabff70c04f865eaa8204dc69eebc991f72fc1cc1c01928ad5490be1ee164f656f39c70cc45
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
@@ -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). The runner
9
- # protocol wraps it in a 1-element Array internally so that eval
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 :config, :sock
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, compress: config.compress)
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
- sleep(config.delay) if config.delay
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
- return if @send_tick_eof || (n && n > 0 && i >= n)
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
- break if @send_tick_eof || (n && n > 0 && i >= n)
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
- break if @recv_tick_eof || (n && n > 0 && i >= n)
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: 1-element Array. nnq sockets take a bare String body.
314
+ # @param msg [String]
315
+ #
289
316
  def send_msg(msg)
290
- return if msg.empty?
291
- msg = [Marshal.dump(msg.first)] if config.format == :marshal
292
- msg = @fmt.compress(msg)
293
- @sock.send(msg.first)
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 [Array<String>, nil] 1-element Array (body), or nil on close.
329
+ # @return [String, nil] message body, or nil on close.
299
330
  def recv_msg
300
- raw = @sock.receive
301
- return nil if raw.nil?
302
- msg = @fmt.decompress([raw])
303
- msg = [Marshal.load(msg.first)] if config.format == :marshal
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 : [data]
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 = config.verbose
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
@@ -10,13 +10,13 @@ module NNQ
10
10
  EXAMPLES = <<~'TEXT'
11
11
  -- Request / Reply ------------------------------------------
12
12
 
13
- +-----+ "hello" +-----+
13
+ +-----+ "hello" +-----+
14
14
  | REQ |------------->| REP |
15
15
  | |<-------------| |
16
- +-----+ "HELLO" +-----+
16
+ +-----+ "HELLO" +-----+
17
17
 
18
18
  # terminal 1: echo server
19
- nnq rep --bind tcp://:5555 --recv-eval '$_.upcase'
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
- +-----+ "weather.nyc 72F" +-----+
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
- +-----+ "tick 1" +-----+
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 '$_.upcase'
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($_)).to_s'
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 '$_.upcase'
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 '$_.upcase'
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 '$_.include?("error") ? $_ : nil'
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($_)["temperature"]'
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 'upcase($_)'
126
+ nnq rep --bind tcp://:5555 --require ./transform.rb -e 'transform(it)'
131
127
 
132
- # next skips, break stops -- regexps match against $_
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($_); nil END{puts @sum}'
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 '$_.upcase'
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 '$_.upcase' -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 '$_.upcase'
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: false,
182
- compress_in: false,
183
- compress_out: false,
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
- o.on("-z", "--compress", "LZ4 compression per message (modal with --in/--out)") do
299
- require "rlz4"
300
- case pipe_side
301
- when :in
302
- opts[:compress_in] = true
303
- when :out
304
- opts[:compress_out] = true
305
- else
306
- opts[:compress] = true
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 ($_ = body)") { |v| opts[:recv_expr] = v }
312
- o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($_ = body)") { |v| opts[:send_expr] = v }
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 (max 16)") { |v|
318
- opts[:parallel] = [v, 16].min
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
- # nnq has no multipart, so `$F` is always a 1-element array and `$_`
12
- # is the body string.
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 # rubocop:disable Security/Eval
32
- @end_proc = eval("proc { #{end_body} }") if end_body # rubocop:disable Security/Eval
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 { $_ = $F&.first; #{expr} }") # rubocop:disable Security/Eval
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 Array, nil (filter/skip), or SENT.
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 [result] if @format == :marshal
54
+ return result if @format == :marshal
56
55
 
57
- result = result.is_a?(Array) ? result.first(1) : [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 = result.is_a?(Array) ? result.first(1) : [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
- extract = ->(expr, kw) {
81
- s = expr.index(/#{kw}\s*\{/)
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 # rubocop:disable Security/Eval
98
- end_proc = eval("proc { #{end_body} }") if end_body # rubocop:disable Security/Eval
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
- ractor_expr = expr.gsub(/\$F\b/, "__F")
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
- private
110
-
111
-
112
- def extract_blocks(expr)
113
- expr, begin_body = extract_block(expr, "BEGIN")
114
- expr, end_body = extract_block(expr, "END")
115
- [expr, begin_body, end_body]
116
- end
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
@@ -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, plus optional LZ4 compression.
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 still accepts/returns a
13
- # 1-element array so that `$F`-based eval expressions work the same
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, :jsonl, :msgpack, :marshal)
17
- # @param compress [Boolean] whether to apply LZ4 compression
18
- def initialize(format, compress: false)
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 [Array<String>] single-element array (the body)
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
- body.b.gsub(/[^[:print:]\t]/, ".") + "\n"
27
+ msg.b.gsub(/[^[:print:]\t]/, ".") << "\n"
33
28
  when :quoted
34
- body.b.dump[1..-2] + "\n"
29
+ msg.b.dump[1..-2] << "\n"
35
30
  when :raw
36
- body
37
- when :jsonl
38
- JSON.generate([body]) + "\n"
31
+ msg # FIXME: are these really the wire bytes?
39
32
  when :msgpack
40
- MessagePack.pack([body])
33
+ MessagePack.pack(msg)
41
34
  when :marshal
42
- body.inspect + "\n"
35
+ msg.inspect << "\n"
43
36
  end
44
37
  end
45
38
 
46
39
 
47
- # Decodes a formatted input line into a 1-element message array.
40
+ # Decodes a formatted input line into a message body.
48
41
  #
49
42
  # @param line [String] input line (newline-terminated)
50
- # @return [Array<String>] 1-element array
43
+ # @return [String] message
44
+ #
51
45
  def decode(line)
52
46
  case @format
53
47
  when :ascii, :marshal
54
- [line.chomp]
48
+ line.chomp
55
49
  when :quoted
56
- ["\"#{line.chomp}\"".undump]
50
+ "\"#{line.chomp}\"".undump
57
51
  when :raw
58
- [line]
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 msg [Array<String>] single-element array
82
+ # @param body [String] message body
116
83
  # @return [String] truncated preview
117
- def self.preview(msg)
118
- body = msg.first.to_s
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, compress: config.compress_in || config.compress)
16
- @fmt_out = Formatter.new(config.format, compress: config.compress_out || config.compress)
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 = @fmt_in.decompress([body])
107
- msg = eval_recv_expr(msg)
108
+ msg = eval_recv_expr(body)
108
109
 
109
- if msg && !msg.empty?
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
- title << "-z" if config.compress || config.compress_in || config.compress_out
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 << "->"
@@ -24,8 +24,8 @@ module NNQ
24
24
  compile_expr
25
25
  run_message_loop
26
26
  run_end_block
27
- rescue NNQ::CLI::DecompressError => e
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([event.detail[:body]])}"
68
+ "nnq: >> #{NNQ::CLI::Formatter.preview(event.detail[:body])}"
67
69
  when :message_received
68
- "nnq: << #{NNQ::CLI::Formatter.preview([event.detail[:body]])}"
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(@fmt_in.decompress([body]), &@eval_proc)
103
+ @ctx.instance_exec(body, &@eval_proc)
104
104
  )
105
- unless msg.nil? || msg.empty?
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
- out = @fmt_out.compress(@fmt_in.decompress([body]))
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
- if out && !out.empty?
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
@@ -13,33 +13,39 @@ module NNQ
13
13
  def run_loop(task)
14
14
  n = config.count
15
15
  i = 0
16
- sleep(config.delay) if config.delay
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
- msg = read_next
19
- break unless msg
20
- msg = eval_send_expr(msg)
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
- msg = [Marshal.dump(msg.first)] if config.format == :marshal
36
- msg = @fmt.compress(msg)
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
- reply = @fmt.decompress([reply_body])
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.empty?
98
- msg = [Marshal.dump(msg.first)] if config.format == :marshal
99
- msg = @fmt.compress(msg)
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
@@ -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
- sock.options.max_message_size =
28
- case config.recv_maxsz
29
- when nil then DEFAULT_RECV_MAXSZ
30
- when 0 then nil
31
- else config.recv_maxsz
32
- end
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
- sock = klass.new(
42
- linger: config.linger,
43
- send_hwm: config.send_hwm || DEFAULT_HWM,
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
- msg = [Marshal.dump(msg.first)] if config.format == :marshal
34
- msg = @fmt.compress(msg)
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 = @fmt.decompress([body])
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.empty?
105
- msg = [Marshal.dump(msg.first)] if config.format == :marshal
106
- msg = @fmt.compress(msg)
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([event.detail[:body]])}"
39
+ "#{prefix}nnq: >> #{Formatter.preview(event.detail[:body])}"
40
40
  when :message_received
41
- "#{prefix}nnq: << #{Formatter.preview([event.detail[:body]])}"
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}" : ""
@@ -2,6 +2,6 @@
2
2
 
3
3
  module NNQ
4
4
  module CLI
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
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 DecompressError => e
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.2.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: msgpack
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: rlz4
41
+ name: msgpack
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '0.1'
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.1'
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 LZ4 compression. Like
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