omq-cli 0.11.4 → 0.12.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 +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +41 -29
- data/lib/omq/cli/cli_parser.rb +60 -38
- data/lib/omq/cli/expression_evaluator.rb +7 -12
- data/lib/omq/cli/formatter.rb +10 -7
- data/lib/omq/cli/pipe.rb +12 -3
- data/lib/omq/cli/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24cc89ae4c000ff153c8277b140b6ebf58cdcb4b9c7ad73acfc86edb35341ade
|
|
4
|
+
data.tar.gz: 0ab5cd61e2cb7cba892eeb7fa92761d2dd057d0e3ffe5ae217d88e5dd222cf43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b4857e0c3388e32a24e1b415132266af442308494c569a7c5441c8b0cff87a8c5e872cda9f48568a3416e9f5030a6dab66fdc309212000f6c00cb293b5a3925
|
|
7
|
+
data.tar.gz: 6de453e861b990520665178dbe0066e8b18372dc224985cc3f66b3356d0d172a0fd02a2286e70885c92ee2acd9e61eb809bc607e8079ae2565d41a79cb11ea6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.1 — 2026-04-10
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **`it` replaces `$F` and `$_` in eval expressions.** The `-e`/`-E`
|
|
8
|
+
message parts variable is now Ruby's default block variable `it`
|
|
9
|
+
instead of the `$F` global. `$_` is removed — use `it.first` instead.
|
|
10
|
+
This also simplifies Ractor worker compilation by removing the
|
|
11
|
+
`$F` → `__F` rewrite.
|
|
12
|
+
|
|
13
|
+
- **Block parameter syntax in `-e`/`-E` expressions.** Expressions can
|
|
14
|
+
now declare parameters like Ruby blocks: `-e '|msg| msg.map(&:upcase)'`.
|
|
15
|
+
A single parameter receives the whole parts array. Use `|(a, b)|` for
|
|
16
|
+
destructuring.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`@name` endpoint shorthand.** `-c@work` and `-b@sink` expand to
|
|
21
|
+
`ipc://@work` and `ipc://@sink` (Linux abstract namespace). Only
|
|
22
|
+
triggers when the value starts with `@` and has no `://` scheme.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Flaky FFI backend tests.** Wait for both REP peers to be connected
|
|
27
|
+
before round-robining requests, instead of only waiting for the first.
|
|
28
|
+
|
|
29
|
+
- **Improved verbose preview format.** Empty frames render as `''`
|
|
30
|
+
instead of `[0B]`. Multipart messages show frame count: `(18B 4F)`.
|
|
31
|
+
|
|
32
|
+
- **Compression skips nil/empty frames.** `compress` passes `nil` parts
|
|
33
|
+
through (coerced to empty frames by the socket layer). `decompress`
|
|
34
|
+
skips empty frames instead of feeding them to LZ4.
|
|
35
|
+
|
|
36
|
+
- **Pipe `--out` without `--in` promotes bare endpoints.** Bare `-c`/`-b`
|
|
37
|
+
before `--out` are now treated as `--in` endpoints (and vice versa),
|
|
38
|
+
fixing `pipe -c SRC --out -c DST` which previously errored.
|
|
39
|
+
|
|
40
|
+
- **Pipe fan-out distributes across output peers.** Multi-output pipes
|
|
41
|
+
now yield after each send, giving send-pump fibers a turn to drain the
|
|
42
|
+
shared queue. Without this, one pump monopolized the queue via
|
|
43
|
+
`drain_send_queue_capped` when messages arrived in bursts.
|
|
44
|
+
|
|
45
|
+
- **Pipe waits for all output peers with `--timeout`.** `wait_for_peers_with_timeout`
|
|
46
|
+
now waits for `connection_count >= out_eps.size` instead of just the first peer.
|
|
47
|
+
|
|
3
48
|
## 0.11.4 — 2026-04-10
|
|
4
49
|
|
|
5
50
|
### Fixed
|
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ omq rep -b tcp://:5555 --echo
|
|
|
25
25
|
echo "hello" | omq req -c tcp://localhost:5555
|
|
26
26
|
|
|
27
27
|
# Upcase server — -e evals Ruby on each incoming message
|
|
28
|
-
omq rep -b tcp://:5555 -e '
|
|
28
|
+
omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
```
|
|
@@ -44,7 +44,7 @@ Every socket needs at least one `--bind` or `--connect`:
|
|
|
44
44
|
omq pull --bind tcp://:5557 # listen on port 5557
|
|
45
45
|
omq push --connect tcp://host:5557 # connect to host
|
|
46
46
|
omq pull -b ipc:///tmp/feed.sock # IPC (unix socket)
|
|
47
|
-
omq push -c
|
|
47
|
+
omq push -c@work # IPC abstract namespace (@name → ipc://@name)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Multiple endpoints are allowed — `omq pull -b tcp://:5557 -b tcp://:5558` binds both.
|
|
@@ -82,7 +82,7 @@ omq pull -b tcp://:5557
|
|
|
82
82
|
omq rep -b tcp://:5555 --echo
|
|
83
83
|
|
|
84
84
|
# upcase server
|
|
85
|
-
omq rep -b tcp://:5555 -e '
|
|
85
|
+
omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
|
|
86
86
|
|
|
87
87
|
# client
|
|
88
88
|
echo "hello" | omq req -c tcp://localhost:5555
|
|
@@ -115,7 +115,7 @@ omq router -b tcp://:5555
|
|
|
115
115
|
omq router -b tcp://:5555 --target worker-1 -D "reply"
|
|
116
116
|
|
|
117
117
|
# dynamic routing via send-eval (first element = identity)
|
|
118
|
-
omq router -b tcp://:5555 -E '["worker-1",
|
|
118
|
+
omq router -b tcp://:5555 -E '["worker-1", it.first.upcase]'
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
`--target` and `--send-eval` are mutually exclusive on routing sockets.
|
|
@@ -125,10 +125,10 @@ omq router -b tcp://:5555 -E '["worker-1", $_.upcase]'
|
|
|
125
125
|
Pipe creates an internal PULL → eval → PUSH pipeline:
|
|
126
126
|
|
|
127
127
|
```sh
|
|
128
|
-
omq pipe -c
|
|
128
|
+
omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
|
|
129
129
|
|
|
130
130
|
# with Ractor workers for CPU parallelism
|
|
131
|
-
omq pipe -c
|
|
131
|
+
omq pipe -c@work -c@sink -P 4 -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).
|
|
@@ -139,12 +139,24 @@ Both must use `-c`.
|
|
|
139
139
|
`-e` (alias `--recv-eval`) runs a Ruby expression for each **incoming** message.
|
|
140
140
|
`-E` (alias `--send-eval`) runs a Ruby expression for each **outgoing** message.
|
|
141
141
|
|
|
142
|
-
###
|
|
142
|
+
### Variables
|
|
143
143
|
|
|
144
144
|
| Variable | Value |
|
|
145
145
|
|----------|-------|
|
|
146
|
-
|
|
|
147
|
-
|
|
146
|
+
| `it` | Message parts (`Array<String>`) — Ruby's default block variable |
|
|
147
|
+
|
|
148
|
+
### Block parameters
|
|
149
|
+
|
|
150
|
+
Expressions support Ruby block parameter syntax. A single parameter receives
|
|
151
|
+
the whole parts array; use `|(a, b)|` to destructure:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
# single param = parts array
|
|
155
|
+
omq pull -b tcp://:5557 -e '|msg| msg.map(&:upcase)'
|
|
156
|
+
|
|
157
|
+
# destructure multipart messages
|
|
158
|
+
omq pull -b tcp://:5557 -e '|(key, value)| "#{key}=#{value}"'
|
|
159
|
+
```
|
|
148
160
|
|
|
149
161
|
### Return value
|
|
150
162
|
|
|
@@ -159,10 +171,10 @@ Both must use `-c`.
|
|
|
159
171
|
|
|
160
172
|
```sh
|
|
161
173
|
# skip messages matching a pattern
|
|
162
|
-
omq pull -b tcp://:5557 -e 'next if /^#/;
|
|
174
|
+
omq pull -b tcp://:5557 -e 'next if /^#/; it'
|
|
163
175
|
|
|
164
176
|
# stop on "quit"
|
|
165
|
-
omq pull -b tcp://:5557 -e 'break if /quit/;
|
|
177
|
+
omq pull -b tcp://:5557 -e 'break if /quit/; it'
|
|
166
178
|
```
|
|
167
179
|
|
|
168
180
|
### BEGIN/END blocks
|
|
@@ -170,7 +182,7 @@ omq pull -b tcp://:5557 -e 'break if /quit/; $F'
|
|
|
170
182
|
Like awk — `BEGIN{}` runs once before the message loop, `END{}` runs after:
|
|
171
183
|
|
|
172
184
|
```sh
|
|
173
|
-
omq pull -b tcp://:5557 -e 'BEGIN{ @sum = 0 } @sum += Integer(
|
|
185
|
+
omq pull -b tcp://:5557 -e 'BEGIN{ @sum = 0 } @sum += Integer(it.first); next END{ puts @sum }'
|
|
174
186
|
```
|
|
175
187
|
|
|
176
188
|
Local variables won't work to share state between the blocks. Use `@ivars` instead.
|
|
@@ -191,23 +203,23 @@ Local variables won't work to share state between the blocks. Use `@ivars` inste
|
|
|
191
203
|
|
|
192
204
|
```sh
|
|
193
205
|
# upcase echo server
|
|
194
|
-
omq rep -b tcp://:5555 -e '
|
|
206
|
+
omq rep -b tcp://:5555 -e 'it.map(&:upcase)'
|
|
195
207
|
|
|
196
208
|
# transform before sending
|
|
197
|
-
echo hello | omq push -c tcp://localhost:5557 -E '
|
|
209
|
+
echo hello | omq push -c tcp://localhost:5557 -E 'it.map(&:upcase)'
|
|
198
210
|
|
|
199
211
|
# filter incoming
|
|
200
|
-
omq pull -b tcp://:5557 -e '
|
|
212
|
+
omq pull -b tcp://:5557 -e 'it.first.include?("error") ? it : nil'
|
|
201
213
|
|
|
202
214
|
# REQ: different transforms per direction
|
|
203
215
|
echo hello | omq req -c tcp://localhost:5555 \
|
|
204
|
-
-E '
|
|
216
|
+
-E 'it.map(&:upcase)' -e 'it.map(&:reverse)'
|
|
205
217
|
|
|
206
218
|
# generate messages without stdin
|
|
207
219
|
omq pub -c tcp://localhost:5556 -E 'Time.now.to_s' -i 1
|
|
208
220
|
|
|
209
221
|
# use gems
|
|
210
|
-
omq sub -c tcp://localhost:5556 -s "" -rjson -e 'JSON.parse(
|
|
222
|
+
omq sub -c tcp://localhost:5556 -s "" -rjson -e 'JSON.parse(it.first)["temperature"]'
|
|
211
223
|
```
|
|
212
224
|
|
|
213
225
|
## Script handlers (-r)
|
|
@@ -235,7 +247,7 @@ omq req -c tcp://localhost:5555 -r./handler.rb
|
|
|
235
247
|
| `OMQ.outgoing { |msg| ... }` | Register outgoing message transform |
|
|
236
248
|
| `OMQ.incoming { |msg| ... }` | Register incoming message transform |
|
|
237
249
|
|
|
238
|
-
- use explicit block variable (like `msg`)
|
|
250
|
+
- use explicit block variable (like `msg`) or `it`
|
|
239
251
|
- Setup: use local variables and closures at the top of the script
|
|
240
252
|
- Teardown: use Ruby's `at_exit { ... }`
|
|
241
253
|
- CLI flags (`-e`/`-E`) override script-registered handlers for the same direction
|
|
@@ -243,7 +255,7 @@ omq req -c tcp://localhost:5555 -r./handler.rb
|
|
|
243
255
|
|
|
244
256
|
```sh
|
|
245
257
|
# handler.rb registers recv_eval, CLI adds send_eval
|
|
246
|
-
omq req -c tcp://localhost:5555 -r./handler.rb -E '
|
|
258
|
+
omq req -c tcp://localhost:5555 -r./handler.rb -E 'it.map(&:upcase)'
|
|
247
259
|
```
|
|
248
260
|
|
|
249
261
|
### Script handler examples
|
|
@@ -415,7 +427,7 @@ omq router -b tcp://:5555
|
|
|
415
427
|
omq router -b tcp://:5555 --target worker-1 -D "reply"
|
|
416
428
|
|
|
417
429
|
# ROUTER dynamic routing via -E (first element = routing identity)
|
|
418
|
-
omq router -b tcp://:5555 -E '["worker-1",
|
|
430
|
+
omq router -b tcp://:5555 -E '["worker-1", it.first.upcase]'
|
|
419
431
|
|
|
420
432
|
# binary routing IDs (0x prefix)
|
|
421
433
|
omq router -b tcp://:5555 --target 0xdeadbeef -D "reply"
|
|
@@ -427,16 +439,16 @@ Pipe creates an in-process PULL → eval → PUSH pipeline:
|
|
|
427
439
|
|
|
428
440
|
```sh
|
|
429
441
|
# basic pipe (positional: first = input, second = output)
|
|
430
|
-
omq pipe -c
|
|
442
|
+
omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
|
|
431
443
|
|
|
432
444
|
# parallel Ractor workers (default: all CPUs)
|
|
433
|
-
omq pipe -c
|
|
445
|
+
omq pipe -c@work -c@sink -P -r./fib.rb -e 'fib(Integer(it.first)).to_s'
|
|
434
446
|
|
|
435
447
|
# fixed number of workers
|
|
436
|
-
omq pipe -c
|
|
448
|
+
omq pipe -c@work -c@sink -P 4 -e 'it.map(&:upcase)'
|
|
437
449
|
|
|
438
450
|
# exit when producer disconnects
|
|
439
|
-
omq pipe -c
|
|
451
|
+
omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
|
|
440
452
|
```
|
|
441
453
|
|
|
442
454
|
### Multi-peer pipe with `--in`/`--out`
|
|
@@ -446,16 +458,16 @@ Use `--in` and `--out` to attach multiple endpoints per side. These are modal sw
|
|
|
446
458
|
|
|
447
459
|
```sh
|
|
448
460
|
# fan-in: 2 producers → 1 consumer
|
|
449
|
-
omq pipe --in -c
|
|
461
|
+
omq pipe --in -c@work1 -c@work2 --out -c@sink -e 'it'
|
|
450
462
|
|
|
451
463
|
# fan-out: 1 producer → 2 consumers (round-robin)
|
|
452
|
-
omq pipe --in -b tcp://:5555 --out -c
|
|
464
|
+
omq pipe --in -b tcp://:5555 --out -c@sink1 -c@sink2 -e 'it'
|
|
453
465
|
|
|
454
466
|
# bind on input, connect on output
|
|
455
|
-
omq pipe --in -b tcp://:5555 -b tcp://:5556 --out -c tcp://sink:5557 -e '
|
|
467
|
+
omq pipe --in -b tcp://:5555 -b tcp://:5556 --out -c tcp://sink:5557 -e 'it'
|
|
456
468
|
|
|
457
469
|
# parallel workers with fan-in (all must be -c)
|
|
458
|
-
omq pipe --in -c
|
|
470
|
+
omq pipe --in -c@a -c@b --out -c@sink -P 4 -e 'it'
|
|
459
471
|
```
|
|
460
472
|
|
|
461
473
|
`-P`/`--parallel` requires all endpoints to be `--connect`. In parallel mode, each Ractor worker
|
|
@@ -467,7 +479,7 @@ gets its own PULL/PUSH pair connecting to all endpoints.
|
|
|
467
479
|
|
|
468
480
|
```sh
|
|
469
481
|
# worker exits when producer is done
|
|
470
|
-
omq pipe -c
|
|
482
|
+
omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
|
|
471
483
|
|
|
472
484
|
# sink exits when all workers disconnect
|
|
473
485
|
omq pull -b tcp://:5557 --transient
|
data/lib/omq/cli/cli_parser.rb
CHANGED
|
@@ -10,13 +10,13 @@ module OMQ
|
|
|
10
10
|
EXAMPLES = <<~'TEXT'
|
|
11
11
|
-- Request / Reply ------------------------------------------
|
|
12
12
|
|
|
13
|
-
+-----+
|
|
13
|
+
+-----+ "hello" +-----+
|
|
14
14
|
| REQ |------------->| REP |
|
|
15
15
|
| |<-------------| |
|
|
16
|
-
+-----+
|
|
16
|
+
+-----+ "HELLO" +-----+
|
|
17
17
|
|
|
18
18
|
# terminal 1: echo server
|
|
19
|
-
omq rep --bind tcp://:5555 --recv-eval '
|
|
19
|
+
omq rep --bind tcp://:5555 --recv-eval 'it.map(&:upcase)'
|
|
20
20
|
|
|
21
21
|
# terminal 2: send a request
|
|
22
22
|
echo "hello" | omq req --connect tcp://localhost:5555
|
|
@@ -28,7 +28,7 @@ module OMQ
|
|
|
28
28
|
-- Publish / Subscribe --------------------------------------
|
|
29
29
|
|
|
30
30
|
+-----+ "weather.nyc 72F" +-----+
|
|
31
|
-
| PUB
|
|
31
|
+
| PUB |-------------------->| SUB | --subscribe "weather."
|
|
32
32
|
+-----+ +-----+
|
|
33
33
|
|
|
34
34
|
# terminal 1: subscriber (all topics by default)
|
|
@@ -39,25 +39,23 @@ module OMQ
|
|
|
39
39
|
|
|
40
40
|
-- Periodic Publish -------------------------------------------
|
|
41
41
|
|
|
42
|
-
+-----+
|
|
42
|
+
+-----+ "tick 1" +-----+
|
|
43
43
|
| PUB |--(every 1s)-->| SUB |
|
|
44
|
-
+-----+
|
|
44
|
+
+-----+ +-----+
|
|
45
45
|
|
|
46
46
|
# terminal 1: subscriber
|
|
47
47
|
omq sub --bind tcp://:5556
|
|
48
48
|
|
|
49
49
|
# terminal 2: publish a tick every second (wall-clock aligned)
|
|
50
|
-
omq pub --connect tcp://localhost:5556 --delay 1
|
|
51
|
-
--data "tick" --interval 1
|
|
50
|
+
omq pub --connect tcp://localhost:5556 --delay 1 --data "tick" --interval 1
|
|
52
51
|
|
|
53
52
|
# 5 ticks, then exit
|
|
54
|
-
omq pub --connect tcp://localhost:5556 --
|
|
55
|
-
--data "tick" --interval 1 --count 5
|
|
53
|
+
omq pub --connect tcp://localhost:5556 -d1 -D "tick" -i0.5 --count 5
|
|
56
54
|
|
|
57
55
|
-- Pipeline -------------------------------------------------
|
|
58
56
|
|
|
59
57
|
+------+ +------+
|
|
60
|
-
| PUSH
|
|
58
|
+
| PUSH |---------->| PULL |
|
|
61
59
|
+------+ +------+
|
|
62
60
|
|
|
63
61
|
# terminal 1: worker
|
|
@@ -73,39 +71,38 @@ module OMQ
|
|
|
73
71
|
-- Pipe (PULL -> eval -> PUSH) --------------------------------
|
|
74
72
|
|
|
75
73
|
+------+ +------+ +------+
|
|
76
|
-
| PUSH
|
|
74
|
+
| PUSH |-------->| pipe |-------->| PULL |
|
|
77
75
|
+------+ +------+ +------+
|
|
78
76
|
|
|
79
77
|
# terminal 1: producer
|
|
80
|
-
echo -e "hello\nworld" | omq push
|
|
78
|
+
echo -e "hello\nworld" | omq push -b@work
|
|
81
79
|
|
|
82
80
|
# terminal 2: worker -- uppercase each message
|
|
83
|
-
omq pipe -c
|
|
81
|
+
omq pipe -c@work -c@sink -e 'it.map(&:upcase)'
|
|
84
82
|
# terminal 3: collector
|
|
85
|
-
omq pull
|
|
83
|
+
omq pull -b@sink
|
|
86
84
|
|
|
87
85
|
# 4 Ractor workers in a single process (-P)
|
|
88
|
-
omq pipe -c
|
|
86
|
+
omq pipe -c@work -c@sink -P4 -r./fib -e 'fib(it.first.to_i).to_s'
|
|
89
87
|
|
|
90
88
|
# exit when producer disconnects (--transient)
|
|
91
|
-
omq pipe -c
|
|
89
|
+
omq pipe -c@work -c@sink --transient -e 'it.map(&:upcase)'
|
|
92
90
|
|
|
93
91
|
# fan-in: multiple sources -> one sink
|
|
94
|
-
omq pipe --in -c
|
|
95
|
-
--out -c ipc://@sink -e '$F.map(&:upcase)'
|
|
92
|
+
omq pipe --in -c@work1 -c@work2 --out -c@sink -e 'it.map(&:upcase)'
|
|
96
93
|
|
|
97
94
|
# fan-out: one source -> multiple sinks (round-robin)
|
|
98
|
-
omq pipe --in -b tcp://:5555 --out -c
|
|
95
|
+
omq pipe --in -b tcp://:5555 --out -c@sink1 -c@sink2 -e 'it'
|
|
99
96
|
|
|
100
97
|
-- CLIENT / SERVER (draft) ----------------------------------
|
|
101
98
|
|
|
102
|
-
+--------+
|
|
103
|
-
| CLIENT |------------>| SERVER | --recv-eval '
|
|
99
|
+
+--------+ "hello" +--------+
|
|
100
|
+
| CLIENT |------------>| SERVER | --recv-eval 'it.map(&:upcase)'
|
|
104
101
|
| |<------------| |
|
|
105
|
-
+--------+
|
|
102
|
+
+--------+ "HELLO" +--------+
|
|
106
103
|
|
|
107
104
|
# terminal 1: upcasing server
|
|
108
|
-
omq server --bind tcp://:5555 --recv-eval '
|
|
105
|
+
omq server --bind tcp://:5555 --recv-eval 'it.map(&:upcase)'
|
|
109
106
|
|
|
110
107
|
# terminal 2: client
|
|
111
108
|
echo "hello" | omq client --connect tcp://localhost:5555
|
|
@@ -127,9 +124,9 @@ module OMQ
|
|
|
127
124
|
|
|
128
125
|
-- Compression ----------------------------------------------
|
|
129
126
|
|
|
130
|
-
# both sides must use --compress
|
|
127
|
+
# both sides must use --compress/-z
|
|
131
128
|
omq pull --bind tcp://:5557 --compress &
|
|
132
|
-
echo "compressible data" | omq push --connect tcp://localhost:5557
|
|
129
|
+
echo "compressible data" | omq push --connect tcp://localhost:5557 -z
|
|
133
130
|
|
|
134
131
|
-- CURVE Encryption -----------------------------------------
|
|
135
132
|
|
|
@@ -143,7 +140,7 @@ module OMQ
|
|
|
143
140
|
-- ROUTER / DEALER ------------------------------------------
|
|
144
141
|
|
|
145
142
|
+--------+ +--------+
|
|
146
|
-
| DEALER
|
|
143
|
+
| DEALER |--------->| ROUTER |
|
|
147
144
|
| id=w1 | | |
|
|
148
145
|
+--------+ +--------+
|
|
149
146
|
|
|
@@ -156,25 +153,31 @@ module OMQ
|
|
|
156
153
|
-- Ruby Eval ------------------------------------------------
|
|
157
154
|
|
|
158
155
|
# filter incoming: only pass messages containing "error"
|
|
159
|
-
omq pull -b tcp://:5557 --recv-eval '
|
|
156
|
+
omq pull -b tcp://:5557 --recv-eval 'it.first.include?("error") ? it : nil'
|
|
160
157
|
|
|
161
158
|
# transform incoming with gems
|
|
162
|
-
omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(
|
|
159
|
+
omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse(it.first)["temperature"]'
|
|
163
160
|
|
|
164
161
|
# require a local file, use its methods
|
|
165
|
-
omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all(
|
|
162
|
+
omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all(it)'
|
|
166
163
|
|
|
167
|
-
# next skips, break stops
|
|
168
|
-
omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/;
|
|
164
|
+
# next skips, break stops
|
|
165
|
+
omq pull -b tcp://:5557 -e 'next if it.first =~ /^#/; break if it.first =~ /quit/; it'
|
|
169
166
|
|
|
170
167
|
# BEGIN/END blocks (like awk) -- accumulate and summarize
|
|
171
|
-
omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum +=
|
|
168
|
+
omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += it.first.to_i; nil END{puts @sum}'
|
|
172
169
|
|
|
173
170
|
# transform outgoing messages
|
|
174
|
-
echo hello | omq push -c tcp://localhost:5557 --send-eval '
|
|
171
|
+
echo hello | omq push -c tcp://localhost:5557 --send-eval 'it.map(&:upcase)'
|
|
175
172
|
|
|
176
173
|
# REQ: transform request and reply independently
|
|
177
|
-
echo hello | omq req -c tcp://localhost:5555 -E '
|
|
174
|
+
echo hello | omq req -c tcp://localhost:5555 -E 'it.map(&:upcase)' -e 'it.first'
|
|
175
|
+
|
|
176
|
+
# block parameter: single param receives parts array
|
|
177
|
+
omq pull -b tcp://:5557 -e '|msg| msg.map(&:upcase)'
|
|
178
|
+
|
|
179
|
+
# destructure multipart messages with parens
|
|
180
|
+
omq pull -b tcp://:5557 -e '|(key, value)| "#{key}=#{value}"'
|
|
178
181
|
|
|
179
182
|
-- Script Handlers (-r) ------------------------------------
|
|
180
183
|
|
|
@@ -185,7 +188,7 @@ module OMQ
|
|
|
185
188
|
omq pull --bind tcp://:5557 -r./handler.rb
|
|
186
189
|
|
|
187
190
|
# combine script handlers with inline eval
|
|
188
|
-
omq req -c tcp://localhost:5555 -r./handler.rb -E '
|
|
191
|
+
omq req -c tcp://localhost:5555 -r./handler.rb -E 'it.map(&:upcase)'
|
|
189
192
|
|
|
190
193
|
# OMQ.outgoing { |msg| ... } -- registered outgoing transform
|
|
191
194
|
# OMQ.incoming { |msg| ... } -- registered incoming transform
|
|
@@ -278,6 +281,7 @@ module OMQ
|
|
|
278
281
|
|
|
279
282
|
o.separator "Connection:"
|
|
280
283
|
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
|
|
284
|
+
v = expand_endpoint(v)
|
|
281
285
|
ep = Endpoint.new(v, false)
|
|
282
286
|
case pipe_side
|
|
283
287
|
when :in
|
|
@@ -290,6 +294,7 @@ module OMQ
|
|
|
290
294
|
end
|
|
291
295
|
}
|
|
292
296
|
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
|
|
297
|
+
v = expand_endpoint(v)
|
|
293
298
|
ep = Endpoint.new(v, true)
|
|
294
299
|
case pipe_side
|
|
295
300
|
when :in
|
|
@@ -373,8 +378,8 @@ module OMQ
|
|
|
373
378
|
end
|
|
374
379
|
|
|
375
380
|
o.separator "\nProcessing (-e = incoming, -E = outgoing):"
|
|
376
|
-
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message (
|
|
377
|
-
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message (
|
|
381
|
+
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message (it = parts, or |a, b|)") { |v| opts[:recv_expr] = v }
|
|
382
|
+
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message (it = parts, or |a, b|)") { |v| opts[:send_expr] = v }
|
|
378
383
|
o.on("-r", "--require LIB", "Require lib/file in Async context; use '-' for stdin. Scripts can register OMQ.outgoing/incoming") { |v|
|
|
379
384
|
require "omq" unless defined?(OMQ::VERSION)
|
|
380
385
|
opts[:scripts] << (v == "-" ? :stdin : (v.start_with?("./", "../") ? File.expand_path(v) : v))
|
|
@@ -495,6 +500,15 @@ module OMQ
|
|
|
495
500
|
if type_name == "pipe"
|
|
496
501
|
has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
497
502
|
if has_in_out
|
|
503
|
+
# Promote bare endpoints into the missing side:
|
|
504
|
+
# `pipe -c SRC --out -c DST` → bare SRC becomes --in
|
|
505
|
+
if opts[:in_endpoints].empty? && opts[:endpoints].any?
|
|
506
|
+
opts[:in_endpoints] = opts[:endpoints]
|
|
507
|
+
opts[:endpoints] = []
|
|
508
|
+
elsif opts[:out_endpoints].empty? && opts[:endpoints].any?
|
|
509
|
+
opts[:out_endpoints] = opts[:endpoints]
|
|
510
|
+
opts[:endpoints] = []
|
|
511
|
+
end
|
|
498
512
|
abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
|
|
499
513
|
abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
|
|
500
514
|
abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
|
|
@@ -543,6 +557,14 @@ module OMQ
|
|
|
543
557
|
end
|
|
544
558
|
|
|
545
559
|
|
|
560
|
+
# Expands shorthand `@name` to `ipc://@name` (Linux abstract namespace).
|
|
561
|
+
# Only triggers when the value starts with `@` and has no `://` scheme.
|
|
562
|
+
#
|
|
563
|
+
def expand_endpoint(url)
|
|
564
|
+
url.start_with?("@") && !url.include?("://") ? "ipc://#{url}" : url
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
|
|
546
568
|
# Returns the loopback address for bind normalization.
|
|
547
569
|
# Prefers IPv6 loopback ([::1]) when the host has at least one
|
|
548
570
|
# non-loopback, non-link-local IPv6 address, otherwise 127.0.0.1.
|
|
@@ -26,16 +26,13 @@ module OMQ
|
|
|
26
26
|
|
|
27
27
|
if src
|
|
28
28
|
expr, begin_body, end_body = extract_blocks(src)
|
|
29
|
-
@begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
30
|
-
@end_proc = eval("proc { #{end_body} }") if end_body
|
|
29
|
+
@begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
30
|
+
@end_proc = eval("proc { #{end_body} }") if end_body
|
|
31
31
|
if expr && !expr.strip.empty?
|
|
32
|
-
@eval_proc = eval("proc {
|
|
32
|
+
@eval_proc = eval("proc { #{expr} }")
|
|
33
33
|
end
|
|
34
34
|
elsif fallback_proc
|
|
35
|
-
@eval_proc = proc {
|
|
36
|
-
$_ = msg&.first
|
|
37
|
-
fallback_proc.call(msg)
|
|
38
|
-
}
|
|
35
|
+
@eval_proc = proc { fallback_proc.call(it) }
|
|
39
36
|
end
|
|
40
37
|
end
|
|
41
38
|
|
|
@@ -46,7 +43,6 @@ module OMQ
|
|
|
46
43
|
def call(parts, context)
|
|
47
44
|
return parts unless @eval_proc
|
|
48
45
|
|
|
49
|
-
$F = parts
|
|
50
46
|
result = context.instance_exec(parts, &@eval_proc)
|
|
51
47
|
return nil if result.nil?
|
|
52
48
|
return SENT if result.equal?(context)
|
|
@@ -108,12 +104,11 @@ module OMQ
|
|
|
108
104
|
expr, begin_body = extract.(src, "BEGIN")
|
|
109
105
|
expr, end_body = extract.(expr, "END")
|
|
110
106
|
|
|
111
|
-
begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
112
|
-
end_proc = eval("proc { #{end_body} }") if end_body
|
|
107
|
+
begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
108
|
+
end_proc = eval("proc { #{end_body} }") if end_body
|
|
113
109
|
eval_proc = nil
|
|
114
110
|
if expr && !expr.strip.empty?
|
|
115
|
-
|
|
116
|
-
eval_proc = eval("proc { |__F| $_ = __F&.first; #{ractor_expr} }") # rubocop:disable Security/Eval
|
|
111
|
+
eval_proc = eval("proc { #{expr} }")
|
|
117
112
|
end
|
|
118
113
|
|
|
119
114
|
[begin_proc, end_proc, eval_proc]
|
data/lib/omq/cli/formatter.rb
CHANGED
|
@@ -88,16 +88,17 @@ module OMQ
|
|
|
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| RLZ4.compress(p) } : parts
|
|
91
|
+
@compress ? parts.map { |p| RLZ4.compress(p) if p } : parts
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
# Decompresses each frame with LZ4 if compression is enabled.
|
|
96
|
+
# nil/empty frames pass through — they were nil before send coercion.
|
|
96
97
|
#
|
|
97
98
|
# @param parts [Array<String>] possibly compressed message frames
|
|
98
99
|
# @return [Array<String>] decompressed frames
|
|
99
100
|
def decompress(parts)
|
|
100
|
-
@compress ? parts.map { |p| RLZ4.decompress(p) } : parts
|
|
101
|
+
@compress ? parts.map { |p| p && !p.empty? ? RLZ4.decompress(p) : p } : parts
|
|
101
102
|
rescue RLZ4::DecompressError
|
|
102
103
|
raise DecompressError, "decompression failed (did the sender use --compress?)"
|
|
103
104
|
end
|
|
@@ -108,11 +109,13 @@ module OMQ
|
|
|
108
109
|
# @param parts [Array<String>] message frames
|
|
109
110
|
# @return [String] truncated preview of each frame joined by |
|
|
110
111
|
def self.preview(parts)
|
|
111
|
-
total
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
total = parts.sum(&:bytesize)
|
|
113
|
+
nparts = parts.size
|
|
114
|
+
shown = parts.first(3).map { |p| preview_frame(p) }
|
|
115
|
+
tail = nparts > 3 ? "|..." : ""
|
|
116
|
+
header = nparts > 1 ? "(#{total}B #{nparts}F)" : "(#{total}B)"
|
|
114
117
|
|
|
115
|
-
"
|
|
118
|
+
"#{header} #{shown.join("|")}#{tail}"
|
|
116
119
|
end
|
|
117
120
|
|
|
118
121
|
|
|
@@ -122,7 +125,7 @@ module OMQ
|
|
|
122
125
|
# string — otherwise joining with "|" would produce misleading
|
|
123
126
|
# output like "|body" for REP/REQ-style envelopes where the first
|
|
124
127
|
# wire frame is an empty delimiter.
|
|
125
|
-
return "
|
|
128
|
+
return "''" if bytes.empty?
|
|
126
129
|
|
|
127
130
|
sample = bytes[0, 12]
|
|
128
131
|
printable = sample.count("\x20-\x7e")
|
data/lib/omq/cli/pipe.rb
CHANGED
|
@@ -55,7 +55,7 @@ module OMQ
|
|
|
55
55
|
wait_for_peers_with_timeout if config.timeout
|
|
56
56
|
setup_sequential_transient(task)
|
|
57
57
|
@sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
|
|
58
|
-
sequential_message_loop
|
|
58
|
+
sequential_message_loop(fan_out: out_eps.size > 1)
|
|
59
59
|
@sock.instance_exec(&@recv_end_proc) if @recv_end_proc
|
|
60
60
|
ensure
|
|
61
61
|
@pull&.close
|
|
@@ -67,10 +67,13 @@ module OMQ
|
|
|
67
67
|
# it, there's no point waiting: PULL#receive blocks naturally
|
|
68
68
|
# and PUSH buffers up to send_hwm when no peer is present.
|
|
69
69
|
def wait_for_peers_with_timeout
|
|
70
|
+
_, out_eps = resolve_endpoints
|
|
70
71
|
Fiber.scheduler.with_timeout(config.timeout) do
|
|
71
72
|
Barrier do |barrier|
|
|
72
|
-
barrier.async(annotation: "wait push peer") { @push.peer_connected.wait }
|
|
73
73
|
barrier.async(annotation: "wait pull peer") { @pull.peer_connected.wait }
|
|
74
|
+
barrier.async(annotation: "wait push peers") do
|
|
75
|
+
sleep 0.01 until @push.connection_count >= out_eps.size
|
|
76
|
+
end
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
end
|
|
@@ -98,7 +101,7 @@ module OMQ
|
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
|
|
101
|
-
def sequential_message_loop
|
|
104
|
+
def sequential_message_loop(fan_out: false)
|
|
102
105
|
n = config.count
|
|
103
106
|
i = 0
|
|
104
107
|
loop do
|
|
@@ -109,6 +112,12 @@ module OMQ
|
|
|
109
112
|
if parts && !parts.empty?
|
|
110
113
|
@push.send(@fmt_out.compress(parts))
|
|
111
114
|
end
|
|
115
|
+
# Yield after send so send-pump fibers can drain the queue
|
|
116
|
+
# before the next message is enqueued. Without this, one pump
|
|
117
|
+
# monopolizes the shared queue via drain_send_queue_capped when
|
|
118
|
+
# messages arrive in bursts (recv prefetch). Only needed for
|
|
119
|
+
# multi-output pipes; single-output has no fairness concern.
|
|
120
|
+
Async::Task.current.yield if fan_out
|
|
112
121
|
i += 1
|
|
113
122
|
break if n && n > 0 && i >= n
|
|
114
123
|
end
|
data/lib/omq/cli/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.12.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
version: '0.17'
|
|
19
19
|
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 0.17.
|
|
21
|
+
version: 0.17.8
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -28,7 +28,7 @@ dependencies:
|
|
|
28
28
|
version: '0.17'
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 0.17.
|
|
31
|
+
version: 0.17.8
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: omq-ffi
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|