nnq-cli 0.2.0 → 0.3.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: b3b69a427aa491a1d47cb1144cf96824469c9029658239250d0b98f9d69ae43a
4
+ data.tar.gz: 43e68cf5d450fa777e79dd8913d9381eb0b436fe08d6e193c90cb47ecc42e036
5
5
  SHA512:
6
- metadata.gz: fe5db04b1ebb441274c4b182d3eabed59229c040593fe7f0388c123972a6059572c6a2dff3633e1f72abd0a6771b3b6e2812e6b00d07b9c4d75ad9bd831b8059
7
- data.tar.gz: bba50ee147da4897cefce7297834584127183a04a6bb3a8c15525dabff70c04f865eaa8204dc69eebc991f72fc1cc1c01928ad5490be1ee164f656f39c70cc45
6
+ metadata.gz: afc90caf9d6ba1bdfe488af48991af91a73a95d88ce5d6fe156f0255c7faef85de7b6016bb9db36c41be94c504c9ac8562ebd28ba20544f280afa00a8fff2ee9
7
+ data.tar.gz: 4fe278dcb3f5cdfc9171c6ebbb6de1cf2a30b49c01d1597b6c359cdb1d8903290be7ed068c7c7e2dab98001c9fac5207b544b5c6b1c611a18ebac1585baceedf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-04-15
4
+
5
+ - **Compression switched from LZ4 to Zstd** via the new
6
+ [nnq-zstd](https://github.com/paddor/nnq-zstd) gem. `-z`
7
+ (fast, level −3) and `-Z` (balanced, level 3) are mutually
8
+ exclusive and map to transparent `NNQ::Zstd.wrap` decorators
9
+ around each socket. Sender-side dictionary training and in-band
10
+ dict shipping mean compression ratios on streams of similar
11
+ small messages are now dramatically better than the old
12
+ stateless LZ4 path. Both peers still have to pass `-z`/`-Z`;
13
+ there is no negotiation.
14
+ - **`-Z` / `--compress-high`** flag for balanced Zstd.
15
+ - **Formatter no longer knows about compression.** All per-message
16
+ compression state and logic moved out of `Formatter`; compression
17
+ is applied transparently at the socket boundary via the new
18
+ wrapper. Fewer code paths in runners.
19
+ - **`rlz4` dependency dropped** in favor of `nnq-zstd ~> 0.1`.
20
+ - **Lazy loading.** `nnq/zstd` (and therefore `rzstd` and the Rust
21
+ extension) is only required when `-z`/`-Z` is actually used,
22
+ keeping startup cost unchanged for non-compressing runs.
23
+
3
24
  ## 0.2.0 — 2026-04-15
4
25
 
5
26
  - **Peer wait for bind-mode bounded senders** — `nnq push -b ... -d
@@ -20,7 +20,7 @@ module NNQ
20
20
  def initialize(config, socket_class)
21
21
  @config = config
22
22
  @klass = socket_class
23
- @fmt = Formatter.new(config.format, compress: config.compress)
23
+ @fmt = Formatter.new(config.format)
24
24
  end
25
25
 
26
26
 
@@ -64,7 +64,8 @@ module NNQ
64
64
 
65
65
 
66
66
  def create_socket
67
- SocketSetup.build(@klass, config)
67
+ sock = SocketSetup.build(@klass, config)
68
+ SocketSetup.maybe_wrap_zstd(sock, config.compress)
68
69
  end
69
70
 
70
71
 
@@ -288,9 +289,8 @@ module NNQ
288
289
  # msg: 1-element Array. nnq sockets take a bare String body.
289
290
  def send_msg(msg)
290
291
  return if msg.empty?
291
- msg = [Marshal.dump(msg.first)] if config.format == :marshal
292
- msg = @fmt.compress(msg)
293
- @sock.send(msg.first)
292
+ body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
293
+ @sock.send(body)
294
294
  transient_ready!
295
295
  end
296
296
 
@@ -299,10 +299,9 @@ module NNQ
299
299
  def recv_msg
300
300
  raw = @sock.receive
301
301
  return nil if raw.nil?
302
- msg = @fmt.decompress([raw])
303
- msg = [Marshal.load(msg.first)] if config.format == :marshal
302
+ body = config.format == :marshal ? Marshal.load(raw) : raw
304
303
  transient_ready!
305
- msg
304
+ [body]
306
305
  end
307
306
 
308
307
 
@@ -415,7 +414,7 @@ module NNQ
415
414
  def set_process_title(endpoints: nil)
416
415
  eps = endpoints || config.endpoints
417
416
  title = ["nnq", config.type_name]
418
- title << "-z" if config.compress
417
+ title << (config.compress == :balanced ? "-Z" : "-z") if config.compress
419
418
  title << "-P#{config.parallel}" if config.parallel
420
419
  eps.each do |ep|
421
420
  title << (ep.respond_to?(:url) ? ep.url : ep.to_s)
@@ -178,9 +178,9 @@ module NNQ
178
178
  send_hwm: nil,
179
179
  sndbuf: nil,
180
180
  rcvbuf: nil,
181
- compress: false,
182
- compress_in: false,
183
- compress_out: false,
181
+ compress: nil,
182
+ compress_in: nil,
183
+ compress_out: nil,
184
184
  send_expr: nil,
185
185
  recv_expr: nil,
186
186
  parallel: nil,
@@ -295,16 +295,24 @@ module NNQ
295
295
  o.on("--rcvbuf N", "SO_RCVBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:rcvbuf] = parse_byte_size(v) }
296
296
 
297
297
  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
298
+ load_zstd = -> { require "nnq/zstd" }
299
+ set_compress = lambda do |sym|
300
+ load_zstd.call
301
+ target = case pipe_side
302
+ when :in then :compress_in
303
+ when :out then :compress_out
304
+ else :compress
305
+ end
306
+ if opts[target] && opts[target] != sym
307
+ abort "nnq: -z and -Z are mutually exclusive"
307
308
  end
309
+ opts[target] = sym
310
+ end
311
+ o.on("-z", "--compress", "Zstd compression (fast, level -3; modal with --in/--out)") do
312
+ set_compress.call(:fast)
313
+ end
314
+ o.on("-Z", "--compress-high", "Zstd compression (balanced, level 3; modal with --in/--out)") do
315
+ set_compress.call(:balanced)
308
316
  end
309
317
 
310
318
  o.separator "\nProcessing (-e = incoming, -E = outgoing):"
@@ -2,11 +2,9 @@
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
10
  # `String` body per message. The API still accepts/returns a
@@ -14,10 +12,8 @@ module NNQ
14
12
  # way.
15
13
  class Formatter
16
14
  # @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
15
+ def initialize(format)
16
+ @format = format
21
17
  end
22
18
 
23
19
 
@@ -89,27 +85,6 @@ module NNQ
89
85
  end
90
86
 
91
87
 
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
88
  # Formats a message body for human-readable preview (logging).
114
89
  #
115
90
  # @param msg [Array<String>] single-element array
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,12 +105,10 @@ 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
110
  if msg && !msg.empty?
110
- out = @fmt_out.compress(msg)
111
- @push.send(out.first)
111
+ @push.send(msg.first)
112
112
  end
113
113
 
114
114
  # Yield after send so send-pump fibers can drain the queue
@@ -163,7 +163,9 @@ module NNQ
163
163
  def set_pipe_process_title
164
164
  in_eps, out_eps = resolve_endpoints
165
165
  title = ["nnq pipe"]
166
- title << "-z" if config.compress || config.compress_in || config.compress_out
166
+ if (m = config.compress || config.compress_in || config.compress_out)
167
+ title << (m == :balanced ? "-Z" : "-z")
168
+ end
167
169
  title << "-P#{config.parallel}" if config.parallel
168
170
  title.concat(in_eps.map(&:url))
169
171
  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
 
@@ -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,11 +100,10 @@ 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
105
  unless msg.nil? || msg.empty?
106
- out = @fmt_out.compress(msg)
107
- @push.send(out.first)
106
+ @push.send(msg.first)
108
107
  end
109
108
  n -= 1 if n && n > 0
110
109
  break if n == 0
@@ -113,8 +112,7 @@ module NNQ
113
112
  loop do
114
113
  body = @pull.receive
115
114
  break if body.nil?
116
- out = @fmt_out.compress(@fmt_in.decompress([body]))
117
- @push.send(out.first)
115
+ @push.send(body)
118
116
  n -= 1 if n && n > 0
119
117
  break if n == 0
120
118
  end
@@ -130,7 +128,7 @@ module NNQ
130
128
  @ctx.instance_exec(&@end_proc)
131
129
  )
132
130
  if out && !out.empty?
133
- @push.send(@fmt_out.compress(out).first)
131
+ @push.send(out.first)
134
132
  end
135
133
  end
136
134
  end
@@ -32,14 +32,12 @@ module NNQ
32
32
 
33
33
  def request_and_receive(msg)
34
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)
35
+ body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
36
+ reply_body = @sock.send_request(body)
38
37
  transient_ready!
39
38
  return nil if reply_body.nil?
40
- reply = @fmt.decompress([reply_body])
41
- reply = [Marshal.load(reply.first)] if config.format == :marshal
42
- reply
39
+ reply = config.format == :marshal ? Marshal.load(reply_body) : reply_body
40
+ [reply]
43
41
  end
44
42
 
45
43
 
@@ -95,9 +93,8 @@ module NNQ
95
93
 
96
94
  def send_reply(msg)
97
95
  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)
96
+ body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
97
+ @sock.send_reply(body)
101
98
  transient_ready!
102
99
  end
103
100
  end
@@ -77,6 +77,16 @@ module NNQ
77
77
  end
78
78
 
79
79
 
80
+ # Wrap +sock+ with NNQ::Zstd if +mode+ is :fast or :balanced.
81
+ # Returns the wrapper (decorator) or +sock+ unchanged.
82
+ def self.maybe_wrap_zstd(sock, mode)
83
+ return sock unless mode
84
+ require "nnq/zstd"
85
+ level = mode == :balanced ? 3 : -3
86
+ NNQ::Zstd.wrap(sock, level: level)
87
+ end
88
+
89
+
80
90
  # Subscribe to prefixes on a SUB socket.
81
91
  #
82
92
  # Unlike ZeroMQ, nng's sub0 starts with an empty subscription set,
@@ -30,9 +30,8 @@ module NNQ
30
30
 
31
31
  def survey_and_collect(msg)
32
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)
33
+ body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
34
+ @sock.send_survey(body)
36
35
  transient_ready!
37
36
  collect_replies
38
37
  end
@@ -42,9 +41,8 @@ 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
47
- output(eval_recv_expr(reply))
44
+ reply = config.format == :marshal ? Marshal.load(body) : body
45
+ output(eval_recv_expr([reply]))
48
46
  rescue NNQ::TimedOut
49
47
  break
50
48
  end
@@ -102,9 +100,8 @@ module NNQ
102
100
 
103
101
  def send_reply(msg)
104
102
  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)
103
+ body = config.format == :marshal ? Marshal.dump(msg.first) : msg.first
104
+ @sock.send_reply(body)
108
105
  transient_ready!
109
106
  end
110
107
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module NNQ
4
4
  module CLI
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.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.3.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