omq-cli 0.8.2 → 0.9.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: ca06571b7529ab16630ce2f769a2320607baf67b87888218ada7a1aa6f05f52a
4
- data.tar.gz: c46c9b2a8b358ac16d295ea15031da901859c0130295f4508cf3422eac083a24
3
+ metadata.gz: a4acc7ab47167da27247f7b45b82729da6302868879f1a003727ff2b359d42d5
4
+ data.tar.gz: 4a667cab2f594ade3cdc7b4234dc97562bbee043707992935d566dd563530795
5
5
  SHA512:
6
- metadata.gz: bcaa20a52a55171a61098296f0a0a9d8e653bbc6758cf0b21c692c54baa483b3d0d07d96e8c1c9095853984e7c4fe64a2efee60934c193a583892651afa896d8
7
- data.tar.gz: 93f70eb26ca6e182486fb2280dedf1b780fa084e9d810c5d9eaeeb3842cdcbe968e22b9ec6517ce69d395c095350f59c94aff142c2ce75f9ecc31a925b1ff73b
6
+ metadata.gz: 41f4141def92292e2a35412af5ddd9d1a567af2b150b59e805452cde24f9a17279e162d47f44bcb84c9d1e7fb3ad8dad2971162ffad2f38814f753c17db95ab7
7
+ data.tar.gz: 340c7f417266cbeb7020cd19a3a0161de78310b78cf554443c71ba22cef05c69611e38f9d4ef9370ae37b2c27bed2b59241c15ff88e7e077663ae0d0503d6f9b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 — 2026-04-08
4
+
5
+ ### Changed
6
+
7
+ - **`--recv-maxsz` defaults to 1 MiB in the CLI** — the underlying `omq`
8
+ library no longer imposes a default (it's `nil`/unlimited as of this
9
+ release), but the CLI keeps a conservative 1 MiB cap for safety when
10
+ connecting to untrusted peers from a terminal. Pass `--recv-maxsz 0`
11
+ to disable the cap explicitly, or `--recv-maxsz N` to raise it.
12
+ - **Default HWM lowered to 100** (from libzmq's 1000) for both send and
13
+ recv. The CLI is typically used interactively or for short pipelines
14
+ where a smaller in-flight queue keeps memory bounded and surfaces
15
+ backpressure earlier. Users who want the old behavior can pass
16
+ `--send-hwm 1000 --recv-hwm 1000` (or `0` for unbounded). Pipe worker
17
+ sockets are unaffected — they still override to `PIPE_HWM` internally.
18
+ - **Compression codec: Zstandard → LZ4 frame format (BREAKING on the wire).**
19
+ `--compress` now uses the new [`rlz4`](../rlz4) gem (Rust extension over
20
+ `lz4_flex`) instead of `zstd-ruby`. Motivation: `zstd-ruby` is the only
21
+ existing Ractor-safe compressor gem, but LZ4 is a better fit for the
22
+ per-message-part workload (smaller frames, lower CPU). `rlz4` is
23
+ Ractor-safe by construction, so parallel `-P` workers now use the same
24
+ codec as the sequential path. **Wire format is incompatible** with prior
25
+ omq-cli versions when `--compress` is in use — both ends must upgrade.
26
+
27
+ ### Added
28
+
29
+ - **`--ffi` flag** — opt-in libzmq backend for any socket runner. Builds
30
+ sockets with `backend: :ffi`, so the CLI can drive native libzmq instead
31
+ of the pure-Ruby engine. Requires the optional `omq-ffi` gem and a
32
+ system libzmq 4.x; missing dependencies abort with a clear error.
33
+ Propagated through all socket construction sites: `BaseRunner`,
34
+ `PipeRunner`, `PipeWorker`, and `ParallelWorker`.
35
+
36
+ ### Fixed
37
+
38
+ - **`--send-eval` / `-E` on REP** — now rejected at validation time. REP
39
+ derives its reply from `--recv-eval` / `-e`, so `-E` was silently
40
+ ignored and the runner fell through to reading stdin, hanging the
41
+ request-reply cycle.
42
+ - **`-vvv` preview of REP/REQ envelopes** — empty delimiter frames now
43
+ render as `[0B]` instead of an empty string, so a REP reply with wire
44
+ parts `["", "1"]` previews as `(1B) [0B]|1` instead of the misleading
45
+ `(1B) |1` with a dangling leading pipe.
46
+
3
47
  ## 0.8.2 — 2026-04-08
4
48
 
5
49
  ### Fixed
data/README.md CHANGED
@@ -336,7 +336,8 @@ omq pull -b tcp://:5557 -t 5
336
336
 
337
337
  ## Compression
338
338
 
339
- Both sides must use `--compress` (`-z`). Requires the `zstd-ruby` gem.
339
+ Both sides must use `--compress` (`-z`). Uses LZ4 frame format, provided
340
+ by the `rlz4` gem (Ractor-safe, Rust extension via `lz4_flex`).
340
341
 
341
342
  ```sh
342
343
  omq push -c tcp://remote:5557 -z < data.txt
@@ -233,6 +233,7 @@ module OMQ
233
233
  curve_server: false,
234
234
  curve_server_key: nil,
235
235
  crypto: nil,
236
+ ffi: false,
236
237
  }.freeze
237
238
 
238
239
 
@@ -338,9 +339,9 @@ module OMQ
338
339
  end
339
340
  }
340
341
  o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
341
- o.on("--recv-maxsz COUNT", Integer, "Max inbound message size in bytes (larger messages dropped)") { |v| opts[:recv_maxsz] = v }
342
- o.on("--send-hwm N", Integer, "Send high water mark (default 1000, 0=unbounded)") { |v| opts[:send_hwm] = v }
343
- o.on("--recv-hwm N", Integer, "Recv high water mark (default 1000, 0=unbounded)") { |v| opts[:recv_hwm] = v }
342
+ o.on("--recv-maxsz SIZE", "Max inbound message size, e.g. 4096, 64K, 1M, 2G (default 1M, 0=unlimited; larger messages drop the connection)") { |v| opts[:recv_maxsz] = parse_byte_size(v) }
343
+ o.on("--send-hwm N", Integer, "Send high water mark (default 100, 0=unbounded)") { |v| opts[:send_hwm] = v }
344
+ o.on("--recv-hwm N", Integer, "Recv high water mark (default 100, 0=unbounded)") { |v| opts[:recv_hwm] = v }
344
345
  o.on("--sndbuf N", "SO_SNDBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:sndbuf] = parse_byte_size(v) }
345
346
  o.on("--rcvbuf N", "SO_RCVBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:rcvbuf] = parse_byte_size(v) }
346
347
 
@@ -348,8 +349,8 @@ module OMQ
348
349
  o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
349
350
 
350
351
  o.separator "\nCompression:"
351
- o.on("-z", "--compress", "Zstandard compression per frame (modal with --in/--out)") do
352
- require "zstd-ruby"
352
+ o.on("-z", "--compress", "LZ4 compression per frame (modal with --in/--out)") do
353
+ require "rlz4"
353
354
  case pipe_side
354
355
  when :in
355
356
  opts[:compress_in] = true
@@ -383,6 +384,14 @@ module OMQ
383
384
  o.on("-v", "--verbose", "Verbosity: -v endpoints, -vv events, -vvv messages") { opts[:verbose] += 1 }
384
385
  o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
385
386
  o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
387
+ o.on( "--ffi", "Use libzmq FFI backend (requires omq-ffi gem + system libzmq 4.x)") do
388
+ begin
389
+ require "omq/ffi"
390
+ rescue LoadError => e
391
+ abort "omq: --ffi requires the omq-ffi gem and system libzmq 4.x (#{e.message})"
392
+ end
393
+ opts[:ffi] = true
394
+ end
386
395
  o.on("-V", "--version") {
387
396
  if ENV["OMQ_DEV"]
388
397
  require_relative "../../../../omq/lib/omq/version"
@@ -430,21 +439,20 @@ module OMQ
430
439
  end
431
440
 
432
441
 
433
- # Parses a byte size string with optional K/M suffix.
442
+ # Parses a byte size string with an optional K/M/G suffix (binary,
443
+ # i.e. 1K = 1024 bytes).
434
444
  #
435
- # @param str [String] e.g. "4096", "4K", "1M"
445
+ # @param str [String] e.g. "4096", "4K", "1M", "2G"
436
446
  # @return [Integer] size in bytes
437
447
  #
438
448
  def parse_byte_size(str)
439
449
  case str
440
- when /\A(\d+)[kK]\z/
441
- $1.to_i * 1024
442
- when /\A(\d+)[mM]\z/
443
- $1.to_i * 1024 * 1024
444
- when /\A\d+\z/
445
- str.to_i
450
+ when /\A(\d+)[kK]\z/ then $1.to_i * 1024
451
+ when /\A(\d+)[mM]\z/ then $1.to_i * 1024 * 1024
452
+ when /\A(\d+)[gG]\z/ then $1.to_i * 1024 * 1024 * 1024
453
+ when /\A\d+\z/ then str.to_i
446
454
  else
447
- abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M)"
455
+ abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M, 2G)"
448
456
  end
449
457
  end
450
458
 
@@ -482,6 +490,7 @@ module OMQ
482
490
  abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
483
491
  abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
484
492
  abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
493
+ abort "--send-eval is not valid for REP (the reply is the result of --recv-eval / -e)" if opts[:send_expr] && type_name == "rep"
485
494
  abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
486
495
 
487
496
  if opts[:parallel]
@@ -58,6 +58,7 @@ module OMQ
58
58
  :curve_server,
59
59
  :curve_server_key,
60
60
  :crypto,
61
+ :ffi,
61
62
  :stdin_is_tty,
62
63
  ) do
63
64
  # @return [Boolean] true if this socket type only sends
@@ -2,14 +2,14 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- # Raised when Zstandard decompression fails.
5
+ # Raised when LZ4 decompression fails.
6
6
  class DecompressError < RuntimeError; end
7
7
 
8
8
  # Handles encoding/decoding messages in the configured format,
9
- # plus optional Zstandard compression.
9
+ # plus optional LZ4 compression.
10
10
  class Formatter
11
11
  # @param format [Symbol] wire format (:ascii, :quoted, :raw, :jsonl, :msgpack, :marshal)
12
- # @param compress [Boolean] whether to apply Zstandard compression per frame
12
+ # @param compress [Boolean] whether to apply LZ4 compression per frame
13
13
  def initialize(format, compress: false)
14
14
  @format = format
15
15
  @compress = compress
@@ -83,22 +83,22 @@ module OMQ
83
83
  end
84
84
 
85
85
 
86
- # Compresses each frame with Zstandard if compression is enabled.
86
+ # Compresses each frame with LZ4 if compression is enabled.
87
87
  #
88
88
  # @param parts [Array<String>] message frames
89
89
  # @return [Array<String>] optionally compressed frames
90
90
  def compress(parts)
91
- @compress ? parts.map { |p| Zstd.compress(p) } : parts
91
+ @compress ? parts.map { |p| RLZ4.compress(p) } : parts
92
92
  end
93
93
 
94
94
 
95
- # Decompresses each frame with Zstandard if compression is enabled.
95
+ # Decompresses each frame with LZ4 if compression is enabled.
96
96
  #
97
97
  # @param parts [Array<String>] possibly compressed message frames
98
98
  # @return [Array<String>] decompressed frames
99
99
  def decompress(parts)
100
- @compress ? parts.map { |p| Zstd.decompress(p) } : parts
101
- rescue
100
+ @compress ? parts.map { |p| RLZ4.decompress(p) } : parts
101
+ rescue RLZ4::DecompressError
102
102
  raise DecompressError, "decompression failed (did the sender use --compress?)"
103
103
  end
104
104
 
@@ -110,15 +110,23 @@ module OMQ
110
110
  def self.preview(parts)
111
111
  total = parts.sum(&:bytesize)
112
112
  shown = parts.first(3).map { |p| preview_frame(p) }
113
- tail = parts.size > 3 ? "|...(#{parts.size} parts)" : ""
113
+ tail = parts.size > 3 ? "|...(#{parts.size} parts)" : ""
114
+
114
115
  "(#{total}B) #{shown.join("|")}#{tail}"
115
116
  end
116
117
 
117
118
 
118
119
  def self.preview_frame(part)
119
120
  bytes = part.b
120
- sample = bytes[0, 12]
121
+ # Empty frames must render as a visible marker, not as the empty
122
+ # string — otherwise joining with "|" would produce misleading
123
+ # output like "|body" for REP/REQ-style envelopes where the first
124
+ # wire frame is an empty delimiter.
125
+ return "[0B]" if bytes.empty?
126
+
127
+ sample = bytes[0, 12]
121
128
  printable = sample.count("\x20-\x7e")
129
+
122
130
  if printable < sample.bytesize / 2
123
131
  "[#{bytes.bytesize}B]"
124
132
  elsif bytes.bytesize > 12
@@ -41,7 +41,7 @@ module OMQ
41
41
 
42
42
 
43
43
  def setup_socket
44
- @sock = OMQ.const_get(@socket_sym).new
44
+ @sock = @config.ffi ? OMQ.const_get(@socket_sym).new(backend: :ffi) : OMQ.const_get(@socket_sym).new
45
45
  OMQ::CLI::SocketSetup.apply_options(@sock, @config)
46
46
  @sock.identity = @config.identity if @config.identity
47
47
  OMQ::CLI::SocketSetup.attach_endpoints(@sock, @endpoints, verbose: false)
data/lib/omq/cli/pipe.rb CHANGED
@@ -80,8 +80,9 @@ module OMQ
80
80
 
81
81
 
82
82
  def build_pull_push(in_eps, out_eps)
83
- pull = OMQ::PULL.new
84
- push = OMQ::PUSH.new
83
+ kwargs = config.ffi ? { backend: :ffi } : {}
84
+ pull = OMQ::PULL.new(**kwargs)
85
+ push = OMQ::PUSH.new(**kwargs)
85
86
  SocketSetup.apply_options(pull, config)
86
87
  SocketSetup.apply_options(push, config)
87
88
  pull.recv_hwm = PIPE_HWM unless config.recv_hwm
@@ -37,8 +37,9 @@ module OMQ
37
37
 
38
38
 
39
39
  def setup_sockets
40
- @pull = OMQ::PULL.new
41
- @push = OMQ::PUSH.new
40
+ kwargs = @config.ffi ? { backend: :ffi } : {}
41
+ @pull = OMQ::PULL.new(**kwargs)
42
+ @push = OMQ::PUSH.new(**kwargs)
42
43
  OMQ::CLI::SocketSetup.apply_options(@pull, @config)
43
44
  OMQ::CLI::SocketSetup.apply_options(@push, @config)
44
45
  @pull.recv_hwm = PipeRunner::PIPE_HWM unless @config.recv_hwm
@@ -71,7 +72,7 @@ module OMQ
71
72
  when :message_received
72
73
  "omq: << #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
73
74
  else
74
- ep = event.endpoint ? " #{event.endpoint}" : ""
75
+ ep = event.endpoint ? " #{event.endpoint}" : ""
75
76
  detail = event.detail ? " #{event.detail}" : ""
76
77
  "omq: #{event.type}#{ep}#{detail}"
77
78
  end
@@ -6,6 +6,21 @@ module OMQ
6
6
  # All methods are module-level so callers compose rather than inherit.
7
7
  #
8
8
  module SocketSetup
9
+ # Default high water mark applied when the user does not pass
10
+ # --send-hwm / --recv-hwm. Lower than libzmq's default (1000) to keep
11
+ # memory footprint small for the typical CLI use cases (interactive
12
+ # debugging, short-lived pipelines). Pipe worker sockets override this
13
+ # with a still-smaller value for tighter backpressure.
14
+ DEFAULT_HWM = 100
15
+
16
+ # Default max inbound message size applied when the user does not
17
+ # pass --recv-maxsz. The omq library itself is unlimited by default;
18
+ # the CLI caps inbound messages at 1 MiB so that a misconfigured or
19
+ # malicious peer cannot force arbitrary memory allocation on a
20
+ # terminal user. Users can raise it with --recv-maxsz N, or disable
21
+ # it entirely with --recv-maxsz 0.
22
+ DEFAULT_RECV_MAXSZ = 1 << 20
23
+
9
24
  # Apply common socket options from +config+ to +sock+.
10
25
  #
11
26
  def self.apply_options(sock, config)
@@ -14,8 +29,9 @@ module OMQ
14
29
  sock.send_timeout = config.timeout if config.timeout
15
30
  sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
16
31
  sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
17
- sock.send_hwm = config.send_hwm if config.send_hwm
18
- sock.recv_hwm = config.recv_hwm if config.recv_hwm
32
+ # nil → default; 0 stays 0 (unbounded), any other integer is taken as-is.
33
+ sock.send_hwm = config.send_hwm || DEFAULT_HWM
34
+ sock.recv_hwm = config.recv_hwm || DEFAULT_HWM
19
35
  sock.sndbuf = config.sndbuf if config.sndbuf
20
36
  sock.rcvbuf = config.rcvbuf if config.rcvbuf
21
37
  end
@@ -24,10 +40,16 @@ module OMQ
24
40
  # Create and fully configure a socket from +klass+ and +config+.
25
41
  #
26
42
  def self.build(klass, config)
27
- sock = klass.new
43
+ sock = config.ffi ? klass.new(backend: :ffi) : klass.new
28
44
  sock.conflate = true if config.conflate && %w[pub radio].include?(config.type_name)
29
45
  apply_options(sock, config)
30
- sock.max_message_size = config.recv_maxsz if config.recv_maxsz
46
+ # --recv-maxsz: nil 1 MiB default; 0 → explicitly unlimited; else → as-is.
47
+ sock.max_message_size =
48
+ case config.recv_maxsz
49
+ when nil then DEFAULT_RECV_MAXSZ
50
+ when 0 then nil
51
+ else config.recv_maxsz
52
+ end
31
53
  sock.identity = config.identity if config.identity
32
54
  sock.router_mandatory = true if config.type_name == "router"
33
55
  sock
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.8.2"
5
+ VERSION = "0.9.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -128,24 +128,24 @@ dependencies:
128
128
  - !ruby/object:Gem::Version
129
129
  version: '7.0'
130
130
  - !ruby/object:Gem::Dependency
131
- name: zstd-ruby
131
+ name: rlz4
132
132
  requirement: !ruby/object:Gem::Requirement
133
133
  requirements:
134
- - - ">="
134
+ - - "~>"
135
135
  - !ruby/object:Gem::Version
136
- version: '0'
136
+ version: '0.1'
137
137
  type: :runtime
138
138
  prerelease: false
139
139
  version_requirements: !ruby/object:Gem::Requirement
140
140
  requirements:
141
- - - ">="
141
+ - - "~>"
142
142
  - !ruby/object:Gem::Version
143
- version: '0'
143
+ version: '0.1'
144
144
  description: Command-line tool for sending and receiving ZeroMQ messages on any socket
145
145
  type (REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER, and all draft types). Supports
146
146
  Ruby eval (-e/-E), script handlers (-r), pipe virtual socket with Ractor parallelism,
147
- multiple formats (ASCII, JSON Lines, msgpack, Marshal), Zstandard compression, and
148
- CURVE encryption. Like nngcat from libnng, but with Ruby superpowers.
147
+ multiple formats (ASCII, JSON Lines, msgpack, Marshal), LZ4 compression, and CURVE
148
+ encryption. Like nngcat from libnng, but with Ruby superpowers.
149
149
  email:
150
150
  - paddor@gmail.com
151
151
  executables: