omq-cli 0.7.2 → 0.8.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 +30 -0
- data/lib/omq/cli/base_runner.rb +54 -28
- data/lib/omq/cli/cli_parser.rb +57 -52
- data/lib/omq/cli/formatter.rb +16 -0
- data/lib/omq/cli/parallel_worker.rb +191 -0
- data/lib/omq/cli/pipe.rb +38 -34
- data/lib/omq/cli/pipe_worker.rb +9 -64
- data/lib/omq/cli/push_pull.rb +8 -0
- data/lib/omq/cli/ractor_helpers.rb +81 -0
- data/lib/omq/cli/req_rep.rb +5 -0
- data/lib/omq/cli/scatter_gather.rb +8 -0
- data/lib/omq/cli/socket_setup.rb +21 -14
- data/lib/omq/cli/transient_monitor.rb +1 -1
- data/lib/omq/cli/version.rb +1 -1
- data/lib/omq/cli.rb +4 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9d13a0ffe0dbde54709fe0489385447cf61913d8a0b7d9abc2caa67e64fd7a5
|
|
4
|
+
data.tar.gz: 6987e6aa0569b9e2bc41d6265adf7ea6bc05458086764df0e3c6ca501f08849d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71c929c48a622fe318a531b03a6a7ab3f15c418dcf5b7662f000be0865423c660beb5967536d9ff30d26cd601648638cfdaa63f535a2da0e3dcc076c5bb36ed3
|
|
7
|
+
data.tar.gz: a3e937b036c55ae4072ddccb38b9acf278e22f8e60ee957b9441611d2a256f34b02cde3c3c847b53811e7365e498fcdeeab5aa512bf2f22c05fa309ce6c997e0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`-P` for pull, gather, and rep** — parallel Ractor workers for recv-only
|
|
8
|
+
and request-reply socket types. Output serialized through `Ractor::Port`
|
|
9
|
+
to avoid scrambled stdout.
|
|
10
|
+
- **`RactorHelpers` module** — shared Ractor infrastructure: `preresolve_tcp`,
|
|
11
|
+
`start_log_consumer`, `start_output_consumer` with `SHUTDOWN` sentinel for
|
|
12
|
+
clean consumer shutdown.
|
|
13
|
+
- **`ParallelWorker` class** — general Ractor worker for parallel socket modes.
|
|
14
|
+
- **Process titles** — all runners set descriptive `proctitle`
|
|
15
|
+
(`omq TYPE [-z] [-PN] ENDPOINTS`). Pipe shows `omq pipe [-z] [-PN] IN -> OUT`.
|
|
16
|
+
Bare script mode shows `omq script`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **ASCII-only source** — replaced all Unicode special characters (em-dashes,
|
|
21
|
+
box-drawing, arrows) with ASCII equivalents in lib/ and test/.
|
|
22
|
+
- **Pipe default HWM** — pipe sockets now default to HWM of 64 (instead of
|
|
23
|
+
the socket default 1000) to bound memory with large messages in pipeline
|
|
24
|
+
stages. Override with `--send-hwm` / `--recv-hwm`.
|
|
25
|
+
- **Message preview** — total byte count first, 12 chars per part, max 3 parts
|
|
26
|
+
shown (`(1234B) frame1|frame2|frame3|...(5 parts)`).
|
|
27
|
+
- **`SocketSetup.apply_options`** — extracted shared socket option setup,
|
|
28
|
+
used by `BaseRunner`, `PipeRunner`, `PipeWorker`, and `ParallelWorker`.
|
|
29
|
+
- **`Formatter.preview`** — extracted from duplicated `msg_preview` methods.
|
|
30
|
+
- **Pipe/PipeWorker** — use bare `.new` for sockets, `SocketSetup.apply_options`
|
|
31
|
+
for configuration, `RactorHelpers` for Ractor infrastructure.
|
|
32
|
+
|
|
3
33
|
## 0.7.2 — 2026-04-07
|
|
4
34
|
|
|
5
35
|
### Changed
|
data/lib/omq/cli/base_runner.rb
CHANGED
|
@@ -24,6 +24,7 @@ module OMQ
|
|
|
24
24
|
# @param task [Async::Task] the parent async task
|
|
25
25
|
# @return [void]
|
|
26
26
|
def call(task)
|
|
27
|
+
set_process_title
|
|
27
28
|
setup_socket
|
|
28
29
|
start_event_monitor if config.verbose >= 2
|
|
29
30
|
maybe_start_transient_monitor(task)
|
|
@@ -46,7 +47,33 @@ module OMQ
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
#
|
|
50
|
+
# -- Parallel Ractor workers -----------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_parallel_workers(socket_sym)
|
|
54
|
+
OMQ.freeze_for_ractors!
|
|
55
|
+
eps = RactorHelpers.preresolve_tcp(config.endpoints)
|
|
56
|
+
output_port, output_thread = RactorHelpers.start_output_consumer
|
|
57
|
+
log_port, log_thread = RactorHelpers.start_log_consumer
|
|
58
|
+
|
|
59
|
+
workers = config.parallel.times.map do
|
|
60
|
+
::Ractor.new(config, socket_sym, eps, output_port, log_port) do |cfg, sym, e, oport, lport|
|
|
61
|
+
ParallelWorker.new(cfg, sym, e, oport, lport).call
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
workers.each do |w|
|
|
66
|
+
w.join
|
|
67
|
+
rescue ::Ractor::RemoteError => e
|
|
68
|
+
$stderr.write("omq: Ractor error: #{e.cause&.message || e.message}\n")
|
|
69
|
+
end
|
|
70
|
+
ensure
|
|
71
|
+
RactorHelpers.stop_consumer(output_port, output_thread) if output_port
|
|
72
|
+
RactorHelpers.stop_consumer(log_port, log_thread) if log_port
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# -- Socket creation ---------------------------------------------
|
|
50
77
|
|
|
51
78
|
|
|
52
79
|
def setup_socket
|
|
@@ -68,7 +95,7 @@ module OMQ
|
|
|
68
95
|
end
|
|
69
96
|
|
|
70
97
|
|
|
71
|
-
#
|
|
98
|
+
# -- Transient disconnect monitor --------------------------------
|
|
72
99
|
|
|
73
100
|
|
|
74
101
|
def maybe_start_transient_monitor(task)
|
|
@@ -83,7 +110,7 @@ module OMQ
|
|
|
83
110
|
end
|
|
84
111
|
|
|
85
112
|
|
|
86
|
-
#
|
|
113
|
+
# -- BEGIN / END blocks ------------------------------------------
|
|
87
114
|
|
|
88
115
|
|
|
89
116
|
def run_begin_blocks
|
|
@@ -98,7 +125,7 @@ module OMQ
|
|
|
98
125
|
end
|
|
99
126
|
|
|
100
127
|
|
|
101
|
-
#
|
|
128
|
+
# -- Peer wait with grace period ---------------------------------
|
|
102
129
|
|
|
103
130
|
|
|
104
131
|
def needs_peer_wait?
|
|
@@ -139,7 +166,7 @@ module OMQ
|
|
|
139
166
|
end
|
|
140
167
|
|
|
141
168
|
|
|
142
|
-
#
|
|
169
|
+
# -- Socket setup ------------------------------------------------
|
|
143
170
|
|
|
144
171
|
|
|
145
172
|
def setup_subscriptions
|
|
@@ -157,7 +184,7 @@ module OMQ
|
|
|
157
184
|
end
|
|
158
185
|
|
|
159
186
|
|
|
160
|
-
#
|
|
187
|
+
# -- Shared loop bodies ------------------------------------------
|
|
161
188
|
|
|
162
189
|
|
|
163
190
|
def run_send_logic
|
|
@@ -273,7 +300,7 @@ module OMQ
|
|
|
273
300
|
end
|
|
274
301
|
|
|
275
302
|
|
|
276
|
-
#
|
|
303
|
+
# -- Message I/O -------------------------------------------------
|
|
277
304
|
|
|
278
305
|
|
|
279
306
|
def send_msg(parts)
|
|
@@ -360,7 +387,7 @@ module OMQ
|
|
|
360
387
|
end
|
|
361
388
|
|
|
362
389
|
|
|
363
|
-
#
|
|
390
|
+
# -- Eval --------------------------------------------------------
|
|
364
391
|
|
|
365
392
|
|
|
366
393
|
def compile_expr
|
|
@@ -377,7 +404,7 @@ module OMQ
|
|
|
377
404
|
|
|
378
405
|
|
|
379
406
|
def assign_send_aliases
|
|
380
|
-
# Keep ivar aliases
|
|
407
|
+
# Keep ivar aliases -- subclasses check these directly
|
|
381
408
|
@send_begin_proc = @send_evaluator.begin_proc
|
|
382
409
|
@send_eval_proc = @send_evaluator.eval_proc
|
|
383
410
|
@send_end_proc = @send_evaluator.end_proc
|
|
@@ -404,7 +431,22 @@ module OMQ
|
|
|
404
431
|
SENT = ExpressionEvaluator::SENT
|
|
405
432
|
|
|
406
433
|
|
|
407
|
-
#
|
|
434
|
+
# -- Process title -------------------------------------------------
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def set_process_title(endpoints: nil)
|
|
438
|
+
eps = endpoints || config.endpoints
|
|
439
|
+
title = ["omq", config.type_name]
|
|
440
|
+
title << "-z" if config.compress
|
|
441
|
+
title << "-P#{config.parallel}" if config.parallel
|
|
442
|
+
eps.each do |ep|
|
|
443
|
+
title << (ep.respond_to?(:url) ? ep.url : ep.to_s)
|
|
444
|
+
end
|
|
445
|
+
Process.setproctitle(title.join(" "))
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# -- Logging -----------------------------------------------------
|
|
408
450
|
|
|
409
451
|
|
|
410
452
|
def log(msg)
|
|
@@ -419,9 +461,9 @@ module OMQ
|
|
|
419
461
|
@sock.monitor(verbose: verbose) do |event|
|
|
420
462
|
case event.type
|
|
421
463
|
when :message_sent
|
|
422
|
-
$stderr.write("omq: >> #{
|
|
464
|
+
$stderr.write("omq: >> #{Formatter.preview(event.detail[:parts])}\n")
|
|
423
465
|
when :message_received
|
|
424
|
-
$stderr.write("omq: << #{
|
|
466
|
+
$stderr.write("omq: << #{Formatter.preview(event.detail[:parts])}\n")
|
|
425
467
|
else
|
|
426
468
|
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
427
469
|
detail = event.detail ? " #{event.detail}" : ""
|
|
@@ -429,22 +471,6 @@ module OMQ
|
|
|
429
471
|
end
|
|
430
472
|
end
|
|
431
473
|
end
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def msg_preview(parts)
|
|
435
|
-
parts.map { |p| preview_bytes(p) }.join(" | ")
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def preview_bytes(str)
|
|
440
|
-
bytes = str.b
|
|
441
|
-
preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
|
|
442
|
-
if bytes.bytesize > 10
|
|
443
|
-
"#{preview}... (#{bytes.bytesize}B)"
|
|
444
|
-
else
|
|
445
|
-
preview
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
474
|
end
|
|
449
475
|
end
|
|
450
476
|
end
|
data/lib/omq/cli/cli_parser.rb
CHANGED
|
@@ -6,12 +6,12 @@ module OMQ
|
|
|
6
6
|
#
|
|
7
7
|
class CliParser
|
|
8
8
|
EXAMPLES = <<~'TEXT'
|
|
9
|
-
|
|
9
|
+
-- Request / Reply ------------------------------------------
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
+-----+ "hello" +-----+
|
|
12
|
+
| REQ |------------->| REP |
|
|
13
|
+
| |<-------------| |
|
|
14
|
+
+-----+ "HELLO" +-----+
|
|
15
15
|
|
|
16
16
|
# terminal 1: echo server
|
|
17
17
|
omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
@@ -23,11 +23,11 @@ module OMQ
|
|
|
23
23
|
omq rep --bind ipc:///tmp/echo.sock --echo &
|
|
24
24
|
echo "hello" | omq req --connect ipc:///tmp/echo.sock
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
-- Publish / Subscribe --------------------------------------
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
+-----+ "weather.nyc 72F" +-----+
|
|
29
|
+
| PUB |--------------------->| SUB | --subscribe "weather."
|
|
30
|
+
+-----+ +-----+
|
|
31
31
|
|
|
32
32
|
# terminal 1: subscriber (all topics by default)
|
|
33
33
|
omq sub --bind tcp://:5556
|
|
@@ -35,11 +35,11 @@ module OMQ
|
|
|
35
35
|
# terminal 2: publisher (needs --delay for subscription to propagate)
|
|
36
36
|
echo "weather.nyc 72F" | omq pub --connect tcp://localhost:5556 --delay 1
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
-- Periodic Publish -------------------------------------------
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
+-----+ "tick 1" +-----+
|
|
41
|
+
| PUB |--(every 1s)-->| SUB |
|
|
42
|
+
+-----+ +-----+
|
|
43
43
|
|
|
44
44
|
# terminal 1: subscriber
|
|
45
45
|
omq sub --bind tcp://:5556
|
|
@@ -52,11 +52,11 @@ module OMQ
|
|
|
52
52
|
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
53
53
|
--data "tick" --interval 1 --count 5
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
-- Pipeline -------------------------------------------------
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
+------+ +------+
|
|
58
|
+
| PUSH |----------->| PULL |
|
|
59
|
+
+------+ +------+
|
|
60
60
|
|
|
61
61
|
# terminal 1: worker
|
|
62
62
|
omq pull --bind tcp://:5557
|
|
@@ -68,16 +68,16 @@ module OMQ
|
|
|
68
68
|
omq pull --bind ipc:///tmp/pipeline.sock &
|
|
69
69
|
echo "task 1" | omq push --connect ipc:///tmp/pipeline.sock
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
-- Pipe (PULL -> eval -> PUSH) --------------------------------
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
+------+ +------+ +------+
|
|
74
|
+
| PUSH |--------->| pipe |--------->| PULL |
|
|
75
|
+
+------+ +------+ +------+
|
|
76
76
|
|
|
77
77
|
# terminal 1: producer
|
|
78
78
|
echo -e "hello\nworld" | omq push --bind ipc://@work
|
|
79
79
|
|
|
80
|
-
# terminal 2: worker
|
|
80
|
+
# terminal 2: worker -- uppercase each message
|
|
81
81
|
omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
|
|
82
82
|
# terminal 3: collector
|
|
83
83
|
omq pull --bind ipc://@sink
|
|
@@ -88,19 +88,19 @@ module OMQ
|
|
|
88
88
|
# exit when producer disconnects (--transient)
|
|
89
89
|
omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
|
|
90
90
|
|
|
91
|
-
# fan-in: multiple sources
|
|
91
|
+
# fan-in: multiple sources -> one sink
|
|
92
92
|
omq pipe --in -c ipc://@work1 -c ipc://@work2 \
|
|
93
93
|
--out -c ipc://@sink -e '$F.map(&:upcase)'
|
|
94
94
|
|
|
95
|
-
# fan-out: one source
|
|
95
|
+
# fan-out: one source -> multiple sinks (round-robin)
|
|
96
96
|
omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
-- CLIENT / SERVER (draft) ----------------------------------
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
+--------+ "hello" +--------+
|
|
101
|
+
| CLIENT |------------>| SERVER | --recv-eval '$F.map(&:upcase)'
|
|
102
|
+
| |<------------| |
|
|
103
|
+
+--------+ "HELLO" +--------+
|
|
104
104
|
|
|
105
105
|
# terminal 1: upcasing server
|
|
106
106
|
omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
@@ -108,28 +108,28 @@ module OMQ
|
|
|
108
108
|
# terminal 2: client
|
|
109
109
|
echo "hello" | omq client --connect tcp://localhost:5555
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
-- Formats --------------------------------------------------
|
|
112
112
|
|
|
113
|
-
# ascii (default)
|
|
113
|
+
# ascii (default) -- non-printable replaced with dots
|
|
114
114
|
omq pull --bind tcp://:5557 --ascii
|
|
115
115
|
|
|
116
|
-
# quoted
|
|
116
|
+
# quoted -- lossless, round-trippable (uses String#dump escaping)
|
|
117
117
|
omq pull --bind tcp://:5557 --quoted
|
|
118
118
|
|
|
119
|
-
# JSON Lines
|
|
119
|
+
# JSON Lines -- structured, multipart as arrays
|
|
120
120
|
echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
|
|
121
121
|
omq pull --bind tcp://:5557 --jsonl
|
|
122
122
|
|
|
123
123
|
# multipart via tabs
|
|
124
124
|
printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
-- Compression ----------------------------------------------
|
|
127
127
|
|
|
128
128
|
# both sides must use --compress
|
|
129
129
|
omq pull --bind tcp://:5557 --compress &
|
|
130
130
|
echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
-- CURVE Encryption -----------------------------------------
|
|
133
133
|
|
|
134
134
|
# server (prints OMQ_SERVER_KEY=...)
|
|
135
135
|
omq rep --bind tcp://:5555 --echo --curve-server
|
|
@@ -138,12 +138,12 @@ module OMQ
|
|
|
138
138
|
echo "secret" | omq req --connect tcp://localhost:5555 \
|
|
139
139
|
--curve-server-key '<key from server>'
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
-- ROUTER / DEALER ------------------------------------------
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
+--------+ +--------+
|
|
144
|
+
| DEALER |---------->| ROUTER |
|
|
145
|
+
| id=w1 | | |
|
|
146
|
+
+--------+ +--------+
|
|
147
147
|
|
|
148
148
|
# terminal 1: router shows identity + message
|
|
149
149
|
omq router --bind tcp://:5555
|
|
@@ -151,7 +151,7 @@ module OMQ
|
|
|
151
151
|
# terminal 2: dealer with identity
|
|
152
152
|
echo "hello" | omq dealer --connect tcp://localhost:5555 --identity worker-1
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
-- Ruby Eval ------------------------------------------------
|
|
155
155
|
|
|
156
156
|
# filter incoming: only pass messages containing "error"
|
|
157
157
|
omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
|
|
@@ -162,10 +162,10 @@ module OMQ
|
|
|
162
162
|
# require a local file, use its methods
|
|
163
163
|
omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
|
|
164
164
|
|
|
165
|
-
# next skips, break stops
|
|
165
|
+
# next skips, break stops -- regexps match against $_
|
|
166
166
|
omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
|
|
167
167
|
|
|
168
|
-
# BEGIN/END blocks (like awk)
|
|
168
|
+
# BEGIN/END blocks (like awk) -- accumulate and summarize
|
|
169
169
|
omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
|
|
170
170
|
|
|
171
171
|
# transform outgoing messages
|
|
@@ -174,9 +174,9 @@ module OMQ
|
|
|
174
174
|
# REQ: transform request and reply independently
|
|
175
175
|
echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
-- Script Handlers (-r) ------------------------------------
|
|
178
178
|
|
|
179
|
-
# handler.rb
|
|
179
|
+
# handler.rb -- register transforms from a file
|
|
180
180
|
# db = PG.connect("dbname=app")
|
|
181
181
|
# OMQ.incoming { |first_part, _| db.exec(first_part).values.flatten }
|
|
182
182
|
# at_exit { db.close }
|
|
@@ -185,8 +185,8 @@ module OMQ
|
|
|
185
185
|
# combine script handlers with inline eval
|
|
186
186
|
omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
|
|
187
187
|
|
|
188
|
-
# OMQ.outgoing { |msg| ... }
|
|
189
|
-
# OMQ.incoming { |msg| ... }
|
|
188
|
+
# OMQ.outgoing { |msg| ... } -- registered outgoing transform
|
|
189
|
+
# OMQ.incoming { |msg| ... } -- registered incoming transform
|
|
190
190
|
# CLI flags (-e/-E) override registered handlers
|
|
191
191
|
TEXT
|
|
192
192
|
|
|
@@ -271,7 +271,7 @@ module OMQ
|
|
|
271
271
|
o.banner = "Usage: omq TYPE [options]\n\n" \
|
|
272
272
|
"Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
|
|
273
273
|
"Draft: client, server, radio, dish, scatter, gather, channel, peer\n" \
|
|
274
|
-
"Virtual: pipe (PULL
|
|
274
|
+
"Virtual: pipe (PULL -> eval -> PUSH)\n\n"
|
|
275
275
|
|
|
276
276
|
o.separator "Connection:"
|
|
277
277
|
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
|
|
@@ -412,7 +412,7 @@ module OMQ
|
|
|
412
412
|
type_name = argv.shift
|
|
413
413
|
if type_name.nil?
|
|
414
414
|
abort parser.to_s if opts[:scripts].empty?
|
|
415
|
-
# bare script mode
|
|
415
|
+
# bare script mode -- type_name stays nil
|
|
416
416
|
elsif !SOCKET_TYPE_NAMES.include?(type_name.downcase)
|
|
417
417
|
abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
|
|
418
418
|
else
|
|
@@ -486,10 +486,15 @@ module OMQ
|
|
|
486
486
|
abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
|
|
487
487
|
|
|
488
488
|
if opts[:parallel]
|
|
489
|
-
|
|
489
|
+
parallel_types = %w[pipe pull gather rep]
|
|
490
|
+
abort "-P/--parallel is only valid for #{parallel_types.join(", ")}" unless parallel_types.include?(type_name)
|
|
490
491
|
abort "-P/--parallel must be 1..16" unless (1..16).include?(opts[:parallel])
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
if type_name == "pipe"
|
|
493
|
+
all_eps = opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]
|
|
494
|
+
else
|
|
495
|
+
all_eps = opts[:endpoints]
|
|
496
|
+
end
|
|
497
|
+
abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if all_eps.any?(&:bind?)
|
|
493
498
|
end
|
|
494
499
|
|
|
495
500
|
(opts[:connects] + opts[:binds]).each do |url|
|
data/lib/omq/cli/formatter.rb
CHANGED
|
@@ -98,6 +98,22 @@ module OMQ
|
|
|
98
98
|
rescue
|
|
99
99
|
abort "omq: decompression failed (did the sender use --compress?)"
|
|
100
100
|
end
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Formats message parts for human-readable preview (logging).
|
|
104
|
+
#
|
|
105
|
+
# @param parts [Array<String>] message frames
|
|
106
|
+
# @return [String] truncated preview of each frame joined by |
|
|
107
|
+
def self.preview(parts)
|
|
108
|
+
total = parts.sum(&:bytesize)
|
|
109
|
+
shown = parts.first(3).map do |p|
|
|
110
|
+
bytes = p.b
|
|
111
|
+
preview = bytes[0, 12].gsub(/[^[:print:]]/, ".")
|
|
112
|
+
bytes.bytesize > 12 ? "#{preview}..." : preview
|
|
113
|
+
end
|
|
114
|
+
tail = parts.size > 3 ? "|...(#{parts.size} parts)" : ""
|
|
115
|
+
"(#{total}B) #{shown.join("|")}#{tail}"
|
|
116
|
+
end
|
|
101
117
|
end
|
|
102
118
|
end
|
|
103
119
|
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Worker that runs inside a Ractor for parallel socket modes (-P).
|
|
6
|
+
# Each worker owns its own Async reactor and socket instance.
|
|
7
|
+
#
|
|
8
|
+
# Supported socket types:
|
|
9
|
+
# - pull, gather (recv-only)
|
|
10
|
+
# - rep (recv-reply with echo/data/eval)
|
|
11
|
+
#
|
|
12
|
+
class ParallelWorker
|
|
13
|
+
def initialize(config, socket_sym, endpoints, output_port, log_port)
|
|
14
|
+
@config = config
|
|
15
|
+
@socket_sym = socket_sym
|
|
16
|
+
@endpoints = endpoints
|
|
17
|
+
@output_port = output_port
|
|
18
|
+
@log_port = log_port
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
Async do
|
|
24
|
+
setup_socket
|
|
25
|
+
log_endpoints
|
|
26
|
+
start_monitors
|
|
27
|
+
wait_for_peer
|
|
28
|
+
compile_expr
|
|
29
|
+
run_loop
|
|
30
|
+
run_end_block
|
|
31
|
+
ensure
|
|
32
|
+
@sock&.close
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def setup_socket
|
|
41
|
+
@sock = OMQ.const_get(@socket_sym).new
|
|
42
|
+
OMQ::CLI::SocketSetup.apply_options(@sock, @config)
|
|
43
|
+
@sock.identity = @config.identity if @config.identity
|
|
44
|
+
OMQ::CLI::SocketSetup.attach_endpoints(@sock, @endpoints, verbose: false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def log_endpoints
|
|
49
|
+
return unless @config.verbose >= 1
|
|
50
|
+
@endpoints.each do |ep|
|
|
51
|
+
@log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def start_monitors
|
|
57
|
+
return unless @config.verbose >= 2
|
|
58
|
+
trace = @config.verbose >= 3
|
|
59
|
+
@sock.monitor(verbose: trace) do |event|
|
|
60
|
+
@log_port.send(format_event(event))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def format_event(event)
|
|
66
|
+
case event.type
|
|
67
|
+
when :message_sent
|
|
68
|
+
"omq: >> #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
|
|
69
|
+
when :message_received
|
|
70
|
+
"omq: << #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
|
|
71
|
+
else
|
|
72
|
+
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
73
|
+
detail = event.detail ? " #{event.detail}" : ""
|
|
74
|
+
"omq: #{event.type}#{ep}#{detail}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def wait_for_peer
|
|
80
|
+
if @config.timeout
|
|
81
|
+
Fiber.scheduler.with_timeout(@config.timeout) do
|
|
82
|
+
@sock.peer_connected.wait
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
@sock.peer_connected.wait
|
|
86
|
+
end
|
|
87
|
+
rescue IO::TimeoutError, Async::TimeoutError
|
|
88
|
+
# Proceed anyway -- recv will timeout if no messages arrive
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def compile_expr
|
|
93
|
+
@begin_proc, @end_proc, @eval_proc =
|
|
94
|
+
OMQ::CLI::ExpressionEvaluator.compile_inside_ractor(@config.recv_expr)
|
|
95
|
+
@fmt = OMQ::CLI::Formatter.new(@config.format, compress: @config.compress)
|
|
96
|
+
@ctx = Object.new
|
|
97
|
+
@ctx.instance_exec(&@begin_proc) if @begin_proc
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_loop
|
|
102
|
+
case @config.type_name
|
|
103
|
+
when "pull", "gather"
|
|
104
|
+
run_recv_loop
|
|
105
|
+
when "rep"
|
|
106
|
+
run_rep_loop
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# -- Recv-only loop (PULL, GATHER) -----------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run_recv_loop
|
|
115
|
+
n = @config.count
|
|
116
|
+
i = 0
|
|
117
|
+
loop do
|
|
118
|
+
parts = @sock.receive
|
|
119
|
+
break if parts.nil?
|
|
120
|
+
parts = @fmt.decompress(parts)
|
|
121
|
+
if @eval_proc
|
|
122
|
+
parts = normalize(
|
|
123
|
+
@ctx.instance_exec(parts, &@eval_proc)
|
|
124
|
+
)
|
|
125
|
+
next if parts.nil?
|
|
126
|
+
end
|
|
127
|
+
output(parts) unless parts.empty?
|
|
128
|
+
i += 1
|
|
129
|
+
break if n && n > 0 && i >= n
|
|
130
|
+
end
|
|
131
|
+
rescue IO::TimeoutError, Async::TimeoutError
|
|
132
|
+
# recv timed out -- fall through to END block
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# -- REP loop (recv request, process, send reply) --------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_rep_loop
|
|
140
|
+
n = @config.count
|
|
141
|
+
i = 0
|
|
142
|
+
loop do
|
|
143
|
+
parts = @sock.receive
|
|
144
|
+
break if parts.nil?
|
|
145
|
+
parts = @fmt.decompress(parts)
|
|
146
|
+
reply = compute_reply(parts)
|
|
147
|
+
output(reply)
|
|
148
|
+
@sock.send(@fmt.compress(reply))
|
|
149
|
+
i += 1
|
|
150
|
+
break if n && n > 0 && i >= n
|
|
151
|
+
end
|
|
152
|
+
rescue IO::TimeoutError, Async::TimeoutError
|
|
153
|
+
# recv timed out -- fall through to END block
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def compute_reply(parts)
|
|
158
|
+
if @eval_proc
|
|
159
|
+
normalize(@ctx.instance_exec(parts, &@eval_proc)) || [""]
|
|
160
|
+
elsif @config.echo
|
|
161
|
+
parts
|
|
162
|
+
elsif @config.data
|
|
163
|
+
@fmt.decode(@config.data + "\n")
|
|
164
|
+
else
|
|
165
|
+
parts
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# -- Output and helpers ----------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def output(parts)
|
|
174
|
+
return if @config.quiet || parts.nil?
|
|
175
|
+
@output_port.send(@fmt.encode(parts))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def normalize(result)
|
|
180
|
+
OMQ::CLI::ExpressionEvaluator.normalize_result(result)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_end_block
|
|
185
|
+
return unless @end_proc
|
|
186
|
+
out = normalize(@ctx.instance_exec(&@end_proc))
|
|
187
|
+
output(out) if out && !out.empty?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
data/lib/omq/cli/pipe.rb
CHANGED
|
@@ -5,6 +5,11 @@ module OMQ
|
|
|
5
5
|
# Runner for the virtual "pipe" socket type (PULL -> eval -> PUSH).
|
|
6
6
|
# Supports sequential and parallel (Ractor-based) processing modes.
|
|
7
7
|
class PipeRunner
|
|
8
|
+
# Default HWM for pipe sockets when the user hasn't set one.
|
|
9
|
+
# Much lower than the socket default (1000) to bound memory
|
|
10
|
+
# with large messages in pipeline stages.
|
|
11
|
+
PIPE_HWM = 64
|
|
12
|
+
|
|
8
13
|
# @return [Config] frozen CLI configuration
|
|
9
14
|
attr_reader :config
|
|
10
15
|
|
|
@@ -42,10 +47,11 @@ module OMQ
|
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
|
|
45
|
-
#
|
|
50
|
+
# -- Sequential ---------------------------------------------------
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
def run_sequential(task)
|
|
54
|
+
set_pipe_process_title
|
|
49
55
|
in_eps, out_eps = resolve_endpoints
|
|
50
56
|
@pull, @push = build_pull_push(in_eps, out_eps)
|
|
51
57
|
compile_expr
|
|
@@ -73,21 +79,13 @@ module OMQ
|
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
|
|
76
|
-
def apply_socket_options(sock)
|
|
77
|
-
sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
78
|
-
sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
|
|
79
|
-
sock.send_hwm = config.send_hwm if config.send_hwm
|
|
80
|
-
sock.recv_hwm = config.recv_hwm if config.recv_hwm
|
|
81
|
-
sock.sndbuf = config.sndbuf if config.sndbuf
|
|
82
|
-
sock.rcvbuf = config.rcvbuf if config.rcvbuf
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
|
|
86
82
|
def build_pull_push(in_eps, out_eps)
|
|
87
|
-
pull = OMQ::PULL.new
|
|
88
|
-
push = OMQ::PUSH.new
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
pull = OMQ::PULL.new
|
|
84
|
+
push = OMQ::PUSH.new
|
|
85
|
+
SocketSetup.apply_options(pull, config)
|
|
86
|
+
SocketSetup.apply_options(push, config)
|
|
87
|
+
pull.recv_hwm = PIPE_HWM unless config.recv_hwm
|
|
88
|
+
push.send_hwm = PIPE_HWM unless config.send_hwm
|
|
91
89
|
SocketSetup.attach_endpoints(pull, in_eps, verbose: config.verbose >= 1)
|
|
92
90
|
SocketSetup.attach_endpoints(push, out_eps, verbose: config.verbose >= 1)
|
|
93
91
|
[pull, push]
|
|
@@ -121,15 +119,16 @@ module OMQ
|
|
|
121
119
|
end
|
|
122
120
|
|
|
123
121
|
|
|
124
|
-
#
|
|
122
|
+
# -- Parallel -----------------------------------------------------
|
|
125
123
|
|
|
126
124
|
|
|
127
125
|
def run_parallel(task)
|
|
126
|
+
set_pipe_process_title
|
|
128
127
|
OMQ.freeze_for_ractors!
|
|
129
128
|
in_eps, out_eps = resolve_endpoints
|
|
130
|
-
in_eps =
|
|
131
|
-
out_eps =
|
|
132
|
-
log_port, log_thread =
|
|
129
|
+
in_eps = RactorHelpers.preresolve_tcp(in_eps)
|
|
130
|
+
out_eps = RactorHelpers.preresolve_tcp(out_eps)
|
|
131
|
+
log_port, log_thread = RactorHelpers.start_log_consumer
|
|
133
132
|
workers = config.parallel.times.map do
|
|
134
133
|
::Ractor.new(config, in_eps, out_eps, log_port) do |cfg, ins, outs, lport|
|
|
135
134
|
PipeWorker.new(cfg, ins, outs, lport).call
|
|
@@ -141,12 +140,26 @@ module OMQ
|
|
|
141
140
|
$stderr.write("omq: Ractor error: #{e.cause&.message || e.message}\n")
|
|
142
141
|
end
|
|
143
142
|
ensure
|
|
144
|
-
log_port
|
|
145
|
-
log_thread.join
|
|
143
|
+
RactorHelpers.stop_consumer(log_port, log_thread) if log_port
|
|
146
144
|
end
|
|
147
145
|
|
|
148
146
|
|
|
149
|
-
#
|
|
147
|
+
# -- Process title -------------------------------------------------
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def set_pipe_process_title
|
|
151
|
+
in_eps, out_eps = resolve_endpoints
|
|
152
|
+
title = ["omq pipe"]
|
|
153
|
+
title << "-z" if config.compress || config.compress_in || config.compress_out
|
|
154
|
+
title << "-P#{config.parallel}" if config.parallel
|
|
155
|
+
title.concat(in_eps.map(&:url))
|
|
156
|
+
title << "->"
|
|
157
|
+
title.concat(out_eps.map(&:url))
|
|
158
|
+
Process.setproctitle(title.join(" "))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# -- Expression eval ----------------------------------------------
|
|
150
163
|
|
|
151
164
|
|
|
152
165
|
def compile_expr
|
|
@@ -163,7 +176,7 @@ module OMQ
|
|
|
163
176
|
end
|
|
164
177
|
|
|
165
178
|
|
|
166
|
-
#
|
|
179
|
+
# -- Event monitoring ---------------------------------------------
|
|
167
180
|
|
|
168
181
|
|
|
169
182
|
def start_event_monitors
|
|
@@ -172,9 +185,9 @@ module OMQ
|
|
|
172
185
|
sock.monitor(verbose: verbose) do |event|
|
|
173
186
|
case event.type
|
|
174
187
|
when :message_sent
|
|
175
|
-
$stderr.write("omq: >> #{
|
|
188
|
+
$stderr.write("omq: >> #{Formatter.preview(event.detail[:parts])}\n")
|
|
176
189
|
when :message_received
|
|
177
|
-
$stderr.write("omq: << #{
|
|
190
|
+
$stderr.write("omq: << #{Formatter.preview(event.detail[:parts])}\n")
|
|
178
191
|
else
|
|
179
192
|
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
180
193
|
detail = event.detail ? " #{event.detail}" : ""
|
|
@@ -183,15 +196,6 @@ module OMQ
|
|
|
183
196
|
end
|
|
184
197
|
end
|
|
185
198
|
end
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def msg_preview(parts)
|
|
189
|
-
parts.map { |p|
|
|
190
|
-
bytes = p.b
|
|
191
|
-
preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
|
|
192
|
-
bytes.bytesize > 10 ? "#{preview}... (#{bytes.bytesize}B)" : preview
|
|
193
|
-
}.join(" | ")
|
|
194
|
-
end
|
|
195
199
|
end
|
|
196
200
|
end
|
|
197
201
|
end
|
data/lib/omq/cli/pipe_worker.rb
CHANGED
|
@@ -6,42 +6,6 @@ module OMQ
|
|
|
6
6
|
# Each worker owns its own Async reactor, PULL socket, and PUSH socket.
|
|
7
7
|
#
|
|
8
8
|
class PipeWorker
|
|
9
|
-
# Starts a Ractor::Port and a consumer thread that drains log
|
|
10
|
-
# messages to stderr sequentially. Returns [port, thread].
|
|
11
|
-
#
|
|
12
|
-
def self.start_log_consumer
|
|
13
|
-
port = Ractor::Port.new
|
|
14
|
-
thread = Thread.new(port) do |p|
|
|
15
|
-
loop do
|
|
16
|
-
$stderr.write("#{p.receive}\n")
|
|
17
|
-
rescue Ractor::ClosedError
|
|
18
|
-
break
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
[port, thread]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Resolves TCP hostnames to IP addresses so Ractors don't touch
|
|
26
|
-
# Resolv::DefaultResolver (which is not shareable).
|
|
27
|
-
#
|
|
28
|
-
def self.preresolve_tcp(endpoints)
|
|
29
|
-
endpoints.flat_map do |ep|
|
|
30
|
-
url = ep.url
|
|
31
|
-
if url.start_with?("tcp://")
|
|
32
|
-
host, port = OMQ::Transport::TCP.parse_endpoint(url)
|
|
33
|
-
Addrinfo.getaddrinfo(host, port, nil, :STREAM).map do |addr|
|
|
34
|
-
ip = addr.ip_address
|
|
35
|
-
ip = "[#{ip}]" if ip.include?(":")
|
|
36
|
-
Endpoint.new("tcp://#{ip}:#{addr.ip_port}", ep.bind?)
|
|
37
|
-
end
|
|
38
|
-
else
|
|
39
|
-
ep
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
|
|
45
9
|
def initialize(config, in_eps, out_eps, log_port)
|
|
46
10
|
@config = config
|
|
47
11
|
@in_eps = in_eps
|
|
@@ -70,27 +34,17 @@ module OMQ
|
|
|
70
34
|
|
|
71
35
|
|
|
72
36
|
def setup_sockets
|
|
73
|
-
@pull = OMQ::PULL.new
|
|
74
|
-
@push = OMQ::PUSH.new
|
|
75
|
-
@pull
|
|
76
|
-
@push
|
|
77
|
-
|
|
78
|
-
|
|
37
|
+
@pull = OMQ::PULL.new
|
|
38
|
+
@push = OMQ::PUSH.new
|
|
39
|
+
OMQ::CLI::SocketSetup.apply_options(@pull, @config)
|
|
40
|
+
OMQ::CLI::SocketSetup.apply_options(@push, @config)
|
|
41
|
+
@pull.recv_hwm = PipeRunner::PIPE_HWM unless @config.recv_hwm
|
|
42
|
+
@push.send_hwm = PipeRunner::PIPE_HWM unless @config.send_hwm
|
|
79
43
|
OMQ::CLI::SocketSetup.attach_endpoints(@pull, @in_eps, verbose: false)
|
|
80
44
|
OMQ::CLI::SocketSetup.attach_endpoints(@push, @out_eps, verbose: false)
|
|
81
45
|
end
|
|
82
46
|
|
|
83
47
|
|
|
84
|
-
def apply_socket_options(sock)
|
|
85
|
-
sock.reconnect_interval = @config.reconnect_ivl if @config.reconnect_ivl
|
|
86
|
-
sock.heartbeat_interval = @config.heartbeat_ivl if @config.heartbeat_ivl
|
|
87
|
-
sock.send_hwm = @config.send_hwm if @config.send_hwm
|
|
88
|
-
sock.recv_hwm = @config.recv_hwm if @config.recv_hwm
|
|
89
|
-
sock.sndbuf = @config.sndbuf if @config.sndbuf
|
|
90
|
-
sock.rcvbuf = @config.rcvbuf if @config.rcvbuf
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
|
|
94
48
|
def log_endpoints
|
|
95
49
|
@in_eps.each { |ep| @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}") }
|
|
96
50
|
@out_eps.each { |ep| @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}") }
|
|
@@ -110,9 +64,9 @@ module OMQ
|
|
|
110
64
|
def format_event(event)
|
|
111
65
|
case event.type
|
|
112
66
|
when :message_sent
|
|
113
|
-
"omq: >> #{
|
|
67
|
+
"omq: >> #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
|
|
114
68
|
when :message_received
|
|
115
|
-
"omq: << #{
|
|
69
|
+
"omq: << #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
|
|
116
70
|
else
|
|
117
71
|
ep = event.endpoint ? " #{event.endpoint}" : ""
|
|
118
72
|
detail = event.detail ? " #{event.detail}" : ""
|
|
@@ -121,15 +75,6 @@ module OMQ
|
|
|
121
75
|
end
|
|
122
76
|
|
|
123
77
|
|
|
124
|
-
def msg_preview(parts)
|
|
125
|
-
parts.map { |p|
|
|
126
|
-
bytes = p.b
|
|
127
|
-
preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
|
|
128
|
-
bytes.bytesize > 10 ? "#{preview}... (#{bytes.bytesize}B)" : preview
|
|
129
|
-
}.join(" | ")
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
|
|
133
78
|
def wait_for_peers
|
|
134
79
|
Barrier do |barrier|
|
|
135
80
|
barrier.async { @pull.peer_connected.wait }
|
|
@@ -188,7 +133,7 @@ module OMQ
|
|
|
188
133
|
end
|
|
189
134
|
end
|
|
190
135
|
rescue IO::TimeoutError, Async::TimeoutError
|
|
191
|
-
# recv timed out
|
|
136
|
+
# recv timed out -- fall through to END block
|
|
192
137
|
end
|
|
193
138
|
|
|
194
139
|
|
data/lib/omq/cli/push_pull.rb
CHANGED
|
@@ -10,6 +10,14 @@ module OMQ
|
|
|
10
10
|
|
|
11
11
|
# Runner for PULL sockets (receive-only pipeline consumer).
|
|
12
12
|
class PullRunner < BaseRunner
|
|
13
|
+
def call(task)
|
|
14
|
+
config.parallel ? run_parallel_workers(:PULL) : super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
def run_loop(task) = run_recv_logic
|
|
14
22
|
end
|
|
15
23
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Shared Ractor infrastructure for parallel worker modes.
|
|
6
|
+
module RactorHelpers
|
|
7
|
+
# Sentinel value sent through ports to signal consumer threads to exit.
|
|
8
|
+
# Port#close does not unblock a waiting #receive, so we must send an
|
|
9
|
+
# explicit shutdown marker.
|
|
10
|
+
SHUTDOWN = :__omq_shutdown__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Resolves TCP hostnames to IP addresses so Ractors don't touch
|
|
14
|
+
# Resolv::DefaultResolver (which is not shareable).
|
|
15
|
+
#
|
|
16
|
+
def self.preresolve_tcp(endpoints)
|
|
17
|
+
endpoints.flat_map do |ep|
|
|
18
|
+
url = ep.url
|
|
19
|
+
if url.start_with?("tcp://")
|
|
20
|
+
host, port = OMQ::Transport::TCP.parse_endpoint(url)
|
|
21
|
+
Addrinfo.getaddrinfo(host, port, nil, :STREAM).map do |addr|
|
|
22
|
+
ip = addr.ip_address
|
|
23
|
+
ip = "[#{ip}]" if ip.include?(":")
|
|
24
|
+
Endpoint.new("tcp://#{ip}:#{addr.ip_port}", ep.bind?)
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
ep
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Starts a Ractor::Port and a consumer thread that drains log
|
|
34
|
+
# messages to stderr sequentially. Returns [port, thread].
|
|
35
|
+
# Send SHUTDOWN through the port to stop the consumer.
|
|
36
|
+
#
|
|
37
|
+
def self.start_log_consumer
|
|
38
|
+
port = Ractor::Port.new
|
|
39
|
+
thread = Thread.new(port) do |p|
|
|
40
|
+
loop do
|
|
41
|
+
msg = p.receive
|
|
42
|
+
break if msg.equal?(SHUTDOWN)
|
|
43
|
+
$stderr.write("#{msg}\n")
|
|
44
|
+
rescue Ractor::ClosedError
|
|
45
|
+
break
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
[port, thread]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Starts a Ractor::Port and a consumer thread that drains
|
|
53
|
+
# formatted output to stdout sequentially. Returns [port, thread].
|
|
54
|
+
# Send SHUTDOWN through the port to stop the consumer.
|
|
55
|
+
#
|
|
56
|
+
def self.start_output_consumer
|
|
57
|
+
port = Ractor::Port.new
|
|
58
|
+
thread = Thread.new(port) do |p|
|
|
59
|
+
loop do
|
|
60
|
+
msg = p.receive
|
|
61
|
+
break if msg.equal?(SHUTDOWN)
|
|
62
|
+
$stdout.write(msg)
|
|
63
|
+
rescue Ractor::ClosedError
|
|
64
|
+
break
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
[port, thread]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Sends the shutdown sentinel and joins the consumer thread.
|
|
72
|
+
#
|
|
73
|
+
def self.stop_consumer(port, thread)
|
|
74
|
+
port.send(SHUTDOWN)
|
|
75
|
+
thread.join
|
|
76
|
+
rescue Ractor::ClosedError
|
|
77
|
+
thread.join(1)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/omq/cli/req_rep.rb
CHANGED
|
@@ -10,6 +10,14 @@ module OMQ
|
|
|
10
10
|
|
|
11
11
|
# Runner for GATHER sockets (draft; fan-in receive).
|
|
12
12
|
class GatherRunner < BaseRunner
|
|
13
|
+
def call(task)
|
|
14
|
+
config.parallel ? run_parallel_workers(:GATHER) : super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
def run_loop(task) = run_recv_logic
|
|
14
22
|
end
|
|
15
23
|
end
|
data/lib/omq/cli/socket_setup.rb
CHANGED
|
@@ -6,23 +6,30 @@ module OMQ
|
|
|
6
6
|
# All methods are module-level so callers compose rather than inherit.
|
|
7
7
|
#
|
|
8
8
|
module SocketSetup
|
|
9
|
+
# Apply common socket options from +config+ to +sock+.
|
|
10
|
+
#
|
|
11
|
+
def self.apply_options(sock, config)
|
|
12
|
+
sock.linger = config.linger
|
|
13
|
+
sock.recv_timeout = config.timeout if config.timeout
|
|
14
|
+
sock.send_timeout = config.timeout if config.timeout
|
|
15
|
+
sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
16
|
+
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
|
|
19
|
+
sock.sndbuf = config.sndbuf if config.sndbuf
|
|
20
|
+
sock.rcvbuf = config.rcvbuf if config.rcvbuf
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
9
24
|
# Create and fully configure a socket from +klass+ and +config+.
|
|
10
25
|
#
|
|
11
26
|
def self.build(klass, config)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
sock
|
|
15
|
-
sock.
|
|
16
|
-
sock.
|
|
17
|
-
sock.
|
|
18
|
-
sock.send_hwm = config.send_hwm if config.send_hwm
|
|
19
|
-
sock.recv_hwm = config.recv_hwm if config.recv_hwm
|
|
20
|
-
sock.sndbuf = config.sndbuf if config.sndbuf
|
|
21
|
-
sock.rcvbuf = config.rcvbuf if config.rcvbuf
|
|
22
|
-
sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
|
|
23
|
-
sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
|
|
24
|
-
sock.identity = config.identity if config.identity
|
|
25
|
-
sock.router_mandatory = true if config.type_name == "router"
|
|
27
|
+
sock = klass.new
|
|
28
|
+
sock.conflate = true if config.conflate && %w[pub radio].include?(config.type_name)
|
|
29
|
+
apply_options(sock, config)
|
|
30
|
+
sock.max_message_size = config.recv_maxsz if config.recv_maxsz
|
|
31
|
+
sock.identity = config.identity if config.identity
|
|
32
|
+
sock.router_mandatory = true if config.type_name == "router"
|
|
26
33
|
sock
|
|
27
34
|
end
|
|
28
35
|
|
data/lib/omq/cli/version.rb
CHANGED
data/lib/omq/cli.rb
CHANGED
|
@@ -18,6 +18,8 @@ require_relative "cli/req_rep"
|
|
|
18
18
|
require_relative "cli/pair"
|
|
19
19
|
require_relative "cli/router_dealer"
|
|
20
20
|
require_relative "cli/client_server"
|
|
21
|
+
require_relative "cli/ractor_helpers"
|
|
22
|
+
require_relative "cli/parallel_worker"
|
|
21
23
|
require_relative "cli/pipe_worker"
|
|
22
24
|
require_relative "cli/pipe"
|
|
23
25
|
|
|
@@ -169,7 +171,7 @@ module OMQ
|
|
|
169
171
|
abort "CURVE requires libsodium. Install it:\n" \
|
|
170
172
|
" apt install libsodium-dev # Debian/Ubuntu\n" \
|
|
171
173
|
" brew install libsodium # macOS\n" \
|
|
172
|
-
"Or use nuckle (pure Ruby, DANGEROUS
|
|
174
|
+
"Or use nuckle (pure Ruby, DANGEROUS -- not audited):\n" \
|
|
173
175
|
" --crypto nuckle"
|
|
174
176
|
end
|
|
175
177
|
else
|
|
@@ -221,6 +223,7 @@ module OMQ
|
|
|
221
223
|
end
|
|
222
224
|
|
|
223
225
|
if config.type_name.nil?
|
|
226
|
+
Process.setproctitle("omq script")
|
|
224
227
|
Object.include(OMQ) unless Object.include?(OMQ)
|
|
225
228
|
Async annotation: 'omq' do
|
|
226
229
|
Async::Debug.serve(endpoint: debug_ep) if debug_ep
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -165,10 +165,12 @@ files:
|
|
|
165
165
|
- lib/omq/cli/expression_evaluator.rb
|
|
166
166
|
- lib/omq/cli/formatter.rb
|
|
167
167
|
- lib/omq/cli/pair.rb
|
|
168
|
+
- lib/omq/cli/parallel_worker.rb
|
|
168
169
|
- lib/omq/cli/pipe.rb
|
|
169
170
|
- lib/omq/cli/pipe_worker.rb
|
|
170
171
|
- lib/omq/cli/pub_sub.rb
|
|
171
172
|
- lib/omq/cli/push_pull.rb
|
|
173
|
+
- lib/omq/cli/ractor_helpers.rb
|
|
172
174
|
- lib/omq/cli/radio_dish.rb
|
|
173
175
|
- lib/omq/cli/req_rep.rb
|
|
174
176
|
- lib/omq/cli/router_dealer.rb
|