omq 0.5.0 → 0.6.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 +195 -0
- data/README.md +21 -19
- data/exe/omq +6 -0
- data/lib/omq/cli/base_runner.rb +423 -0
- data/lib/omq/cli/channel.rb +8 -0
- data/lib/omq/cli/client_server.rb +106 -0
- data/lib/omq/cli/config.rb +51 -0
- data/lib/omq/cli/formatter.rb +75 -0
- data/lib/omq/cli/pair.rb +31 -0
- data/lib/omq/cli/peer.rb +8 -0
- data/lib/omq/cli/pipe.rb +249 -0
- data/lib/omq/cli/pub_sub.rb +14 -0
- data/lib/omq/cli/push_pull.rb +14 -0
- data/lib/omq/cli/radio_dish.rb +27 -0
- data/lib/omq/cli/req_rep.rb +77 -0
- data/lib/omq/cli/router_dealer.rb +70 -0
- data/lib/omq/cli/scatter_gather.rb +14 -0
- data/lib/omq/cli.rb +444 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +2 -2
- data/lib/omq/socket.rb +74 -27
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/connection.rb +59 -12
- data/lib/omq/zmtp/engine.rb +179 -17
- data/lib/omq/zmtp/options.rb +4 -3
- data/lib/omq/zmtp/reactor.rb +25 -36
- data/lib/omq/zmtp/routing/channel.rb +14 -3
- data/lib/omq/zmtp/routing/fan_out.rb +52 -10
- data/lib/omq/zmtp/routing/pair.rb +14 -3
- data/lib/omq/zmtp/routing/peer.rb +28 -6
- data/lib/omq/zmtp/routing/push.rb +14 -7
- data/lib/omq/zmtp/routing/radio.rb +45 -12
- data/lib/omq/zmtp/routing/rep.rb +32 -13
- data/lib/omq/zmtp/routing/req.rb +1 -2
- data/lib/omq/zmtp/routing/round_robin.rb +72 -3
- data/lib/omq/zmtp/routing/router.rb +30 -10
- data/lib/omq/zmtp/routing/scatter.rb +16 -3
- data/lib/omq/zmtp/routing/server.rb +28 -6
- data/lib/omq/zmtp/routing/xsub.rb +7 -1
- data/lib/omq/zmtp/routing.rb +19 -0
- data/lib/omq/zmtp/transport/inproc.rb +48 -5
- data/lib/omq/zmtp/transport/ipc.rb +9 -7
- data/lib/omq/zmtp/transport/tcp.rb +14 -7
- data/lib/omq/zmtp/writable.rb +21 -4
- data/lib/omq.rb +7 -0
- metadata +18 -3
- data/exe/omqcat +0 -532
data/lib/omq/cli.rb
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "cli/config"
|
|
5
|
+
require_relative "cli/formatter"
|
|
6
|
+
require_relative "cli/base_runner"
|
|
7
|
+
require_relative "cli/push_pull"
|
|
8
|
+
require_relative "cli/pub_sub"
|
|
9
|
+
require_relative "cli/scatter_gather"
|
|
10
|
+
require_relative "cli/radio_dish"
|
|
11
|
+
require_relative "cli/req_rep"
|
|
12
|
+
require_relative "cli/pair"
|
|
13
|
+
require_relative "cli/router_dealer"
|
|
14
|
+
require_relative "cli/channel"
|
|
15
|
+
require_relative "cli/client_server"
|
|
16
|
+
require_relative "cli/peer"
|
|
17
|
+
require_relative "cli/pipe"
|
|
18
|
+
|
|
19
|
+
module OMQ
|
|
20
|
+
module CLI
|
|
21
|
+
SOCKET_TYPE_NAMES = %w[
|
|
22
|
+
req rep pub sub push pull pair dealer router
|
|
23
|
+
client server radio dish scatter gather channel peer
|
|
24
|
+
pipe
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RUNNER_MAP = {
|
|
29
|
+
"push" => [PushRunner, :PUSH],
|
|
30
|
+
"pull" => [PullRunner, :PULL],
|
|
31
|
+
"pub" => [PubRunner, :PUB],
|
|
32
|
+
"sub" => [SubRunner, :SUB],
|
|
33
|
+
"req" => [ReqRunner, :REQ],
|
|
34
|
+
"rep" => [RepRunner, :REP],
|
|
35
|
+
"dealer" => [DealerRunner, :DEALER],
|
|
36
|
+
"router" => [RouterRunner, :ROUTER],
|
|
37
|
+
"pair" => [PairRunner, :PAIR],
|
|
38
|
+
"client" => [ClientRunner, :CLIENT],
|
|
39
|
+
"server" => [ServerRunner, :SERVER],
|
|
40
|
+
"radio" => [RadioRunner, :RADIO],
|
|
41
|
+
"dish" => [DishRunner, :DISH],
|
|
42
|
+
"scatter" => [ScatterRunner, :SCATTER],
|
|
43
|
+
"gather" => [GatherRunner, :GATHER],
|
|
44
|
+
"channel" => [ChannelRunner, :CHANNEL],
|
|
45
|
+
"peer" => [PeerRunner, :PEER],
|
|
46
|
+
"pipe" => [PipeRunner, nil],
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
EXAMPLES = <<~'TEXT'
|
|
51
|
+
── Request / Reply ──────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
┌─────┐ "hello" ┌─────┐
|
|
54
|
+
│ REQ │────────────→│ REP │
|
|
55
|
+
│ │←────────────│ │
|
|
56
|
+
└─────┘ "HELLO" └─────┘
|
|
57
|
+
|
|
58
|
+
# terminal 1: echo server
|
|
59
|
+
omq rep --bind tcp://:5555 --eval '$F.map(&:upcase)'
|
|
60
|
+
|
|
61
|
+
# terminal 2: send a request
|
|
62
|
+
echo "hello" | omq req --connect tcp://localhost:5555
|
|
63
|
+
|
|
64
|
+
# or over IPC (unix socket, single machine)
|
|
65
|
+
omq rep --bind ipc:///tmp/echo.sock --echo &
|
|
66
|
+
echo "hello" | omq req --connect ipc:///tmp/echo.sock
|
|
67
|
+
|
|
68
|
+
── Publish / Subscribe ──────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
┌─────┐ "weather.nyc 72F" ┌─────┐
|
|
71
|
+
│ PUB │────────────────────→│ SUB │ --subscribe "weather."
|
|
72
|
+
└─────┘ └─────┘
|
|
73
|
+
|
|
74
|
+
# terminal 1: subscriber (all topics by default)
|
|
75
|
+
omq sub --bind tcp://:5556
|
|
76
|
+
|
|
77
|
+
# terminal 2: publisher (needs --delay for subscription to propagate)
|
|
78
|
+
echo "weather.nyc 72F" | omq pub --connect tcp://localhost:5556 --delay 1
|
|
79
|
+
|
|
80
|
+
── Periodic Publish ───────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
┌─────┐ "tick 1" ┌─────┐
|
|
83
|
+
│ PUB │──(every 1s)─→│ SUB │
|
|
84
|
+
└─────┘ └─────┘
|
|
85
|
+
|
|
86
|
+
# terminal 1: subscriber
|
|
87
|
+
omq sub --bind tcp://:5556
|
|
88
|
+
|
|
89
|
+
# terminal 2: publish a tick every second (wall-clock aligned)
|
|
90
|
+
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
91
|
+
--data "tick" --interval 1
|
|
92
|
+
|
|
93
|
+
# 5 ticks, then exit
|
|
94
|
+
omq pub --connect tcp://localhost:5556 --delay 1 \
|
|
95
|
+
--data "tick" --interval 1 --count 5
|
|
96
|
+
|
|
97
|
+
── Pipeline ─────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
┌──────┐ ┌──────┐
|
|
100
|
+
│ PUSH │──────────→│ PULL │
|
|
101
|
+
└──────┘ └──────┘
|
|
102
|
+
|
|
103
|
+
# terminal 1: worker
|
|
104
|
+
omq pull --bind tcp://:5557
|
|
105
|
+
|
|
106
|
+
# terminal 2: send tasks
|
|
107
|
+
echo "task 1" | omq push --connect tcp://localhost:5557
|
|
108
|
+
|
|
109
|
+
# or over IPC (unix socket)
|
|
110
|
+
omq pull --bind ipc:///tmp/pipeline.sock &
|
|
111
|
+
echo "task 1" | omq push --connect ipc:///tmp/pipeline.sock
|
|
112
|
+
|
|
113
|
+
── CLIENT / SERVER (draft) ──────────────────────────────────
|
|
114
|
+
|
|
115
|
+
┌────────┐ "hello" ┌────────┐
|
|
116
|
+
│ CLIENT │───────────→│ SERVER │ --eval '$F.map(&:upcase)'
|
|
117
|
+
│ │←───────────│ │
|
|
118
|
+
└────────┘ "HELLO" └────────┘
|
|
119
|
+
|
|
120
|
+
# terminal 1: upcasing server
|
|
121
|
+
omq server --bind tcp://:5555 --eval '$F.map(&:upcase)'
|
|
122
|
+
|
|
123
|
+
# terminal 2: client
|
|
124
|
+
echo "hello" | omq client --connect tcp://localhost:5555
|
|
125
|
+
|
|
126
|
+
── Formats ──────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
# ascii (default) — non-printable replaced with dots
|
|
129
|
+
omq pull --bind tcp://:5557 --ascii
|
|
130
|
+
|
|
131
|
+
# quoted — lossless, round-trippable (uses String#dump escaping)
|
|
132
|
+
omq pull --bind tcp://:5557 --quoted
|
|
133
|
+
|
|
134
|
+
# JSON Lines — structured, multipart as arrays
|
|
135
|
+
echo '["key","value"]' | omq push --connect tcp://localhost:5557 --jsonl
|
|
136
|
+
omq pull --bind tcp://:5557 --jsonl
|
|
137
|
+
|
|
138
|
+
# multipart via tabs
|
|
139
|
+
printf "routing-key\tpayload" | omq push --connect tcp://localhost:5557
|
|
140
|
+
|
|
141
|
+
── Compression ──────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
# both sides must use --compress
|
|
144
|
+
omq pull --bind tcp://:5557 --compress &
|
|
145
|
+
echo "compressible data" | omq push --connect tcp://localhost:5557 --compress
|
|
146
|
+
|
|
147
|
+
── CURVE Encryption ─────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
# server (prints OMQ_SERVER_KEY=...)
|
|
150
|
+
omq rep --bind tcp://:5555 --echo --curve-server
|
|
151
|
+
|
|
152
|
+
# client (paste the server's key)
|
|
153
|
+
echo "secret" | omq req --connect tcp://localhost:5555 \
|
|
154
|
+
--curve-server-key '<key from server>'
|
|
155
|
+
|
|
156
|
+
── ROUTER / DEALER ──────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
┌────────┐ ┌────────┐
|
|
159
|
+
│ DEALER │─────────→│ ROUTER │
|
|
160
|
+
│ id=w1 │ │ │
|
|
161
|
+
└────────┘ └────────┘
|
|
162
|
+
|
|
163
|
+
# terminal 1: router shows identity + message
|
|
164
|
+
omq router --bind tcp://:5555
|
|
165
|
+
|
|
166
|
+
# terminal 2: dealer with identity
|
|
167
|
+
echo "hello" | omq dealer --connect tcp://localhost:5555 \
|
|
168
|
+
--identity worker-1
|
|
169
|
+
|
|
170
|
+
── Ruby Eval ────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
# filter: only pass messages containing "error"
|
|
173
|
+
omq pull --bind tcp://:5557 \
|
|
174
|
+
--eval '$F.first.include?("error") ? $F : nil'
|
|
175
|
+
|
|
176
|
+
# transform with gems
|
|
177
|
+
omq sub --connect tcp://localhost:5556 --require json \
|
|
178
|
+
--eval 'JSON.parse($F.first)["temperature"]'
|
|
179
|
+
|
|
180
|
+
# require a local file, use its methods in --eval
|
|
181
|
+
omq rep --bind tcp://:5555 --require ./transform.rb \
|
|
182
|
+
--eval 'upcase_all($F)'
|
|
183
|
+
|
|
184
|
+
# next skips, break stops — regexps match against $_
|
|
185
|
+
omq pull --bind tcp://:5557 \
|
|
186
|
+
--eval 'next if /^#/; break if /quit/; $F'
|
|
187
|
+
|
|
188
|
+
# BEGIN/END blocks (like awk) — accumulate and summarize
|
|
189
|
+
omq pull --bind tcp://:5557 \
|
|
190
|
+
--eval 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
|
|
191
|
+
TEXT
|
|
192
|
+
|
|
193
|
+
module_function
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Displays text through the system pager, or prints directly
|
|
197
|
+
# when stdout is not a terminal.
|
|
198
|
+
#
|
|
199
|
+
def page(text)
|
|
200
|
+
if $stdout.tty?
|
|
201
|
+
if ENV["PAGER"]
|
|
202
|
+
pager = ENV["PAGER"]
|
|
203
|
+
else
|
|
204
|
+
ENV["LESS"] ||= "-FR"
|
|
205
|
+
pager = "less"
|
|
206
|
+
end
|
|
207
|
+
IO.popen(pager, "w") { |io| io.puts text }
|
|
208
|
+
else
|
|
209
|
+
puts text
|
|
210
|
+
end
|
|
211
|
+
rescue Errno::ENOENT
|
|
212
|
+
puts text
|
|
213
|
+
rescue Errno::EPIPE
|
|
214
|
+
# user quit pager early
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# Parses CLI arguments, validates options, and runs the main
|
|
219
|
+
# event loop inside an Async reactor.
|
|
220
|
+
#
|
|
221
|
+
def run(argv = ARGV)
|
|
222
|
+
config = build_config(argv)
|
|
223
|
+
|
|
224
|
+
require_relative "../omq"
|
|
225
|
+
require "async"
|
|
226
|
+
require "json"
|
|
227
|
+
require "console"
|
|
228
|
+
|
|
229
|
+
validate_gems!(config)
|
|
230
|
+
|
|
231
|
+
trap("INT") { Process.exit!(0) }
|
|
232
|
+
trap("TERM") { Process.exit!(0) }
|
|
233
|
+
|
|
234
|
+
Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
|
|
235
|
+
|
|
236
|
+
runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
|
|
237
|
+
|
|
238
|
+
Async do |task|
|
|
239
|
+
runner = if socket_sym
|
|
240
|
+
runner_class.new(config, OMQ.const_get(socket_sym))
|
|
241
|
+
else
|
|
242
|
+
runner_class.new(config)
|
|
243
|
+
end
|
|
244
|
+
runner.call(task)
|
|
245
|
+
rescue IO::TimeoutError, Async::TimeoutError
|
|
246
|
+
$stderr.puts "omq: timeout" unless config.quiet
|
|
247
|
+
exit 2
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# Builds a frozen Config from command-line arguments.
|
|
253
|
+
#
|
|
254
|
+
def build_config(argv)
|
|
255
|
+
opts = parse_options(argv)
|
|
256
|
+
validate!(opts)
|
|
257
|
+
|
|
258
|
+
opts[:has_msgpack] = begin; require "msgpack"; true; rescue LoadError; false; end
|
|
259
|
+
opts[:has_zstd] = begin; require "zstd-ruby"; true; rescue LoadError; false; end
|
|
260
|
+
opts[:stdin_is_tty] = $stdin.tty?
|
|
261
|
+
|
|
262
|
+
Ractor.make_shareable(Config.new(**opts))
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Parses command-line arguments into a mutable options hash.
|
|
267
|
+
#
|
|
268
|
+
def parse_options(argv)
|
|
269
|
+
opts = {
|
|
270
|
+
endpoints: [],
|
|
271
|
+
connects: [],
|
|
272
|
+
binds: [],
|
|
273
|
+
data: nil,
|
|
274
|
+
file: nil,
|
|
275
|
+
format: :ascii,
|
|
276
|
+
subscribes: [],
|
|
277
|
+
joins: [],
|
|
278
|
+
group: nil,
|
|
279
|
+
identity: nil,
|
|
280
|
+
target: nil,
|
|
281
|
+
interval: nil,
|
|
282
|
+
count: nil,
|
|
283
|
+
delay: nil,
|
|
284
|
+
timeout: nil,
|
|
285
|
+
linger: 5,
|
|
286
|
+
reconnect_ivl: nil,
|
|
287
|
+
heartbeat_ivl: nil,
|
|
288
|
+
conflate: false,
|
|
289
|
+
compress: false,
|
|
290
|
+
expr: nil,
|
|
291
|
+
parallel: nil,
|
|
292
|
+
transient: false,
|
|
293
|
+
verbose: false,
|
|
294
|
+
quiet: false,
|
|
295
|
+
echo: false,
|
|
296
|
+
curve_server: false,
|
|
297
|
+
curve_server_key: nil,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
parser = OptionParser.new do |o|
|
|
301
|
+
o.banner = "Usage: omq TYPE [options]\n\n" \
|
|
302
|
+
"Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
|
|
303
|
+
"Draft: client, server, radio, dish, scatter, gather, channel, peer\n\n"
|
|
304
|
+
|
|
305
|
+
o.separator "Connection:"
|
|
306
|
+
o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, false); opts[:connects] << v }
|
|
307
|
+
o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, true); opts[:binds] << v }
|
|
308
|
+
|
|
309
|
+
o.separator "\nData source (REP: reply source):"
|
|
310
|
+
o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
|
|
311
|
+
o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
|
|
312
|
+
o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
|
|
313
|
+
|
|
314
|
+
o.separator "\nFormat (input + output):"
|
|
315
|
+
o.on("-A", "--ascii", "Tab-separated frames, safe ASCII (default)") { opts[:format] = :ascii }
|
|
316
|
+
o.on("-Q", "--quoted", "C-style quoted with escapes") { opts[:format] = :quoted }
|
|
317
|
+
o.on( "--raw", "Raw binary, no framing") { opts[:format] = :raw }
|
|
318
|
+
o.on("-J", "--jsonl", "JSON Lines (array of strings per line)") { opts[:format] = :jsonl }
|
|
319
|
+
o.on( "--msgpack", "MessagePack arrays (binary stream)") { opts[:format] = :msgpack }
|
|
320
|
+
o.on("-M", "--marshal", "Ruby Marshal stream (binary, Array<String>)") { opts[:format] = :marshal }
|
|
321
|
+
|
|
322
|
+
o.separator "\nSubscription/groups:"
|
|
323
|
+
o.on("-s", "--subscribe PREFIX", "Subscribe prefix (SUB, default all)") { |v| opts[:subscribes] << v }
|
|
324
|
+
o.on("-j", "--join GROUP", "Join group (repeatable, DISH only)") { |v| opts[:joins] << v }
|
|
325
|
+
o.on("-g", "--group GROUP", "Publish group (RADIO only)") { |v| opts[:group] = v }
|
|
326
|
+
|
|
327
|
+
o.separator "\nIdentity/routing:"
|
|
328
|
+
o.on("--identity ID", "Set socket identity (DEALER/ROUTER)") { |v| opts[:identity] = v }
|
|
329
|
+
o.on("--target ID", "Target peer (ROUTER/SERVER/PEER, 0x prefix for binary)") { |v| opts[:target] = v }
|
|
330
|
+
|
|
331
|
+
o.separator "\nTiming:"
|
|
332
|
+
o.on("-i", "--interval SECS", Float, "Repeat interval") { |v| opts[:interval] = v }
|
|
333
|
+
o.on("-n", "--count COUNT", Integer, "Max iterations (0=inf)") { |v| opts[:count] = v }
|
|
334
|
+
o.on("-d", "--delay SECS", Float, "Delay before first send") { |v| opts[:delay] = v }
|
|
335
|
+
o.on("-t", "--timeout SECS", Float, "Send/receive timeout") { |v| opts[:timeout] = v }
|
|
336
|
+
o.on("-l", "--linger SECS", Float, "Drain time on close (default 5)") { |v| opts[:linger] = v }
|
|
337
|
+
o.on("--reconnect-ivl IVL", "Reconnect interval: SECS or MIN..MAX (default 0.1)") { |v|
|
|
338
|
+
opts[:reconnect_ivl] = if v.include?("..")
|
|
339
|
+
lo, hi = v.split("..", 2)
|
|
340
|
+
Float(lo)..Float(hi)
|
|
341
|
+
else
|
|
342
|
+
Float(v)
|
|
343
|
+
end
|
|
344
|
+
}
|
|
345
|
+
o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
|
|
346
|
+
|
|
347
|
+
o.separator "\nDelivery:"
|
|
348
|
+
o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
|
|
349
|
+
|
|
350
|
+
o.separator "\nCompression:"
|
|
351
|
+
o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
|
|
352
|
+
|
|
353
|
+
o.separator "\nProcessing:"
|
|
354
|
+
o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
|
|
355
|
+
o.on("-r", "--require LIB", "Require library or file (-r./lib.rb)") { |v|
|
|
356
|
+
v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
|
|
357
|
+
}
|
|
358
|
+
o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
|
|
359
|
+
require "etc"
|
|
360
|
+
opts[:parallel] = v || Etc.nprocessors
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
o.separator "\nCURVE encryption (requires omq-curve gem):"
|
|
364
|
+
o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
|
|
365
|
+
o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
|
|
366
|
+
o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
|
|
367
|
+
|
|
368
|
+
o.separator "\nOther:"
|
|
369
|
+
o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
|
|
370
|
+
o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
|
|
371
|
+
o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
|
|
372
|
+
o.on("-V", "--version") { require_relative "../omq"; puts "omq #{OMQ::VERSION}"; exit }
|
|
373
|
+
o.on("-h") { puts o; exit }
|
|
374
|
+
o.on( "--help") { page "#{o}\n#{EXAMPLES}"; exit }
|
|
375
|
+
o.on( "--examples") { page EXAMPLES; exit }
|
|
376
|
+
|
|
377
|
+
o.separator "\nExit codes: 0 = success, 1 = error, 2 = timeout"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
begin
|
|
381
|
+
parser.parse!(argv)
|
|
382
|
+
rescue OptionParser::ParseError => e
|
|
383
|
+
abort e.message
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
type_name = argv.shift
|
|
387
|
+
abort parser.to_s unless type_name
|
|
388
|
+
unless SOCKET_TYPE_NAMES.include?(type_name.downcase)
|
|
389
|
+
abort "Unknown socket type: #{type_name}. Known: #{SOCKET_TYPE_NAMES.join(', ')}"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
opts[:type_name] = type_name.downcase
|
|
393
|
+
|
|
394
|
+
normalize = ->(url) { url.sub(%r{\Atcp://:}, "tcp://*:") }
|
|
395
|
+
opts[:connects].map!(&normalize)
|
|
396
|
+
opts[:binds].map!(&normalize)
|
|
397
|
+
opts[:endpoints].map! { |ep| Endpoint.new(normalize.call(ep.url), ep.bind?) }
|
|
398
|
+
|
|
399
|
+
opts
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# Validates option combinations.
|
|
404
|
+
#
|
|
405
|
+
def validate!(opts)
|
|
406
|
+
type_name = opts[:type_name]
|
|
407
|
+
|
|
408
|
+
if type_name == "pipe"
|
|
409
|
+
abort "pipe requires exactly 2 endpoints (pull-side and push-side)" if opts[:endpoints].size != 2
|
|
410
|
+
else
|
|
411
|
+
abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
|
|
412
|
+
end
|
|
413
|
+
abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
|
|
414
|
+
abort "--subscribe is only valid for SUB" if !opts[:subscribes].empty? && type_name != "sub"
|
|
415
|
+
abort "--join is only valid for DISH" if !opts[:joins].empty? && type_name != "dish"
|
|
416
|
+
abort "--group is only valid for RADIO" if opts[:group] && type_name != "radio"
|
|
417
|
+
abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
|
|
418
|
+
abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
|
|
419
|
+
abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
|
|
420
|
+
|
|
421
|
+
if opts[:parallel]
|
|
422
|
+
abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
|
|
423
|
+
abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
|
|
424
|
+
abort "-P/--parallel requires both endpoints to use --connect (not --bind)" if opts[:endpoints].any?(&:bind?)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
(opts[:connects] + opts[:binds]).each do |url|
|
|
428
|
+
abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# Validates that required gems are available.
|
|
434
|
+
#
|
|
435
|
+
def validate_gems!(config)
|
|
436
|
+
abort "--msgpack requires the msgpack gem" if config.format == :msgpack && !config.has_msgpack
|
|
437
|
+
abort "--compress requires the zstd-ruby gem" if config.compress && !config.has_zstd
|
|
438
|
+
|
|
439
|
+
if config.recv_only? && (config.data || config.file)
|
|
440
|
+
abort "--data/--file not valid for #{config.type_name} (receive-only)"
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
data/lib/omq/pub_sub.rb
CHANGED
|
@@ -4,8 +4,8 @@ module OMQ
|
|
|
4
4
|
class PUB < Socket
|
|
5
5
|
include ZMTP::Writable
|
|
6
6
|
|
|
7
|
-
def initialize(endpoints = nil, linger: 0)
|
|
8
|
-
_init_engine(:PUB, linger: linger)
|
|
7
|
+
def initialize(endpoints = nil, linger: 0, conflate: false)
|
|
8
|
+
_init_engine(:PUB, linger: linger, conflate: conflate)
|
|
9
9
|
_attach(endpoints, default: :bind)
|
|
10
10
|
end
|
|
11
11
|
end
|
data/lib/omq/radio_dish.rb
CHANGED
|
@@ -4,8 +4,8 @@ module OMQ
|
|
|
4
4
|
class RADIO < Socket
|
|
5
5
|
include ZMTP::Writable
|
|
6
6
|
|
|
7
|
-
def initialize(endpoints = nil, linger: 0)
|
|
8
|
-
_init_engine(:RADIO, linger: linger)
|
|
7
|
+
def initialize(endpoints = nil, linger: 0, conflate: false)
|
|
8
|
+
_init_engine(:RADIO, linger: linger, conflate: conflate)
|
|
9
9
|
_attach(endpoints, default: :bind)
|
|
10
10
|
end
|
|
11
11
|
|
data/lib/omq/socket.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
3
5
|
module OMQ
|
|
4
6
|
# Socket base class.
|
|
5
7
|
#
|
|
@@ -8,32 +10,34 @@ module OMQ
|
|
|
8
10
|
#
|
|
9
11
|
attr_reader :options
|
|
10
12
|
|
|
13
|
+
|
|
11
14
|
# @return [Integer, nil] last auto-selected TCP port
|
|
12
15
|
#
|
|
13
16
|
attr_reader :last_tcp_port
|
|
14
17
|
|
|
18
|
+
|
|
15
19
|
# Delegate socket option accessors to @options.
|
|
16
20
|
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
def_delegators :@options,
|
|
24
|
+
:send_hwm, :send_hwm=,
|
|
25
|
+
:recv_hwm, :recv_hwm=,
|
|
26
|
+
:linger, :linger=,
|
|
27
|
+
:identity, :identity=,
|
|
28
|
+
:recv_timeout, :recv_timeout=,
|
|
29
|
+
:send_timeout, :send_timeout=,
|
|
30
|
+
:read_timeout, :read_timeout=,
|
|
31
|
+
:write_timeout, :write_timeout=,
|
|
32
|
+
:router_mandatory, :router_mandatory=,
|
|
33
|
+
:router_mandatory?,
|
|
34
|
+
:reconnect_interval, :reconnect_interval=,
|
|
35
|
+
:heartbeat_interval, :heartbeat_interval=,
|
|
36
|
+
:heartbeat_ttl, :heartbeat_ttl=,
|
|
37
|
+
:heartbeat_timeout, :heartbeat_timeout=,
|
|
38
|
+
:max_message_size, :max_message_size=,
|
|
39
|
+
:mechanism, :mechanism=
|
|
40
|
+
|
|
37
41
|
|
|
38
42
|
# Creates a new socket and binds it to the given endpoint.
|
|
39
43
|
#
|
|
@@ -45,6 +49,7 @@ module OMQ
|
|
|
45
49
|
new(nil, **opts).tap { |s| s.bind(endpoint) }
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
|
|
48
53
|
# Creates a new socket and connects it to the given endpoint.
|
|
49
54
|
#
|
|
50
55
|
# @param endpoint [String]
|
|
@@ -55,8 +60,10 @@ module OMQ
|
|
|
55
60
|
new(nil, **opts).tap { |s| s.connect(endpoint) }
|
|
56
61
|
end
|
|
57
62
|
|
|
63
|
+
|
|
58
64
|
def initialize(endpoints = nil, linger: 0); end
|
|
59
65
|
|
|
66
|
+
|
|
60
67
|
# Binds to an endpoint.
|
|
61
68
|
#
|
|
62
69
|
# @param endpoint [String]
|
|
@@ -67,6 +74,7 @@ module OMQ
|
|
|
67
74
|
@last_tcp_port = @engine.last_tcp_port
|
|
68
75
|
end
|
|
69
76
|
|
|
77
|
+
|
|
70
78
|
# Connects to an endpoint.
|
|
71
79
|
#
|
|
72
80
|
# @param endpoint [String]
|
|
@@ -76,6 +84,7 @@ module OMQ
|
|
|
76
84
|
@engine.connect(endpoint)
|
|
77
85
|
end
|
|
78
86
|
|
|
87
|
+
|
|
79
88
|
# Disconnects from an endpoint.
|
|
80
89
|
#
|
|
81
90
|
# @param endpoint [String]
|
|
@@ -85,6 +94,7 @@ module OMQ
|
|
|
85
94
|
@engine.disconnect(endpoint)
|
|
86
95
|
end
|
|
87
96
|
|
|
97
|
+
|
|
88
98
|
# Unbinds from an endpoint.
|
|
89
99
|
#
|
|
90
100
|
# @param endpoint [String]
|
|
@@ -94,21 +104,52 @@ module OMQ
|
|
|
94
104
|
@engine.unbind(endpoint)
|
|
95
105
|
end
|
|
96
106
|
|
|
107
|
+
|
|
97
108
|
# @return [String, nil] last bound endpoint
|
|
98
109
|
#
|
|
99
110
|
def last_endpoint
|
|
100
111
|
@engine.last_endpoint
|
|
101
112
|
end
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
|
|
115
|
+
# @return [Async::Promise] resolves when first peer completes handshake
|
|
116
|
+
def peer_connected = @engine.peer_connected
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# @return [Async::Promise] resolves when first subscriber joins (PUB/XPUB only)
|
|
120
|
+
def subscriber_joined = @engine.routing.subscriber_joined
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# @return [Async::Promise] resolves when all peers disconnect (after having had peers)
|
|
124
|
+
def all_peers_gone = @engine.all_peers_gone
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# @return [Integer] current number of peer connections
|
|
128
|
+
def connection_count = @engine.connections.size
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Signals end-of-stream on the receive side. A subsequent
|
|
132
|
+
# +#receive+ call that would otherwise block returns +nil+.
|
|
104
133
|
#
|
|
105
|
-
|
|
134
|
+
def close_read
|
|
135
|
+
@engine.dequeue_recv_sentinel
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Disable auto-reconnect for connected endpoints.
|
|
140
|
+
def reconnect_enabled=(val)
|
|
141
|
+
@engine.reconnect_enabled = val
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Closes the socket.
|
|
106
146
|
#
|
|
107
147
|
def close
|
|
108
148
|
@engine.close
|
|
109
149
|
nil
|
|
110
150
|
end
|
|
111
151
|
|
|
152
|
+
|
|
112
153
|
# Set socket to use unbounded pipes (HWM=0).
|
|
113
154
|
#
|
|
114
155
|
def set_unbounded
|
|
@@ -117,14 +158,17 @@ module OMQ
|
|
|
117
158
|
nil
|
|
118
159
|
end
|
|
119
160
|
|
|
161
|
+
|
|
120
162
|
# @return [String]
|
|
121
163
|
#
|
|
122
164
|
def inspect
|
|
123
165
|
format("#<%s last_endpoint=%p>", self.class, last_endpoint)
|
|
124
166
|
end
|
|
125
167
|
|
|
168
|
+
|
|
126
169
|
private
|
|
127
170
|
|
|
171
|
+
|
|
128
172
|
# Runs a block with a timeout. Uses Async's with_timeout if inside
|
|
129
173
|
# a reactor, otherwise falls back to Timeout.timeout.
|
|
130
174
|
#
|
|
@@ -142,6 +186,7 @@ module OMQ
|
|
|
142
186
|
raise IO::TimeoutError, "timed out"
|
|
143
187
|
end
|
|
144
188
|
|
|
189
|
+
|
|
145
190
|
# Connects or binds based on endpoint prefix convention.
|
|
146
191
|
#
|
|
147
192
|
# @param endpoints [String, nil]
|
|
@@ -159,18 +204,20 @@ module OMQ
|
|
|
159
204
|
end
|
|
160
205
|
end
|
|
161
206
|
|
|
207
|
+
|
|
162
208
|
# Initializes engine and options for a socket type.
|
|
163
209
|
#
|
|
164
210
|
# @param socket_type [Symbol]
|
|
165
211
|
# @param linger [Integer]
|
|
166
212
|
#
|
|
167
213
|
def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
|
|
168
|
-
send_timeout: nil, recv_timeout: nil)
|
|
214
|
+
send_timeout: nil, recv_timeout: nil, conflate: false)
|
|
169
215
|
@options = ZMTP::Options.new(linger: linger)
|
|
170
|
-
@options.send_hwm
|
|
171
|
-
@options.recv_hwm
|
|
172
|
-
@options.send_timeout
|
|
173
|
-
@options.recv_timeout
|
|
216
|
+
@options.send_hwm = send_hwm if send_hwm
|
|
217
|
+
@options.recv_hwm = recv_hwm if recv_hwm
|
|
218
|
+
@options.send_timeout = send_timeout if send_timeout
|
|
219
|
+
@options.recv_timeout = recv_timeout if recv_timeout
|
|
220
|
+
@options.conflate = conflate
|
|
174
221
|
@engine = ZMTP::Engine.new(socket_type, @options)
|
|
175
222
|
end
|
|
176
223
|
end
|
data/lib/omq/version.rb
CHANGED