omq 0.6.5 → 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 +78 -1
- data/README.md +76 -81
- data/lib/omq/cli/base_runner.rb +54 -21
- data/lib/omq/cli/client_server.rb +14 -9
- data/lib/omq/cli/config.rb +7 -4
- data/lib/omq/cli/pair.rb +1 -1
- data/lib/omq/cli/pipe.rb +37 -21
- data/lib/omq/cli/req_rep.rb +8 -4
- data/lib/omq/cli/router_dealer.rb +12 -6
- data/lib/omq/cli.rb +100 -34
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/engine.rb +69 -22
- data/lib/omq/zmtp/routing/peer.rb +3 -2
- data/lib/omq/zmtp/routing/rep.rb +2 -3
- data/lib/omq/zmtp/routing/req.rb +3 -7
- data/lib/omq/zmtp/routing/router.rb +3 -2
- data/lib/omq/zmtp/routing/server.rb +3 -2
- data/lib/omq/zmtp.rb +16 -13
- data/lib/omq.rb +1 -0
- metadata +15 -8
- data/lib/omq/zmtp/codec/command.rb +0 -210
- data/lib/omq/zmtp/codec/frame.rb +0 -89
- data/lib/omq/zmtp/codec/greeting.rb +0 -78
- data/lib/omq/zmtp/codec.rb +0 -18
- data/lib/omq/zmtp/connection.rb +0 -282
- data/lib/omq/zmtp/mechanism/null.rb +0 -72
- data/lib/omq/zmtp/valid_peers.rb +0 -29
data/lib/omq/cli/pipe.rb
CHANGED
|
@@ -24,9 +24,22 @@ module OMQ
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def resolve_endpoints
|
|
28
|
+
if config.in_endpoints.any?
|
|
29
|
+
[config.in_endpoints, config.out_endpoints]
|
|
30
|
+
else
|
|
31
|
+
[[config.endpoints[0]], [config.endpoints[1]]]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def attach_endpoints(sock, endpoints)
|
|
37
|
+
endpoints.each { |ep| ep.bind? ? sock.bind(ep.url) : sock.connect(ep.url) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
27
41
|
def run_sequential(task)
|
|
28
|
-
|
|
29
|
-
push_ep = config.endpoints[1]
|
|
42
|
+
in_eps, out_eps = resolve_endpoints
|
|
30
43
|
|
|
31
44
|
@pull = OMQ::PULL.new(linger: config.linger, recv_timeout: config.timeout)
|
|
32
45
|
@push = OMQ::PUSH.new(linger: config.linger, send_timeout: config.timeout)
|
|
@@ -35,11 +48,11 @@ module OMQ
|
|
|
35
48
|
@pull.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
|
|
36
49
|
@push.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
attach_endpoints(@pull, in_eps)
|
|
52
|
+
attach_endpoints(@push, out_eps)
|
|
40
53
|
|
|
41
54
|
compile_expr
|
|
42
|
-
@sock = @pull # for
|
|
55
|
+
@sock = @pull # for eval instance_exec
|
|
43
56
|
|
|
44
57
|
with_timeout(config.timeout) do
|
|
45
58
|
@push.peer_connected.wait
|
|
@@ -54,7 +67,7 @@ module OMQ
|
|
|
54
67
|
end
|
|
55
68
|
end
|
|
56
69
|
|
|
57
|
-
@sock.instance_exec(&@
|
|
70
|
+
@sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
|
|
58
71
|
|
|
59
72
|
n = config.count
|
|
60
73
|
i = 0
|
|
@@ -62,7 +75,7 @@ module OMQ
|
|
|
62
75
|
parts = @pull.receive
|
|
63
76
|
break if parts.nil?
|
|
64
77
|
parts = @fmt.decompress(parts)
|
|
65
|
-
parts =
|
|
78
|
+
parts = eval_recv_expr(parts)
|
|
66
79
|
if parts && !parts.empty?
|
|
67
80
|
@push.send(@fmt.compress(parts))
|
|
68
81
|
end
|
|
@@ -70,7 +83,7 @@ module OMQ
|
|
|
70
83
|
break if n && n > 0 && i >= n
|
|
71
84
|
end
|
|
72
85
|
|
|
73
|
-
@sock.instance_exec(&@
|
|
86
|
+
@sock.instance_exec(&@recv_end_proc) if @recv_end_proc
|
|
74
87
|
ensure
|
|
75
88
|
@pull&.close
|
|
76
89
|
@push&.close
|
|
@@ -86,7 +99,7 @@ module OMQ
|
|
|
86
99
|
Sync do |task|
|
|
87
100
|
# Parse BEGIN/END blocks and per-message expression
|
|
88
101
|
begin_proc = end_proc = eval_proc = nil
|
|
89
|
-
if cfg.
|
|
102
|
+
if cfg.recv_expr
|
|
90
103
|
extract = ->(src, kw) {
|
|
91
104
|
s = src.index(/#{kw}\s*\{/)
|
|
92
105
|
return [src, nil] unless s
|
|
@@ -97,7 +110,7 @@ module OMQ
|
|
|
97
110
|
end
|
|
98
111
|
[src[0...s] + src[j..], src[(i + 1)..(j - 2)]]
|
|
99
112
|
}
|
|
100
|
-
expr, begin_body = extract.(cfg.
|
|
113
|
+
expr, begin_body = extract.(cfg.recv_expr, "BEGIN")
|
|
101
114
|
expr, end_body = extract.(expr, "END")
|
|
102
115
|
begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
103
116
|
end_proc = eval("proc { #{end_body} }") if end_body
|
|
@@ -115,8 +128,10 @@ module OMQ
|
|
|
115
128
|
push.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
|
|
116
129
|
pull.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
|
|
117
130
|
push.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
in_eps = cfg.in_endpoints.any? ? cfg.in_endpoints : [cfg.endpoints[0]]
|
|
132
|
+
out_eps = cfg.out_endpoints.any? ? cfg.out_endpoints : [cfg.endpoints[1]]
|
|
133
|
+
in_eps.each { |ep| pull.connect(ep.url) }
|
|
134
|
+
out_eps.each { |ep| push.connect(ep.url) }
|
|
120
135
|
|
|
121
136
|
if cfg.timeout
|
|
122
137
|
task.with_timeout(cfg.timeout) do
|
|
@@ -187,11 +202,12 @@ module OMQ
|
|
|
187
202
|
|
|
188
203
|
|
|
189
204
|
def compile_expr
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
@
|
|
194
|
-
@
|
|
205
|
+
src = config.recv_expr
|
|
206
|
+
return unless src
|
|
207
|
+
expr, begin_body, end_body = extract_blocks(src)
|
|
208
|
+
@recv_begin_proc = eval("proc { #{begin_body} }") if begin_body
|
|
209
|
+
@recv_end_proc = eval("proc { #{end_body} }") if end_body
|
|
210
|
+
@recv_eval_proc = eval("proc { $_ = $F&.first; #{expr} }") if expr && !expr.strip.empty?
|
|
195
211
|
end
|
|
196
212
|
|
|
197
213
|
|
|
@@ -224,10 +240,10 @@ module OMQ
|
|
|
224
240
|
end
|
|
225
241
|
|
|
226
242
|
|
|
227
|
-
def
|
|
228
|
-
return parts unless @
|
|
243
|
+
def eval_recv_expr(parts)
|
|
244
|
+
return parts unless @recv_eval_proc
|
|
229
245
|
$F = parts
|
|
230
|
-
result = @sock.instance_exec(&@
|
|
246
|
+
result = @sock.instance_exec(&@recv_eval_proc)
|
|
231
247
|
return nil if result.nil? || result.equal?(@sock)
|
|
232
248
|
return [result] if config.format == :marshal
|
|
233
249
|
case result
|
|
@@ -236,7 +252,7 @@ module OMQ
|
|
|
236
252
|
else [result.to_str]
|
|
237
253
|
end
|
|
238
254
|
rescue => e
|
|
239
|
-
$stderr.puts "omq:
|
|
255
|
+
$stderr.puts "omq: eval error: #{e.message} (#{e.class})"
|
|
240
256
|
exit 3
|
|
241
257
|
end
|
|
242
258
|
|
data/lib/omq/cli/req_rep.rb
CHANGED
|
@@ -14,10 +14,12 @@ module OMQ
|
|
|
14
14
|
loop do
|
|
15
15
|
parts = read_next
|
|
16
16
|
break unless parts
|
|
17
|
+
parts = eval_send_expr(parts)
|
|
18
|
+
next unless parts
|
|
17
19
|
send_msg(parts)
|
|
18
20
|
reply = recv_msg
|
|
19
21
|
break if reply.nil?
|
|
20
|
-
reply =
|
|
22
|
+
reply = eval_recv_expr(reply)
|
|
21
23
|
output(reply)
|
|
22
24
|
i += 1
|
|
23
25
|
break if n && n > 0 && i >= n
|
|
@@ -29,10 +31,12 @@ module OMQ
|
|
|
29
31
|
loop do
|
|
30
32
|
parts = read_next
|
|
31
33
|
break unless parts
|
|
34
|
+
parts = eval_send_expr(parts)
|
|
35
|
+
next unless parts
|
|
32
36
|
send_msg(parts)
|
|
33
37
|
reply = recv_msg
|
|
34
38
|
break if reply.nil?
|
|
35
|
-
reply =
|
|
39
|
+
reply = eval_recv_expr(reply)
|
|
36
40
|
output(reply)
|
|
37
41
|
i += 1
|
|
38
42
|
break if n && n > 0 && i >= n
|
|
@@ -53,8 +57,8 @@ module OMQ
|
|
|
53
57
|
loop do
|
|
54
58
|
msg = recv_msg
|
|
55
59
|
break if msg.nil?
|
|
56
|
-
if config.
|
|
57
|
-
reply =
|
|
60
|
+
if config.recv_expr || @recv_eval_proc
|
|
61
|
+
reply = eval_recv_expr(msg)
|
|
58
62
|
unless reply.equal?(SENT)
|
|
59
63
|
output(reply)
|
|
60
64
|
send_msg(reply || [""])
|
|
@@ -20,7 +20,7 @@ module OMQ
|
|
|
20
20
|
identity = parts.shift
|
|
21
21
|
parts.shift if parts.first == ""
|
|
22
22
|
parts = @fmt.decompress(parts)
|
|
23
|
-
result =
|
|
23
|
+
result = eval_recv_expr([display_routing_id(identity), *parts])
|
|
24
24
|
output(result)
|
|
25
25
|
i += 1
|
|
26
26
|
break if n && n > 0 && i >= n
|
|
@@ -35,18 +35,18 @@ module OMQ
|
|
|
35
35
|
Async::Loop.quantized(interval: config.interval) do
|
|
36
36
|
parts = read_next
|
|
37
37
|
break unless parts
|
|
38
|
-
|
|
38
|
+
send_targeted_or_eval(parts)
|
|
39
39
|
i += 1
|
|
40
40
|
break if n && n > 0 && i >= n
|
|
41
41
|
end
|
|
42
42
|
elsif config.data || config.file
|
|
43
43
|
parts = read_next
|
|
44
|
-
|
|
44
|
+
send_targeted_or_eval(parts) if parts
|
|
45
45
|
else
|
|
46
46
|
loop do
|
|
47
47
|
parts = read_next
|
|
48
48
|
break unless parts
|
|
49
|
-
|
|
49
|
+
send_targeted_or_eval(parts)
|
|
50
50
|
i += 1
|
|
51
51
|
break if n && n > 0 && i >= n
|
|
52
52
|
end
|
|
@@ -57,8 +57,14 @@ module OMQ
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
def
|
|
61
|
-
if
|
|
60
|
+
def send_targeted_or_eval(parts)
|
|
61
|
+
if @send_eval_proc
|
|
62
|
+
parts = eval_send_expr(parts)
|
|
63
|
+
return unless parts
|
|
64
|
+
identity = resolve_target(parts.shift)
|
|
65
|
+
payload = @fmt.compress(parts)
|
|
66
|
+
@sock.send([identity, "", *payload])
|
|
67
|
+
elsif config.target
|
|
62
68
|
payload = @fmt.compress(parts)
|
|
63
69
|
@sock.send([resolve_target(config.target), "", *payload])
|
|
64
70
|
else
|
data/lib/omq/cli.rb
CHANGED
|
@@ -17,6 +17,15 @@ require_relative "cli/peer"
|
|
|
17
17
|
require_relative "cli/pipe"
|
|
18
18
|
|
|
19
19
|
module OMQ
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
attr_reader :outgoing_proc, :incoming_proc
|
|
23
|
+
|
|
24
|
+
def outgoing(&block) = @outgoing_proc = block
|
|
25
|
+
def incoming(&block) = @incoming_proc = block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
|
|
20
29
|
module CLI
|
|
21
30
|
SOCKET_TYPE_NAMES = %w[
|
|
22
31
|
req rep pub sub push pull pair dealer router
|
|
@@ -56,7 +65,7 @@ module OMQ
|
|
|
56
65
|
└─────┘ "HELLO" └─────┘
|
|
57
66
|
|
|
58
67
|
# terminal 1: echo server
|
|
59
|
-
omq rep --bind tcp://:5555 --eval '$F.map(&:upcase)'
|
|
68
|
+
omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
60
69
|
|
|
61
70
|
# terminal 2: send a request
|
|
62
71
|
echo "hello" | omq req --connect tcp://localhost:5555
|
|
@@ -79,7 +88,7 @@ module OMQ
|
|
|
79
88
|
|
|
80
89
|
── Periodic Publish ───────────────────────────────────────────
|
|
81
90
|
|
|
82
|
-
┌─────┐ "tick 1"
|
|
91
|
+
┌─────┐ "tick 1" ┌─────┐
|
|
83
92
|
│ PUB │──(every 1s)─→│ SUB │
|
|
84
93
|
└─────┘ └─────┘
|
|
85
94
|
|
|
@@ -121,27 +130,31 @@ module OMQ
|
|
|
121
130
|
|
|
122
131
|
# terminal 2: worker — uppercase each message
|
|
123
132
|
omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
|
|
124
|
-
|
|
125
133
|
# terminal 3: collector
|
|
126
134
|
omq pull --bind ipc://@sink
|
|
127
135
|
|
|
128
136
|
# 4 Ractor workers in a single process (-P)
|
|
129
|
-
omq pipe -c ipc://@work -c ipc://@sink -
|
|
130
|
-
-r ./fib.rb -e 'fib(Integer($_)).to_s'
|
|
137
|
+
omq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
|
|
131
138
|
|
|
132
139
|
# exit when producer disconnects (--transient)
|
|
133
|
-
omq pipe -c ipc://@work -c ipc://@sink --transient
|
|
134
|
-
|
|
140
|
+
omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
|
|
141
|
+
|
|
142
|
+
# fan-in: multiple sources → one sink
|
|
143
|
+
omq pipe --in -c ipc://@work1 -c ipc://@work2 \
|
|
144
|
+
--out -c ipc://@sink -e '$F.map(&:upcase)'
|
|
145
|
+
|
|
146
|
+
# fan-out: one source → multiple sinks (round-robin)
|
|
147
|
+
omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
|
|
135
148
|
|
|
136
149
|
── CLIENT / SERVER (draft) ──────────────────────────────────
|
|
137
150
|
|
|
138
151
|
┌────────┐ "hello" ┌────────┐
|
|
139
|
-
│ CLIENT │───────────→│ SERVER │ --eval '$F.map(&:upcase)'
|
|
152
|
+
│ CLIENT │───────────→│ SERVER │ --recv-eval '$F.map(&:upcase)'
|
|
140
153
|
│ │←───────────│ │
|
|
141
154
|
└────────┘ "HELLO" └────────┘
|
|
142
155
|
|
|
143
156
|
# terminal 1: upcasing server
|
|
144
|
-
omq server --bind tcp://:5555 --eval '$F.map(&:upcase)'
|
|
157
|
+
omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
|
|
145
158
|
|
|
146
159
|
# terminal 2: client
|
|
147
160
|
echo "hello" | omq client --connect tcp://localhost:5555
|
|
@@ -187,30 +200,45 @@ module OMQ
|
|
|
187
200
|
omq router --bind tcp://:5555
|
|
188
201
|
|
|
189
202
|
# terminal 2: dealer with identity
|
|
190
|
-
echo "hello" | omq dealer --connect tcp://localhost:5555
|
|
191
|
-
--identity worker-1
|
|
203
|
+
echo "hello" | omq dealer --connect tcp://localhost:5555 --identity worker-1
|
|
192
204
|
|
|
193
205
|
── Ruby Eval ────────────────────────────────────────────────
|
|
194
206
|
|
|
195
|
-
# filter: only pass messages containing "error"
|
|
196
|
-
omq pull
|
|
197
|
-
--eval '$F.first.include?("error") ? $F : nil'
|
|
207
|
+
# filter incoming: only pass messages containing "error"
|
|
208
|
+
omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
|
|
198
209
|
|
|
199
|
-
# transform with gems
|
|
200
|
-
omq sub
|
|
201
|
-
--eval 'JSON.parse($F.first)["temperature"]'
|
|
210
|
+
# transform incoming with gems
|
|
211
|
+
omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($F.first)["temperature"]'
|
|
202
212
|
|
|
203
|
-
# require a local file, use its methods
|
|
204
|
-
omq rep --bind tcp://:5555 --require ./transform.rb
|
|
205
|
-
--eval 'upcase_all($F)'
|
|
213
|
+
# require a local file, use its methods
|
|
214
|
+
omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
|
|
206
215
|
|
|
207
216
|
# next skips, break stops — regexps match against $_
|
|
208
|
-
omq pull
|
|
209
|
-
--eval 'next if /^#/; break if /quit/; $F'
|
|
217
|
+
omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
|
|
210
218
|
|
|
211
219
|
# BEGIN/END blocks (like awk) — accumulate and summarize
|
|
212
|
-
omq pull
|
|
213
|
-
|
|
220
|
+
omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
|
|
221
|
+
|
|
222
|
+
# transform outgoing messages
|
|
223
|
+
echo hello | omq push -c tcp://localhost:5557 --send-eval '$F.map(&:upcase)'
|
|
224
|
+
|
|
225
|
+
# REQ: transform request and reply independently
|
|
226
|
+
echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
|
|
227
|
+
|
|
228
|
+
── Script Handlers (-r) ────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
# handler.rb — register transforms from a file
|
|
231
|
+
# db = PG.connect("dbname=app")
|
|
232
|
+
# OMQ.incoming { |first_part, _| db.exec(first_part).values.flatten }
|
|
233
|
+
# at_exit { db.close }
|
|
234
|
+
omq pull --bind tcp://:5557 -r./handler.rb
|
|
235
|
+
|
|
236
|
+
# combine script handlers with inline eval
|
|
237
|
+
omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
|
|
238
|
+
|
|
239
|
+
# OMQ.outgoing { |msg| ... } — registered outgoing transform
|
|
240
|
+
# OMQ.incoming { |msg| ... } — registered incoming transform
|
|
241
|
+
# CLI flags (-e/-E) override registered handlers
|
|
214
242
|
TEXT
|
|
215
243
|
|
|
216
244
|
module_function
|
|
@@ -299,6 +327,8 @@ module OMQ
|
|
|
299
327
|
endpoints: [],
|
|
300
328
|
connects: [],
|
|
301
329
|
binds: [],
|
|
330
|
+
in_endpoints: [],
|
|
331
|
+
out_endpoints: [],
|
|
302
332
|
data: nil,
|
|
303
333
|
file: nil,
|
|
304
334
|
format: :ascii,
|
|
@@ -316,7 +346,8 @@ module OMQ
|
|
|
316
346
|
heartbeat_ivl: nil,
|
|
317
347
|
conflate: false,
|
|
318
348
|
compress: false,
|
|
319
|
-
|
|
349
|
+
send_expr: nil,
|
|
350
|
+
recv_expr: nil,
|
|
320
351
|
parallel: nil,
|
|
321
352
|
transient: false,
|
|
322
353
|
verbose: false,
|
|
@@ -326,6 +357,8 @@ module OMQ
|
|
|
326
357
|
curve_server_key: nil,
|
|
327
358
|
}
|
|
328
359
|
|
|
360
|
+
pipe_side = nil # nil = legacy positional mode; :in/:out = modal
|
|
361
|
+
|
|
329
362
|
parser = OptionParser.new do |o|
|
|
330
363
|
o.banner = "Usage: omq TYPE [options]\n\n" \
|
|
331
364
|
"Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
|
|
@@ -333,8 +366,24 @@ module OMQ
|
|
|
333
366
|
"Virtual: pipe (PULL → eval → PUSH)\n\n"
|
|
334
367
|
|
|
335
368
|
o.separator "Connection:"
|
|
336
|
-
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)")
|
|
337
|
-
|
|
369
|
+
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
|
|
370
|
+
ep = Endpoint.new(v, false)
|
|
371
|
+
case pipe_side
|
|
372
|
+
when :in then opts[:in_endpoints] << ep
|
|
373
|
+
when :out then opts[:out_endpoints] << ep
|
|
374
|
+
else opts[:endpoints] << ep; opts[:connects] << v
|
|
375
|
+
end
|
|
376
|
+
}
|
|
377
|
+
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
|
|
378
|
+
ep = Endpoint.new(v, true)
|
|
379
|
+
case pipe_side
|
|
380
|
+
when :in then opts[:in_endpoints] << ep
|
|
381
|
+
when :out then opts[:out_endpoints] << ep
|
|
382
|
+
else opts[:endpoints] << ep; opts[:binds] << v
|
|
383
|
+
end
|
|
384
|
+
}
|
|
385
|
+
o.on("--in", "Pipe: subsequent -b/-c attach to input (PULL) side") { pipe_side = :in }
|
|
386
|
+
o.on("--out", "Pipe: subsequent -b/-c attach to output (PUSH) side") { pipe_side = :out }
|
|
338
387
|
|
|
339
388
|
o.separator "\nData source (REP: reply source):"
|
|
340
389
|
o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
|
|
@@ -380,9 +429,11 @@ module OMQ
|
|
|
380
429
|
o.separator "\nCompression:"
|
|
381
430
|
o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
|
|
382
431
|
|
|
383
|
-
o.separator "\nProcessing:"
|
|
384
|
-
o.on("-e", "--eval EXPR",
|
|
385
|
-
o.on("-
|
|
432
|
+
o.separator "\nProcessing (-e = incoming, -E = outgoing):"
|
|
433
|
+
o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($F = parts)") { |v| opts[:recv_expr] = v }
|
|
434
|
+
o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($F = parts)") { |v| opts[:send_expr] = v }
|
|
435
|
+
o.on("-r", "--require LIB", "Require lib/file; scripts can register OMQ.outgoing/incoming") { |v|
|
|
436
|
+
require_relative "../omq" unless defined?(OMQ::VERSION)
|
|
386
437
|
v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
|
|
387
438
|
}
|
|
388
439
|
o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
|
|
@@ -421,10 +472,13 @@ module OMQ
|
|
|
421
472
|
|
|
422
473
|
opts[:type_name] = type_name.downcase
|
|
423
474
|
|
|
424
|
-
normalize
|
|
475
|
+
normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
|
|
476
|
+
normalize_ep = ->(ep) { Endpoint.new(normalize.call(ep.url), ep.bind?) }
|
|
425
477
|
opts[:binds].map!(&normalize)
|
|
426
478
|
opts[:connects].map!(&normalize)
|
|
427
|
-
opts[:endpoints].map!
|
|
479
|
+
opts[:endpoints].map!(&normalize_ep)
|
|
480
|
+
opts[:in_endpoints].map!(&normalize_ep)
|
|
481
|
+
opts[:out_endpoints].map!(&normalize_ep)
|
|
428
482
|
|
|
429
483
|
opts
|
|
430
484
|
end
|
|
@@ -436,8 +490,16 @@ module OMQ
|
|
|
436
490
|
type_name = opts[:type_name]
|
|
437
491
|
|
|
438
492
|
if type_name == "pipe"
|
|
439
|
-
|
|
493
|
+
has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
494
|
+
if has_in_out
|
|
495
|
+
abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
|
|
496
|
+
abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
|
|
497
|
+
abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
|
|
498
|
+
else
|
|
499
|
+
abort "pipe requires exactly 2 endpoints (pull-side and push-side), or use --in/--out" if opts[:endpoints].size != 2
|
|
500
|
+
end
|
|
440
501
|
else
|
|
502
|
+
abort "--in/--out are only valid for pipe" if opts[:in_endpoints].any? || opts[:out_endpoints].any?
|
|
441
503
|
abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
|
|
442
504
|
end
|
|
443
505
|
abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
|
|
@@ -447,11 +509,15 @@ module OMQ
|
|
|
447
509
|
abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
|
|
448
510
|
abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
|
|
449
511
|
abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
|
|
512
|
+
abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
|
|
513
|
+
abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
|
|
514
|
+
abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
|
|
450
515
|
|
|
451
516
|
if opts[:parallel]
|
|
452
517
|
abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
|
|
453
518
|
abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
|
|
454
|
-
|
|
519
|
+
all_pipe_eps = opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]
|
|
520
|
+
abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if all_pipe_eps.any?(&:bind?)
|
|
455
521
|
end
|
|
456
522
|
|
|
457
523
|
(opts[:connects] + opts[:binds]).each do |url|
|
data/lib/omq/version.rb
CHANGED
data/lib/omq/zmtp/engine.rb
CHANGED
|
@@ -215,29 +215,50 @@ module OMQ
|
|
|
215
215
|
# fast path when the connection is a DirectPipe.
|
|
216
216
|
#
|
|
217
217
|
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
218
|
+
# Starts a recv pump that dequeues messages from a connection
|
|
219
|
+
# and enqueues them into the routing strategy's recv queue.
|
|
220
|
+
#
|
|
221
|
+
# When a block is given, each message is yielded for transformation
|
|
222
|
+
# before enqueueing. The block is compiled at the call site, giving
|
|
223
|
+
# YJIT a monomorphic call per routing strategy instead of a shared
|
|
224
|
+
# megamorphic `transform.call` dispatch.
|
|
225
|
+
#
|
|
226
|
+
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
218
227
|
# @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
|
|
219
|
-
# @
|
|
228
|
+
# @yield [msg] optional per-message transform
|
|
220
229
|
# @return [#stop, nil] pump task handle, or nil for DirectPipe bypass
|
|
221
230
|
#
|
|
222
|
-
def start_recv_pump(conn, recv_queue, transform
|
|
231
|
+
def start_recv_pump(conn, recv_queue, &transform)
|
|
223
232
|
if conn.is_a?(Transport::Inproc::DirectPipe) && conn.peer
|
|
224
233
|
conn.peer.direct_recv_queue = recv_queue
|
|
225
234
|
conn.peer.direct_recv_transform = transform
|
|
226
235
|
return nil
|
|
227
236
|
end
|
|
228
237
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
if transform
|
|
239
|
+
Reactor.spawn_pump(annotation: "recv pump") do
|
|
240
|
+
loop do
|
|
241
|
+
msg = conn.receive_message
|
|
242
|
+
msg = transform.call(msg).freeze
|
|
243
|
+
recv_queue.enqueue(msg)
|
|
244
|
+
end
|
|
245
|
+
rescue Async::Stop
|
|
246
|
+
rescue ProtocolError, *CONNECTION_LOST
|
|
247
|
+
connection_lost(conn)
|
|
248
|
+
rescue => error
|
|
249
|
+
signal_fatal_error(error)
|
|
250
|
+
end
|
|
251
|
+
else
|
|
252
|
+
Reactor.spawn_pump(annotation: "recv pump") do
|
|
253
|
+
loop do
|
|
254
|
+
recv_queue.enqueue(conn.receive_message)
|
|
255
|
+
end
|
|
256
|
+
rescue Async::Stop
|
|
257
|
+
rescue ProtocolError, *CONNECTION_LOST
|
|
258
|
+
connection_lost(conn)
|
|
259
|
+
rescue => error
|
|
260
|
+
signal_fatal_error(error)
|
|
234
261
|
end
|
|
235
|
-
rescue Async::Stop
|
|
236
|
-
# normal shutdown
|
|
237
|
-
rescue ProtocolError, *CONNECTION_LOST
|
|
238
|
-
connection_lost(conn)
|
|
239
|
-
rescue => error
|
|
240
|
-
signal_fatal_error(error)
|
|
241
262
|
end
|
|
242
263
|
end
|
|
243
264
|
|
|
@@ -411,17 +432,14 @@ module OMQ
|
|
|
411
432
|
def setup_connection(io, as_server:, endpoint: nil, done: nil)
|
|
412
433
|
conn = Connection.new(
|
|
413
434
|
io,
|
|
414
|
-
socket_type:
|
|
415
|
-
identity:
|
|
416
|
-
as_server:
|
|
417
|
-
mechanism:
|
|
418
|
-
|
|
419
|
-
heartbeat_ttl: @options.heartbeat_ttl,
|
|
420
|
-
heartbeat_timeout: @options.heartbeat_timeout,
|
|
421
|
-
max_message_size: @options.max_message_size,
|
|
435
|
+
socket_type: @socket_type.to_s,
|
|
436
|
+
identity: @options.identity,
|
|
437
|
+
as_server: as_server,
|
|
438
|
+
mechanism: @options.mechanism&.dup,
|
|
439
|
+
max_message_size: @options.max_message_size,
|
|
422
440
|
)
|
|
423
441
|
conn.handshake!
|
|
424
|
-
conn
|
|
442
|
+
start_heartbeat(conn)
|
|
425
443
|
@connections << conn
|
|
426
444
|
@connection_endpoints[conn] = endpoint if endpoint
|
|
427
445
|
@connection_promises[conn] = done if done
|
|
@@ -433,6 +451,35 @@ module OMQ
|
|
|
433
451
|
end
|
|
434
452
|
|
|
435
453
|
|
|
454
|
+
# Spawns a heartbeat task for the connection.
|
|
455
|
+
# The connection only tracks timestamps — the engine drives the loop.
|
|
456
|
+
#
|
|
457
|
+
# @param conn [Connection]
|
|
458
|
+
# @return [void]
|
|
459
|
+
#
|
|
460
|
+
def start_heartbeat(conn)
|
|
461
|
+
interval = @options.heartbeat_interval
|
|
462
|
+
return unless interval
|
|
463
|
+
|
|
464
|
+
ttl = @options.heartbeat_ttl || interval
|
|
465
|
+
timeout = @options.heartbeat_timeout || interval
|
|
466
|
+
conn.touch_heartbeat
|
|
467
|
+
|
|
468
|
+
@tasks << Reactor.spawn_pump(annotation: "heartbeat") do
|
|
469
|
+
loop do
|
|
470
|
+
sleep interval
|
|
471
|
+
conn.send_command(Codec::Command.ping(ttl: ttl, context: "".b))
|
|
472
|
+
if conn.heartbeat_expired?(timeout)
|
|
473
|
+
conn.close
|
|
474
|
+
break
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
rescue *CONNECTION_LOST
|
|
478
|
+
# connection closed
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
|
|
436
483
|
# Spawns a background task that reconnects to the given endpoint
|
|
437
484
|
# with exponential back-off based on the reconnect_interval option.
|
|
438
485
|
#
|
|
@@ -34,8 +34,9 @@ module OMQ
|
|
|
34
34
|
routing_id = SecureRandom.bytes(4)
|
|
35
35
|
@connections_by_routing_id[routing_id] = connection
|
|
36
36
|
|
|
37
|
-
task = @engine.start_recv_pump(connection, @recv_queue
|
|
38
|
-
|
|
37
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
38
|
+
[routing_id, *msg]
|
|
39
|
+
end
|
|
39
40
|
@tasks << task if task
|
|
40
41
|
|
|
41
42
|
start_send_pump unless @send_pump_started
|
data/lib/omq/zmtp/routing/rep.rb
CHANGED
|
@@ -29,14 +29,13 @@ module OMQ
|
|
|
29
29
|
# @param connection [Connection]
|
|
30
30
|
#
|
|
31
31
|
def connection_added(connection)
|
|
32
|
-
|
|
32
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
33
33
|
delimiter = msg.index(&:empty?) || msg.size
|
|
34
34
|
envelope = msg[0, delimiter]
|
|
35
35
|
body = msg[(delimiter + 1)..] || []
|
|
36
36
|
@pending_replies << { conn: connection, envelope: envelope }
|
|
37
37
|
body
|
|
38
|
-
|
|
39
|
-
task = @engine.start_recv_pump(connection, @recv_queue, transform: transform)
|
|
38
|
+
end
|
|
40
39
|
@tasks << task if task
|
|
41
40
|
start_send_pump unless @send_pump_started
|
|
42
41
|
end
|
data/lib/omq/zmtp/routing/req.rb
CHANGED
|
@@ -28,8 +28,9 @@ module OMQ
|
|
|
28
28
|
def connection_added(connection)
|
|
29
29
|
@connections << connection
|
|
30
30
|
signal_connection_available
|
|
31
|
-
task = @engine.start_recv_pump(connection, @recv_queue
|
|
32
|
-
|
|
31
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
32
|
+
msg.first&.empty? ? msg[1..] : msg
|
|
33
|
+
end
|
|
33
34
|
@tasks << task if task
|
|
34
35
|
start_send_pump unless @send_pump_started
|
|
35
36
|
end
|
|
@@ -58,11 +59,6 @@ module OMQ
|
|
|
58
59
|
#
|
|
59
60
|
def transform_send(parts) = ["".b, *parts]
|
|
60
61
|
|
|
61
|
-
# REQ strips the leading empty delimiter frame on receive.
|
|
62
|
-
#
|
|
63
|
-
def transform_recv(msg)
|
|
64
|
-
msg.first&.empty? ? msg[1..] : msg
|
|
65
|
-
end
|
|
66
62
|
end
|
|
67
63
|
end
|
|
68
64
|
end
|