omq-cli 0.8.2 → 0.10.1

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: baffaed1125c73842459b939d7be333d00bd9a56716f291951f952dd07140c76
4
+ data.tar.gz: 9065a49914bd1b035ca5cd9dfd65dd22fcb200484f5767c9ff196935d1a0731c
5
5
  SHA512:
6
- metadata.gz: bcaa20a52a55171a61098296f0a0a9d8e653bbc6758cf0b21c692c54baa483b3d0d07d96e8c1c9095853984e7c4fe64a2efee60934c193a583892651afa896d8
7
- data.tar.gz: 93f70eb26ca6e182486fb2280dedf1b780fa084e9d810c5d9eaeeb3842cdcbe968e22b9ec6517ce69d395c095350f59c94aff142c2ce75f9ecc31a925b1ff73b
6
+ metadata.gz: 1132c8e05c291fa46d58d9c4c31612519a4d598c85948910f3292d622580e72d4ad2af2ac64f5dbeba0e98e33571dc985885e39ddc567e5d468dcedb49b4ae43
7
+ data.tar.gz: e223a4c7ae5f495059463bdab4aa28f29d91cf6c106fbcd56ab1945a651f5c7ac018a2de5077fc8fd0bf5ef33360d5afebb6acee6d2612cd8679bd4ff22bb21a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 — 2026-04-09
4
+
5
+ ### Changed
6
+
7
+ - **`--send-hwm` / `--recv-hwm` collapsed into a single `--hwm N`** option.
8
+ Outside pipe modal mode it sets both send and recv HWM on the socket.
9
+ Inside pipe modal mode it follows the current `--in` / `--out` side:
10
+ `--in --hwm N` sets the input PULL's recv HWM, `--out --hwm N` sets
11
+ the output PUSH's send HWM. Breaking CLI change — scripts must update
12
+ from `--send-hwm`/`--recv-hwm` to `--hwm` (with `--in`/`--out` if they
13
+ need per-side values in pipe).
14
+
15
+ ## 0.9.0 — 2026-04-08
16
+
17
+ ### Changed
18
+
19
+ - **`--recv-maxsz` defaults to 1 MiB in the CLI** — the underlying `omq`
20
+ library no longer imposes a default (it's `nil`/unlimited as of this
21
+ release), but the CLI keeps a conservative 1 MiB cap for safety when
22
+ connecting to untrusted peers from a terminal. Pass `--recv-maxsz 0`
23
+ to disable the cap explicitly, or `--recv-maxsz N` to raise it.
24
+ - **Default HWM lowered to 100** (from libzmq's 1000) for both send and
25
+ recv. The CLI is typically used interactively or for short pipelines
26
+ where a smaller in-flight queue keeps memory bounded and surfaces
27
+ backpressure earlier. Users who want the old behavior can pass
28
+ `--send-hwm 1000 --recv-hwm 1000` (or `0` for unbounded). Pipe worker
29
+ sockets are unaffected — they still override to `PIPE_HWM` internally.
30
+ - **Compression codec: Zstandard → LZ4 frame format (BREAKING on the wire).**
31
+ `--compress` now uses the new [`rlz4`](../rlz4) gem (Rust extension over
32
+ `lz4_flex`) instead of `zstd-ruby`. Motivation: `zstd-ruby` is the only
33
+ existing Ractor-safe compressor gem, but LZ4 is a better fit for the
34
+ per-message-part workload (smaller frames, lower CPU). `rlz4` is
35
+ Ractor-safe by construction, so parallel `-P` workers now use the same
36
+ codec as the sequential path. **Wire format is incompatible** with prior
37
+ omq-cli versions when `--compress` is in use — both ends must upgrade.
38
+
39
+ ### Added
40
+
41
+ - **`--ffi` flag** — opt-in libzmq backend for any socket runner. Builds
42
+ sockets with `backend: :ffi`, so the CLI can drive native libzmq instead
43
+ of the pure-Ruby engine. Requires the optional `omq-ffi` gem and a
44
+ system libzmq 4.x; missing dependencies abort with a clear error.
45
+ Propagated through all socket construction sites: `BaseRunner`,
46
+ `PipeRunner`, `PipeWorker`, and `ParallelWorker`.
47
+
48
+ ### Fixed
49
+
50
+ - **`--send-eval` / `-E` on REP** — now rejected at validation time. REP
51
+ derives its reply from `--recv-eval` / `-e`, so `-E` was silently
52
+ ignored and the runner fell through to reading stdin, hanging the
53
+ request-reply cycle.
54
+ - **`-vvv` preview of REP/REQ envelopes** — empty delimiter frames now
55
+ render as `[0B]` instead of an empty string, so a REP reply with wire
56
+ parts `["", "1"]` previews as `(1B) [0B]|1` instead of the misleading
57
+ `(1B) |1` with a dangling leading pipe.
58
+
3
59
  ## 0.8.2 — 2026-04-08
4
60
 
5
61
  ### 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,18 @@ 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("--hwm N", Integer, "High water mark (default 100, 0=unbounded; modal with --in/--out)") do |v|
344
+ case pipe_side
345
+ when :in
346
+ opts[:recv_hwm] = v
347
+ when :out
348
+ opts[:send_hwm] = v
349
+ else
350
+ opts[:send_hwm] = v
351
+ opts[:recv_hwm] = v
352
+ end
353
+ end
344
354
  o.on("--sndbuf N", "SO_SNDBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:sndbuf] = parse_byte_size(v) }
345
355
  o.on("--rcvbuf N", "SO_RCVBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:rcvbuf] = parse_byte_size(v) }
346
356
 
@@ -348,8 +358,8 @@ module OMQ
348
358
  o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
349
359
 
350
360
  o.separator "\nCompression:"
351
- o.on("-z", "--compress", "Zstandard compression per frame (modal with --in/--out)") do
352
- require "zstd-ruby"
361
+ o.on("-z", "--compress", "LZ4 compression per frame (modal with --in/--out)") do
362
+ require "rlz4"
353
363
  case pipe_side
354
364
  when :in
355
365
  opts[:compress_in] = true
@@ -383,6 +393,14 @@ module OMQ
383
393
  o.on("-v", "--verbose", "Verbosity: -v endpoints, -vv events, -vvv messages") { opts[:verbose] += 1 }
384
394
  o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
385
395
  o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
396
+ o.on( "--ffi", "Use libzmq FFI backend (requires omq-ffi gem + system libzmq 4.x)") do
397
+ begin
398
+ require "omq/ffi"
399
+ rescue LoadError => e
400
+ abort "omq: --ffi requires the omq-ffi gem and system libzmq 4.x (#{e.message})"
401
+ end
402
+ opts[:ffi] = true
403
+ end
386
404
  o.on("-V", "--version") {
387
405
  if ENV["OMQ_DEV"]
388
406
  require_relative "../../../../omq/lib/omq/version"
@@ -430,21 +448,20 @@ module OMQ
430
448
  end
431
449
 
432
450
 
433
- # Parses a byte size string with optional K/M suffix.
451
+ # Parses a byte size string with an optional K/M/G suffix (binary,
452
+ # i.e. 1K = 1024 bytes).
434
453
  #
435
- # @param str [String] e.g. "4096", "4K", "1M"
454
+ # @param str [String] e.g. "4096", "4K", "1M", "2G"
436
455
  # @return [Integer] size in bytes
437
456
  #
438
457
  def parse_byte_size(str)
439
458
  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
459
+ when /\A(\d+)[kK]\z/ then $1.to_i * 1024
460
+ when /\A(\d+)[mM]\z/ then $1.to_i * 1024 * 1024
461
+ when /\A(\d+)[gG]\z/ then $1.to_i * 1024 * 1024 * 1024
462
+ when /\A\d+\z/ then str.to_i
446
463
  else
447
- abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M)"
464
+ abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M, 2G)"
448
465
  end
449
466
  end
450
467
 
@@ -482,6 +499,7 @@ module OMQ
482
499
  abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
483
500
  abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
484
501
  abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
502
+ 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
503
  abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
486
504
 
487
505
  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
+ # --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.10.1"
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.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -15,20 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.15'
19
- - - ">="
20
- - !ruby/object:Gem::Version
21
- version: 0.15.2
18
+ version: '0.16'
22
19
  type: :runtime
23
20
  prerelease: false
24
21
  version_requirements: !ruby/object:Gem::Requirement
25
22
  requirements:
26
23
  - - "~>"
27
24
  - !ruby/object:Gem::Version
28
- version: '0.15'
25
+ version: '0.16'
26
+ - !ruby/object:Gem::Dependency
27
+ name: omq-ffi
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
29
30
  - - ">="
30
31
  - !ruby/object:Gem::Version
31
- version: 0.15.2
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
32
40
  - !ruby/object:Gem::Dependency
33
41
  name: omq-rfc-clientserver
34
42
  requirement: !ruby/object:Gem::Requirement
@@ -128,24 +136,24 @@ dependencies:
128
136
  - !ruby/object:Gem::Version
129
137
  version: '7.0'
130
138
  - !ruby/object:Gem::Dependency
131
- name: zstd-ruby
139
+ name: rlz4
132
140
  requirement: !ruby/object:Gem::Requirement
133
141
  requirements:
134
- - - ">="
142
+ - - "~>"
135
143
  - !ruby/object:Gem::Version
136
- version: '0'
144
+ version: '0.1'
137
145
  type: :runtime
138
146
  prerelease: false
139
147
  version_requirements: !ruby/object:Gem::Requirement
140
148
  requirements:
141
- - - ">="
149
+ - - "~>"
142
150
  - !ruby/object:Gem::Version
143
- version: '0'
151
+ version: '0.1'
144
152
  description: Command-line tool for sending and receiving ZeroMQ messages on any socket
145
153
  type (REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER, and all draft types). Supports
146
154
  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.
155
+ multiple formats (ASCII, JSON Lines, msgpack, Marshal), LZ4 compression, and CURVE
156
+ encryption. Like nngcat from libnng, but with Ruby superpowers.
149
157
  email:
150
158
  - paddor@gmail.com
151
159
  executables: