omq-cli 0.12.3 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +118 -0
- data/README.md +20 -12
- data/lib/omq/cli/base_runner.rb +42 -12
- data/lib/omq/cli/cli_parser.rb +51 -50
- data/lib/omq/cli/client_server.rb +5 -7
- data/lib/omq/cli/config.rb +2 -2
- data/lib/omq/cli/expression_evaluator.rb +7 -4
- data/lib/omq/cli/formatter.rb +15 -35
- data/lib/omq/cli/parallel_worker.rb +30 -11
- data/lib/omq/cli/pipe.rb +26 -11
- data/lib/omq/cli/pipe_worker.rb +47 -38
- data/lib/omq/cli/radio_dish.rb +0 -1
- data/lib/omq/cli/router_dealer.rb +1 -2
- data/lib/omq/cli/socket_setup.rb +52 -12
- data/lib/omq/cli/term.rb +36 -22
- data/lib/omq/cli/version.rb +1 -1
- data/lib/omq/cli.rb +1 -3
- metadata +19 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5a558fd06799fcf4bee5d5036a4cac86c8482554df3998e5f42171d04e520db
|
|
4
|
+
data.tar.gz: e53bb5236bb5bc660e04ad00c10aa0f3b1e4a848ed9d74363c3943f877bf210f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5841f42d980388be4ce4d0feea88b76a358e86f6ba6e64f6b1775dfdcad16dc349ba3866c824fa74ffb88005876e64c027de9fd93fa21417c8a5a53be3180861
|
|
7
|
+
data.tar.gz: 4b68d2c56882c87a06e75fa348eff4f9df75d5cd0c3a85c26e20e6676780c61c0413e329dff7bdbb082b861795cdf09bb30aa4b61c7279c88bbd2e29373d1552
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 — 2026-04-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Receive-capable sockets decompress by default.** All socket types
|
|
8
|
+
except pure senders (`push`, `pub`, `scatter`, `radio`) now advertise
|
|
9
|
+
the ZMTP-Zstd profile in **passive mode** at startup, so they accept
|
|
10
|
+
compressed frames from any active-sender peer without requiring
|
|
11
|
+
`-z` on the receive side. They never compress their own outgoing
|
|
12
|
+
frames in this mode — use `-z` / `-Z` / `--compress=LEVEL` on the
|
|
13
|
+
sender to opt it in. A `push` piped into a `pull` with no flags on
|
|
14
|
+
either side stays uncompressed; `omq push -z | omq pull` compresses
|
|
15
|
+
on the wire and the pull side decodes transparently. This is the
|
|
16
|
+
RFC Sec. 6.4 "Passive senders" mode; requires omq-rfc-zstd >= 0.1.0.
|
|
17
|
+
- **`-Z` flag for better-ratio compression (zstd level 3).** `-z`
|
|
18
|
+
remains the fast default (level -3) and `--compress=LEVEL` takes
|
|
19
|
+
a custom zstd level (e.g. `--compress=19`, `--compress=-1`). Short
|
|
20
|
+
bundling (`-zvvv`, `-Zvvv`) still works.
|
|
21
|
+
- **`-vvv` logs `ZDICT` exchange.** When the auto-trained dictionary
|
|
22
|
+
is shipped/received, the trace prints `>> ZDICT (NB)` on the sender
|
|
23
|
+
and `<< ZDICT (NB)` on the receiver.
|
|
24
|
+
- **`-vvv` wire-size annotation for compressed traces.** Message
|
|
25
|
+
previews on compressed sockets include the post-compression byte
|
|
26
|
+
count: `(280B wire=29B) ZZ…`. Plumbed from the ZMTP-Zstd wrapper
|
|
27
|
+
through the engine's verbose monitor.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **Compression backend switched from `rlz4` to `omq-rfc-zstd`.**
|
|
32
|
+
Compression is now a ZMTP wire-protocol extension negotiated via
|
|
33
|
+
the `X-Compression` READY property and applied below the
|
|
34
|
+
application API. Auto-trained dictionaries are shipped over a
|
|
35
|
+
`ZDICT` command frame once the sender has enough samples. The
|
|
36
|
+
`Formatter` no longer compresses or decompresses anything — it
|
|
37
|
+
only encodes/decodes wire formats. Pipe `-z` is no longer modal
|
|
38
|
+
(`compress_in`/`compress_out` removed) since compression is a
|
|
39
|
+
per-socket, send-side property negotiated with each peer.
|
|
40
|
+
- **`-vvv` output ordering under compression.** At `-vvv`, the
|
|
41
|
+
monitor fiber now writes both the trace line and the plaintext
|
|
42
|
+
body, so trace-and-body pairs land on the tty in order instead of
|
|
43
|
+
interleaving between the recv pump and the app fiber.
|
|
44
|
+
- **TCP host normalization moved into `OMQ::Transport::TCP`.** `omq`
|
|
45
|
+
v0.19.0 now handles `tcp://*:PORT`, `tcp://:PORT`, and
|
|
46
|
+
`tcp://localhost:PORT` natively (including dual-stack `*` binding
|
|
47
|
+
both IPv4 and IPv6 wildcards), so `CliParser` no longer rewrites
|
|
48
|
+
these URLs before handing them off. Removed
|
|
49
|
+
`CliParser.loopback_bind_host` and the `normalize_bind`/
|
|
50
|
+
`normalize_connect`/`normalize_ep` block. Requires `omq ~> 0.19`.
|
|
51
|
+
- **Terminate on protocol errors instead of silent reconnect.** When
|
|
52
|
+
a peer sends a frame that violates the ZMTP wire protocol
|
|
53
|
+
(oversized, bad framing, zstd bytebomb, nonce exhaustion, …), the
|
|
54
|
+
library drops that one connection and reconnects — the libzmq
|
|
55
|
+
parity behavior. The CLI is a different audience: a persistent
|
|
56
|
+
protocol violation is almost always a misconfiguration the user
|
|
57
|
+
needs to see, not silently paper over. Every runner
|
|
58
|
+
(`BaseRunner`, `PipeRunner`, `ParallelWorker`, `PipeWorker`) now
|
|
59
|
+
attaches a monitor that watches for `:disconnected` events whose
|
|
60
|
+
`detail[:error]` is a `Protocol::ZMTP::Error`, prints
|
|
61
|
+
`omq: <reason>` to stderr, kills the socket, and exits with
|
|
62
|
+
status 1. Requires `omq ~> 0.19.2` for the new `:disconnected`
|
|
63
|
+
detail shape and `Socket#engine` accessor.
|
|
64
|
+
- **`-vvv` disconnect events render the reason in parentheses.**
|
|
65
|
+
`Term.format_event` now pretty-prints `:disconnected` details
|
|
66
|
+
that contain a `:reason` key, e.g.
|
|
67
|
+
`disconnected tcp://:5555 (frame size 1024 exceeds max_message_size 32)`,
|
|
68
|
+
instead of dumping the raw hash.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- **`--recv-maxsz` is now actually applied in pipe and parallel
|
|
73
|
+
modes.** `PipeRunner`, `PipeWorker`, and `ParallelWorker` were
|
|
74
|
+
only calling `SocketSetup.apply_options` + `apply_compression`
|
|
75
|
+
and skipping `max_message_size` entirely — so the default 1 MiB
|
|
76
|
+
cap (and any `--recv-maxsz` override) silently had no effect on
|
|
77
|
+
`omq pipe` or `omq pull -P`. Extracted the logic into
|
|
78
|
+
`SocketSetup.apply_recv_maxsz` and wired it into all four setup
|
|
79
|
+
paths (sequential pull/rep, sequential pipe, parallel worker,
|
|
80
|
+
pipe worker). Oversized frames now drop the connection as
|
|
81
|
+
intended and — combined with the CLI termination policy above —
|
|
82
|
+
exit with a clear error instead of hanging in a reconnect loop.
|
|
83
|
+
|
|
84
|
+
## 0.13.0 — 2026-04-12
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
- **`--timestamps[=PRECISION]` flag.** Prefix log lines with UTC
|
|
89
|
+
timestamps. Accepts `s`, `ms` (default), or `us`. Replaces the
|
|
90
|
+
former `-vvvv` special meaning, which has been removed.
|
|
91
|
+
- **`-M` / `--marshal` preserves arbitrary objects on the wire.**
|
|
92
|
+
Eval results in `--format marshal` mode (e.g. `Time.now`, hashes,
|
|
93
|
+
UTF-16LE strings) are now passed through unchanged instead of
|
|
94
|
+
being coerced via `#to_s`. Affects both the main runner and
|
|
95
|
+
Ractor workers (`-P`).
|
|
96
|
+
- **`-P0` ⇒ `nproc`.** `-P0` (or bare `-P`) spawns one Ractor worker
|
|
97
|
+
per CPU, capped at 16. Short-option clustering works: `-P0zvv`
|
|
98
|
+
expands to `-P 0 -z -v -v`.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- **`-vvv` shows decompressed message previews.** When `--compress`
|
|
103
|
+
is active, message traces now log the decompressed parts and
|
|
104
|
+
include the on-wire size: `(5B wire=21B) hello`. Engine-level
|
|
105
|
+
message events are suppressed in compressed mode to avoid
|
|
106
|
+
double-logging compressed bytes.
|
|
107
|
+
- **Truncation marker uses `…` (U+2026).** `Formatter.preview`
|
|
108
|
+
truncation now uses the real horizontal ellipsis character
|
|
109
|
+
instead of three ASCII dots.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
- **`FrozenError` in eval handlers returning received parts.**
|
|
114
|
+
`ExpressionEvaluator` no longer mutates the result array with
|
|
115
|
+
`#map!`, which crashed when a script handler returned the frozen
|
|
116
|
+
parts array received from the socket.
|
|
117
|
+
- **README:** fixed broken `if /regex/` examples (no implicit `$_`
|
|
118
|
+
match in `instance_exec`), broken `OMQ.incoming`/`OMQ.outgoing`
|
|
119
|
+
handler table, and `-P` examples that no longer parsed.
|
|
120
|
+
|
|
3
121
|
## 0.12.3 — 2026-04-10
|
|
4
122
|
|
|
5
123
|
### Fixed
|
data/README.md
CHANGED
|
@@ -127,8 +127,8 @@ Pipe creates an internal PULL → eval → PUSH pipeline:
|
|
|
127
127
|
```sh
|
|
128
128
|
omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
|
|
129
129
|
|
|
130
|
-
# with Ractor workers for CPU parallelism
|
|
131
|
-
omq pipe -c@work -c@sink -
|
|
130
|
+
# with Ractor workers for CPU parallelism (-P0 = nproc)
|
|
131
|
+
omq pipe -c@work -c@sink -P0 -r./fib.rb -e 'fib(Integer(it.first)).to_s'
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
The first endpoint is the pull-side (input), the second is the push-side (output).
|
|
@@ -171,10 +171,10 @@ omq pull -b tcp://:5557 -e '|(key, value)| "#{key}=#{value}"'
|
|
|
171
171
|
|
|
172
172
|
```sh
|
|
173
173
|
# skip messages matching a pattern
|
|
174
|
-
omq pull -b tcp://:5557 -e 'next if
|
|
174
|
+
omq pull -b tcp://:5557 -e 'next if it.first.start_with?("#"); it'
|
|
175
175
|
|
|
176
176
|
# stop on "quit"
|
|
177
|
-
omq pull -b tcp://:5557 -e 'break if
|
|
177
|
+
omq pull -b tcp://:5557 -e 'break if it.first == "quit"; it'
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
### BEGIN/END blocks
|
|
@@ -244,8 +244,8 @@ omq req -c tcp://localhost:5555 -r./handler.rb
|
|
|
244
244
|
|
|
245
245
|
| Method | Effect |
|
|
246
246
|
|--------|--------|
|
|
247
|
-
| `OMQ.outgoing {
|
|
248
|
-
| `OMQ.incoming {
|
|
247
|
+
| `OMQ.outgoing { \|msg\| ... }` | Register outgoing message transform |
|
|
248
|
+
| `OMQ.incoming { \|msg\| ... }` | Register incoming message transform |
|
|
249
249
|
|
|
250
250
|
- use explicit block variable (like `msg`) or `it`
|
|
251
251
|
- Setup: use local variables and closures at the top of the script
|
|
@@ -348,8 +348,16 @@ omq pull -b tcp://:5557 -t 5
|
|
|
348
348
|
|
|
349
349
|
## Compression
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
by
|
|
351
|
+
Set `--compress` (`-z`) on either or both sides. The flag enables
|
|
352
|
+
ZMTP-Zstd (provided by `omq-rfc-zstd`), a wire-protocol extension
|
|
353
|
+
that negotiates Zstandard compression during the ZMTP handshake via
|
|
354
|
+
an `X-Compression` READY metadata field. If both peers advertise it,
|
|
355
|
+
each side compresses its outgoing frames; if only one side does, the
|
|
356
|
+
connection stays plaintext (no error). The extension uses the
|
|
357
|
+
auto-trained dictionary mode: the sender feeds the first messages
|
|
358
|
+
into a dictionary trainer, ships the trained dictionary over a
|
|
359
|
+
ZDICT command frame, then switches to dict-bound compression for
|
|
360
|
+
the rest of the connection.
|
|
353
361
|
|
|
354
362
|
```sh
|
|
355
363
|
omq push -c tcp://remote:5557 -z < data.txt
|
|
@@ -441,11 +449,11 @@ Pipe creates an in-process PULL → eval → PUSH pipeline:
|
|
|
441
449
|
# basic pipe (positional: first = input, second = output)
|
|
442
450
|
omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
|
|
443
451
|
|
|
444
|
-
# parallel Ractor workers (
|
|
445
|
-
omq pipe -c@work -c@sink -
|
|
452
|
+
# parallel Ractor workers (-P0 = nproc, also combinable: -P0zvv)
|
|
453
|
+
omq pipe -c@work -c@sink -P0 -r./fib.rb -e 'fib(Integer(it.first)).to_s'
|
|
446
454
|
|
|
447
455
|
# fixed number of workers
|
|
448
|
-
omq pipe -c@work -c@sink -
|
|
456
|
+
omq pipe -c@work -c@sink -P4 -e 'it.map(&:upcase)'
|
|
449
457
|
|
|
450
458
|
# exit when producer disconnects
|
|
451
459
|
omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
|
|
@@ -467,7 +475,7 @@ omq pipe --in -b tcp://:5555 --out -c@sink1 -c@sink2 -e 'it'
|
|
|
467
475
|
omq pipe --in -b tcp://:5555 -b tcp://:5556 --out -c tcp://sink:5557 -e 'it'
|
|
468
476
|
|
|
469
477
|
# parallel workers with fan-in (all must be -c)
|
|
470
|
-
omq pipe --in -c@a -c@b --out -c@sink -
|
|
478
|
+
omq pipe --in -c@a -c@b --out -c@sink -P4 -e 'it'
|
|
471
479
|
```
|
|
472
480
|
|
|
473
481
|
`-P`/`--parallel` requires all endpoints to be `--connect`. In parallel mode, each Ractor worker
|
data/lib/omq/cli/base_runner.rb
CHANGED
|
@@ -15,7 +15,7 @@ module OMQ
|
|
|
15
15
|
def initialize(config, socket_class)
|
|
16
16
|
@config = config
|
|
17
17
|
@klass = socket_class
|
|
18
|
-
@fmt = Formatter.new(config.format
|
|
18
|
+
@fmt = Formatter.new(config.format)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
|
|
@@ -26,13 +26,17 @@ module OMQ
|
|
|
26
26
|
def call(task)
|
|
27
27
|
set_process_title
|
|
28
28
|
setup_socket
|
|
29
|
-
start_event_monitor
|
|
29
|
+
start_event_monitor
|
|
30
30
|
maybe_start_transient_monitor(task)
|
|
31
31
|
sleep(config.delay) if config.delay && config.recv_only?
|
|
32
32
|
wait_for_peer if needs_peer_wait?
|
|
33
33
|
run_begin_blocks
|
|
34
34
|
run_loop(task)
|
|
35
35
|
run_end_blocks
|
|
36
|
+
rescue OMQ::SocketDeadError => error
|
|
37
|
+
reason = error.cause&.message || error.message
|
|
38
|
+
$stderr.write("omq: #{reason}\n")
|
|
39
|
+
exit 1
|
|
36
40
|
ensure
|
|
37
41
|
@sock&.close
|
|
38
42
|
end
|
|
@@ -99,7 +103,7 @@ module OMQ
|
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
def attach_endpoints
|
|
102
|
-
SocketSetup.attach(@sock, config, verbose: config.verbose)
|
|
106
|
+
SocketSetup.attach(@sock, config, verbose: config.verbose, timestamps: config.timestamps)
|
|
103
107
|
end
|
|
104
108
|
|
|
105
109
|
|
|
@@ -314,16 +318,14 @@ module OMQ
|
|
|
314
318
|
def send_msg(parts)
|
|
315
319
|
return if parts.empty?
|
|
316
320
|
parts = [Marshal.dump(parts)] if config.format == :marshal
|
|
317
|
-
parts = @fmt.compress(parts)
|
|
318
321
|
@sock.send(parts)
|
|
319
322
|
transient_ready!
|
|
320
323
|
end
|
|
321
324
|
|
|
322
325
|
|
|
323
326
|
def recv_msg
|
|
324
|
-
|
|
325
|
-
return nil if
|
|
326
|
-
parts = @fmt.decompress(raw)
|
|
327
|
+
parts = @sock.receive
|
|
328
|
+
return nil if parts.nil?
|
|
327
329
|
parts = Marshal.load(parts.first) if config.format == :marshal
|
|
328
330
|
transient_ready!
|
|
329
331
|
parts
|
|
@@ -390,6 +392,10 @@ module OMQ
|
|
|
390
392
|
|
|
391
393
|
def output(parts)
|
|
392
394
|
return if config.quiet || parts.nil?
|
|
395
|
+
# At -vvv, the monitor fiber owns both the trace and the body
|
|
396
|
+
# output (see start_event_monitor). Skipping the app-side write
|
|
397
|
+
# avoids interleaving between the two fibers on a shared tty.
|
|
398
|
+
return if config.verbose >= 3
|
|
393
399
|
$stdout.write(@fmt.encode(parts))
|
|
394
400
|
$stdout.flush
|
|
395
401
|
end
|
|
@@ -462,14 +468,38 @@ module OMQ
|
|
|
462
468
|
end
|
|
463
469
|
|
|
464
470
|
|
|
465
|
-
# -
|
|
466
|
-
#
|
|
467
|
-
#
|
|
471
|
+
# Always attached so protocol-level disconnect events can kill
|
|
472
|
+
# the socket. Verbose gating lives inside the callback:
|
|
473
|
+
# -vv log connect/disconnect/retry/timeout events
|
|
474
|
+
# -vvv also log message sent/received traces
|
|
475
|
+
# --timestamps[=s|ms|us]: prepend UTC timestamps to log lines
|
|
468
476
|
def start_event_monitor
|
|
469
|
-
|
|
470
|
-
|
|
477
|
+
trace = config.verbose >= 3
|
|
478
|
+
log_events = config.verbose >= 2
|
|
479
|
+
@sock.monitor(verbose: trace) do |event|
|
|
480
|
+
Term.write_event(event, config.timestamps) if log_events
|
|
481
|
+
if trace && event.type == :message_received && !config.quiet
|
|
482
|
+
$stdout.write(@fmt.encode(event.detail[:parts]))
|
|
483
|
+
$stdout.flush
|
|
484
|
+
end
|
|
485
|
+
kill_on_protocol_error(event)
|
|
471
486
|
end
|
|
472
487
|
end
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
# omq-cli policy: a peer that commits a protocol-level violation
|
|
491
|
+
# (Protocol::ZMTP::Error — oversized frame, decompression
|
|
492
|
+
# bytebomb, bad framing, …) is almost certainly a
|
|
493
|
+
# misconfiguration the user needs to see. Mark the socket dead
|
|
494
|
+
# so the next receive raises SocketDeadError. The library
|
|
495
|
+
# itself just drops the connection and keeps serving the
|
|
496
|
+
# others; this stricter policy is CLI-only.
|
|
497
|
+
def kill_on_protocol_error(event)
|
|
498
|
+
return unless event.type == :disconnected
|
|
499
|
+
error = event.detail && event.detail[:error]
|
|
500
|
+
return unless error.is_a?(Protocol::ZMTP::Error)
|
|
501
|
+
@sock.engine.signal_fatal_error(error)
|
|
502
|
+
end
|
|
473
503
|
end
|
|
474
504
|
end
|
|
475
505
|
end
|
data/lib/omq/cli/cli_parser.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
|
+
require "etc"
|
|
4
5
|
|
|
5
6
|
module OMQ
|
|
6
7
|
module CLI
|
|
@@ -124,8 +125,12 @@ module OMQ
|
|
|
124
125
|
|
|
125
126
|
-- Compression ----------------------------------------------
|
|
126
127
|
|
|
127
|
-
#
|
|
128
|
-
|
|
128
|
+
# ZMTP-Zstd is negotiated transparently during the handshake.
|
|
129
|
+
# Receive-capable sockets (pull, sub, rep, ...) advertise the
|
|
130
|
+
# profile by default in passive mode: they decode compressed
|
|
131
|
+
# frames from an active sender but never compress their own
|
|
132
|
+
# outgoing frames. Use -z / -Z on the sender to opt it in.
|
|
133
|
+
omq pull --bind tcp://:5557 &
|
|
129
134
|
echo "compressible data" | omq push --connect tcp://localhost:5557 -z
|
|
130
135
|
|
|
131
136
|
-- CURVE Encryption -----------------------------------------
|
|
@@ -224,13 +229,13 @@ module OMQ
|
|
|
224
229
|
rcvbuf: nil,
|
|
225
230
|
conflate: false,
|
|
226
231
|
compress: false,
|
|
227
|
-
|
|
228
|
-
compress_out: false,
|
|
232
|
+
compress_level: nil,
|
|
229
233
|
send_expr: nil,
|
|
230
234
|
recv_expr: nil,
|
|
231
235
|
parallel: nil,
|
|
232
236
|
transient: false,
|
|
233
237
|
verbose: 0,
|
|
238
|
+
timestamps: nil,
|
|
234
239
|
quiet: false,
|
|
235
240
|
echo: false,
|
|
236
241
|
scripts: [],
|
|
@@ -249,6 +254,25 @@ module OMQ
|
|
|
249
254
|
end
|
|
250
255
|
|
|
251
256
|
|
|
257
|
+
# Splits short-option clusters of the form `-P[digits][letters]`
|
|
258
|
+
# so OptionParser sees `-P[digits]` followed by `-[letters]`.
|
|
259
|
+
# Lets `-P0zvv` mean `-P0 -z -v -v` (portable & combinable).
|
|
260
|
+
# Also rewrites bare `--timestamps` to `--timestamps=ms` so
|
|
261
|
+
# OptionParser doesn't consume the next positional token as its
|
|
262
|
+
# argument.
|
|
263
|
+
#
|
|
264
|
+
def split_parallel_cluster(argv)
|
|
265
|
+
argv.flat_map { |a|
|
|
266
|
+
if a =~ /\A-P(\d*)([a-zA-Z].*)\z/
|
|
267
|
+
n, rest = $1, $2
|
|
268
|
+
n.empty? ? ["-P", "-#{rest}"] : ["-P#{n}", "-#{rest}"]
|
|
269
|
+
else
|
|
270
|
+
a
|
|
271
|
+
end
|
|
272
|
+
}.map { |a| a == "--timestamps" ? "--timestamps=ms" : a }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
|
|
252
276
|
# Validates option combinations, aborting on bad combos.
|
|
253
277
|
#
|
|
254
278
|
def self.validate!(opts)
|
|
@@ -365,16 +389,17 @@ module OMQ
|
|
|
365
389
|
o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
|
|
366
390
|
|
|
367
391
|
o.separator "\nCompression:"
|
|
368
|
-
o.on("-z", "
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
392
|
+
o.on("-z", "Zstd compression (level -3, fast)") do
|
|
393
|
+
opts[:compress] = true
|
|
394
|
+
opts[:compress_level] = -3
|
|
395
|
+
end
|
|
396
|
+
o.on("-Z", "Zstd compression (level 3, better ratio)") do
|
|
397
|
+
opts[:compress] = true
|
|
398
|
+
opts[:compress_level] = 3
|
|
399
|
+
end
|
|
400
|
+
o.on("--compress=LEVEL", Integer, "Zstd compression with custom level (e.g. 19, -1)") do |v|
|
|
401
|
+
opts[:compress] = true
|
|
402
|
+
opts[:compress_level] = v
|
|
378
403
|
end
|
|
379
404
|
|
|
380
405
|
o.separator "\nProcessing (-e = incoming, -E = outgoing):"
|
|
@@ -384,8 +409,9 @@ module OMQ
|
|
|
384
409
|
require "omq" unless defined?(OMQ::VERSION)
|
|
385
410
|
opts[:scripts] << (v == "-" ? :stdin : (v.start_with?("./", "../") ? File.expand_path(v) : v))
|
|
386
411
|
}
|
|
387
|
-
o.on("-P", "--parallel N", Integer, "Parallel Ractor workers (max 16)") { |v|
|
|
388
|
-
|
|
412
|
+
o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (0 = nproc, max 16)") { |v|
|
|
413
|
+
n = v.nil? || v.zero? ? Etc.nprocessors : v
|
|
414
|
+
opts[:parallel] = [n, 16].min
|
|
389
415
|
}
|
|
390
416
|
|
|
391
417
|
o.separator "\nCURVE encryption (requires system libsodium):"
|
|
@@ -397,7 +423,10 @@ module OMQ
|
|
|
397
423
|
o.separator " OMQ_CRYPTO (backend: rbnacl or nuckle)"
|
|
398
424
|
|
|
399
425
|
o.separator "\nOther:"
|
|
400
|
-
o.on("-v", "--verbose", "Verbosity: -v endpoints, -vv events, -vvv messages
|
|
426
|
+
o.on("-v", "--verbose", "Verbosity: -v endpoints, -vv events, -vvv messages") { opts[:verbose] += 1 }
|
|
427
|
+
o.on( "--timestamps PRECISION", %w[s ms us], "Prefix log lines with UTC timestamp (s/ms/us, default ms)") { |v|
|
|
428
|
+
opts[:timestamps] = v.to_sym
|
|
429
|
+
}
|
|
401
430
|
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
402
431
|
o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
|
|
403
432
|
o.on( "--ffi", "Use libzmq FFI backend (requires omq-ffi gem + system libzmq 4.x)") do
|
|
@@ -427,6 +456,8 @@ module OMQ
|
|
|
427
456
|
o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
|
|
428
457
|
end
|
|
429
458
|
|
|
459
|
+
argv = split_parallel_cluster(argv)
|
|
460
|
+
|
|
430
461
|
begin
|
|
431
462
|
parser.parse!(argv)
|
|
432
463
|
rescue OptionParser::ParseError => e
|
|
@@ -443,26 +474,9 @@ module OMQ
|
|
|
443
474
|
opts[:type_name] = type_name.downcase
|
|
444
475
|
end
|
|
445
476
|
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
# tcp://*:PORT → 0.0.0.0 (all interfaces, IPv4)
|
|
450
|
-
# tcp://0.0.0.0:PORT, tcp://[::]:PORT → pass through
|
|
451
|
-
#
|
|
452
|
-
# Connects: tcp://:PORT → localhost (Happy Eyeballs)
|
|
453
|
-
# tcp://*:PORT → localhost
|
|
454
|
-
#
|
|
455
|
-
# The hang on macOS (IPv6 connect(2) not getting ECONNREFUSED via
|
|
456
|
-
# kqueue) is fixed by the connect timeout in Engine::Reconnect.
|
|
457
|
-
loopback = self.class.loopback_bind_host
|
|
458
|
-
normalize_bind = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://#{loopback}:") }
|
|
459
|
-
normalize_connect = ->(url) { url.sub(%r{\Atcp://(\*|):}, "tcp://localhost:") }
|
|
460
|
-
normalize_ep = ->(ep) { Endpoint.new(ep.bind? ? normalize_bind.call(ep.url) : normalize_connect.call(ep.url), ep.bind?) }
|
|
461
|
-
opts[:binds].map!(&normalize_bind)
|
|
462
|
-
opts[:connects].map!(&normalize_connect)
|
|
463
|
-
opts[:endpoints].map!(&normalize_ep)
|
|
464
|
-
opts[:in_endpoints].map!(&normalize_ep)
|
|
465
|
-
opts[:out_endpoints].map!(&normalize_ep)
|
|
477
|
+
# Host shorthand (tcp://*:PORT, tcp://:PORT, tcp://localhost:PORT)
|
|
478
|
+
# is normalized inside OMQ::Transport::TCP — see its
|
|
479
|
+
# #normalize_bind_host / #normalize_connect_host / #loopback_host.
|
|
466
480
|
|
|
467
481
|
opts
|
|
468
482
|
end
|
|
@@ -565,19 +579,6 @@ module OMQ
|
|
|
565
579
|
end
|
|
566
580
|
|
|
567
581
|
|
|
568
|
-
# Returns the loopback address for bind normalization.
|
|
569
|
-
# Prefers IPv6 loopback ([::1]) when the host has at least one
|
|
570
|
-
# non-loopback, non-link-local IPv6 address, otherwise 127.0.0.1.
|
|
571
|
-
#
|
|
572
|
-
def self.loopback_bind_host
|
|
573
|
-
@loopback_bind_host ||= begin
|
|
574
|
-
has_ipv6 = ::Socket.getifaddrs.any? { |ifa|
|
|
575
|
-
addr = ifa.addr
|
|
576
|
-
addr&.ipv6? && !addr.ipv6_loopback? && !addr.ipv6_linklocal?
|
|
577
|
-
}
|
|
578
|
-
has_ipv6 ? "[::1]" : "127.0.0.1"
|
|
579
|
-
end
|
|
580
|
-
end
|
|
581
582
|
end
|
|
582
583
|
end
|
|
583
584
|
end
|
|
@@ -25,8 +25,7 @@ module OMQ
|
|
|
25
25
|
parts = recv_msg_raw
|
|
26
26
|
break if parts.nil?
|
|
27
27
|
routing_id = parts.shift
|
|
28
|
-
|
|
29
|
-
break unless handle_server_request(routing_id, body)
|
|
28
|
+
break unless handle_server_request(routing_id, parts)
|
|
30
29
|
i += 1
|
|
31
30
|
break if n && n > 0 && i >= n
|
|
32
31
|
end
|
|
@@ -37,15 +36,15 @@ module OMQ
|
|
|
37
36
|
if config.recv_expr || @recv_eval_proc
|
|
38
37
|
reply = eval_recv_expr(body)
|
|
39
38
|
output([display_routing_id(routing_id), *(reply || [""])])
|
|
40
|
-
@sock.send_to(routing_id,
|
|
39
|
+
@sock.send_to(routing_id, (reply || [""]).first)
|
|
41
40
|
elsif config.echo
|
|
42
41
|
output([display_routing_id(routing_id), *body])
|
|
43
|
-
@sock.send_to(routing_id,
|
|
42
|
+
@sock.send_to(routing_id, body.first || "")
|
|
44
43
|
elsif config.data || config.file || !config.stdin_is_tty
|
|
45
44
|
reply = read_next
|
|
46
45
|
return false unless reply
|
|
47
46
|
output([display_routing_id(routing_id), *body])
|
|
48
|
-
@sock.send_to(routing_id,
|
|
47
|
+
@sock.send_to(routing_id, reply.first || "")
|
|
49
48
|
end
|
|
50
49
|
true
|
|
51
50
|
end
|
|
@@ -66,7 +65,6 @@ module OMQ
|
|
|
66
65
|
parts = recv_msg_raw
|
|
67
66
|
break if parts.nil?
|
|
68
67
|
routing_id = parts.shift
|
|
69
|
-
parts = @fmt.decompress(parts)
|
|
70
68
|
result = eval_recv_expr([display_routing_id(routing_id), *parts])
|
|
71
69
|
output(result)
|
|
72
70
|
i += 1
|
|
@@ -77,7 +75,7 @@ module OMQ
|
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
def send_to_peer(id, parts)
|
|
80
|
-
@sock.send_to(id,
|
|
78
|
+
@sock.send_to(id, parts.first || "")
|
|
81
79
|
end
|
|
82
80
|
end
|
|
83
81
|
end
|
data/lib/omq/cli/config.rb
CHANGED
|
@@ -49,20 +49,23 @@ module OMQ
|
|
|
49
49
|
return [result] if @format == :marshal
|
|
50
50
|
|
|
51
51
|
result = result.is_a?(Array) ? result : [result]
|
|
52
|
-
result.map
|
|
52
|
+
result.map(&:to_s)
|
|
53
53
|
rescue => e
|
|
54
54
|
$stderr.puts "omq: eval error: #{e.message} (#{e.class})"
|
|
55
55
|
exit 3
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
# Normalises an eval result to nil (skip) or an Array
|
|
59
|
+
# Normalises an eval result to nil (skip) or an Array.
|
|
60
60
|
# Used inside Ractor worker blocks where instance methods are unavailable.
|
|
61
|
+
# When +format+ is :marshal, arbitrary objects are preserved (wrapped
|
|
62
|
+
# in a one-element Array so the wire path can Marshal.dump them).
|
|
61
63
|
#
|
|
62
|
-
def self.normalize_result(result)
|
|
64
|
+
def self.normalize_result(result, format: nil)
|
|
63
65
|
return nil if result.nil?
|
|
66
|
+
return [result] if format == :marshal
|
|
64
67
|
result = result.is_a?(Array) ? result : [result]
|
|
65
|
-
result.map
|
|
68
|
+
result.map(&:to_s)
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
|
data/lib/omq/cli/formatter.rb
CHANGED
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
module CLI
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
#
|
|
9
|
-
# plus optional LZ4 compression.
|
|
5
|
+
# Handles encoding/decoding messages in the configured format.
|
|
6
|
+
# Compression is handled below the application API by ZMTP-Zstd
|
|
7
|
+
# (omq-rfc-zstd) once enabled via +socket.compression=+; the
|
|
8
|
+
# formatter sees plaintext frames in both directions.
|
|
10
9
|
class Formatter
|
|
11
10
|
# @param format [Symbol] wire format (:ascii, :quoted, :raw, :jsonl, :msgpack, :marshal)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@format = format
|
|
15
|
-
@compress = compress
|
|
11
|
+
def initialize(format)
|
|
12
|
+
@format = format
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
|
|
@@ -83,37 +80,20 @@ module OMQ
|
|
|
83
80
|
end
|
|
84
81
|
|
|
85
82
|
|
|
86
|
-
# Compresses each frame with LZ4 if compression is enabled.
|
|
87
|
-
#
|
|
88
|
-
# @param parts [Array<String>] message frames
|
|
89
|
-
# @return [Array<String>] optionally compressed frames
|
|
90
|
-
def compress(parts)
|
|
91
|
-
@compress ? parts.map { |p| RLZ4.compress(p) if p } : parts
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# Decompresses each frame with LZ4 if compression is enabled.
|
|
96
|
-
# nil/empty frames pass through — they were nil before send coercion.
|
|
97
|
-
#
|
|
98
|
-
# @param parts [Array<String>] possibly compressed message frames
|
|
99
|
-
# @return [Array<String>] decompressed frames
|
|
100
|
-
def decompress(parts)
|
|
101
|
-
@compress ? parts.map { |p| p && !p.empty? ? RLZ4.decompress(p) : p } : parts
|
|
102
|
-
rescue RLZ4::DecompressError
|
|
103
|
-
raise DecompressError, "decompression failed (did the sender use --compress?)"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
|
|
107
83
|
# Formats message parts for human-readable preview (logging).
|
|
84
|
+
# When +wire_size+ is given (ZMTP-Zstd negotiated), the header
|
|
85
|
+
# also shows the compressed on-the-wire size: "(29B wire=12B)".
|
|
108
86
|
#
|
|
109
|
-
# @param parts [Array<String>] message frames
|
|
87
|
+
# @param parts [Array<String>] plaintext message frames
|
|
88
|
+
# @param wire_size [Integer, nil] compressed bytes on the wire
|
|
110
89
|
# @return [String] truncated preview of each frame joined by |
|
|
111
|
-
def self.preview(parts)
|
|
90
|
+
def self.preview(parts, wire_size: nil)
|
|
112
91
|
total = parts.sum(&:bytesize)
|
|
113
92
|
nparts = parts.size
|
|
114
93
|
shown = parts.first(3).map { |p| preview_frame(p) }
|
|
115
|
-
tail = nparts > 3 ? "
|
|
116
|
-
|
|
94
|
+
tail = nparts > 3 ? "|…" : ""
|
|
95
|
+
size = wire_size ? "#{total}B wire=#{wire_size}B" : "#{total}B"
|
|
96
|
+
header = nparts > 1 ? "(#{size} #{nparts}F)" : "(#{size})"
|
|
117
97
|
|
|
118
98
|
"#{header} #{shown.join("|")}#{tail}"
|
|
119
99
|
end
|
|
@@ -133,7 +113,7 @@ module OMQ
|
|
|
133
113
|
if printable < sample.bytesize / 2
|
|
134
114
|
"[#{bytes.bytesize}B]"
|
|
135
115
|
elsif bytes.bytesize > 12
|
|
136
|
-
"#{sample.gsub(/[^[:print:]]/, ".")}
|
|
116
|
+
"#{sample.gsub(/[^[:print:]]/, ".")}…"
|
|
137
117
|
else
|
|
138
118
|
sample.gsub(/[^[:print:]]/, ".")
|
|
139
119
|
end
|