nnq-cli 0.2.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 +7 -0
- data/CHANGELOG.md +78 -0
- data/LICENSE +15 -0
- data/README.md +391 -0
- data/exe/nnq +10 -0
- data/lib/nnq/cli/base_runner.rb +448 -0
- data/lib/nnq/cli/bus.rb +33 -0
- data/lib/nnq/cli/cli_parser.rb +485 -0
- data/lib/nnq/cli/config.rb +59 -0
- data/lib/nnq/cli/expression_evaluator.rb +142 -0
- data/lib/nnq/cli/formatter.rb +140 -0
- data/lib/nnq/cli/pair.rb +33 -0
- data/lib/nnq/cli/pipe.rb +206 -0
- data/lib/nnq/cli/pipe_worker.rb +138 -0
- data/lib/nnq/cli/pub_sub.rb +16 -0
- data/lib/nnq/cli/push_pull.rb +19 -0
- data/lib/nnq/cli/ractor_helpers.rb +81 -0
- data/lib/nnq/cli/req_rep.rb +105 -0
- data/lib/nnq/cli/socket_setup.rb +93 -0
- data/lib/nnq/cli/surveyor_respondent.rb +112 -0
- data/lib/nnq/cli/term.rb +86 -0
- data/lib/nnq/cli/transient_monitor.rb +41 -0
- data/lib/nnq/cli/version.rb +7 -0
- data/lib/nnq/cli.rb +190 -0
- metadata +110 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Template runner base class for all socket-type CLI runners.
|
|
6
|
+
# Subclasses override {#run_loop} to implement socket-specific behaviour.
|
|
7
|
+
#
|
|
8
|
+
# nnq carries one String body per message (no multipart). The runner
|
|
9
|
+
# protocol wraps it in a 1-element Array internally so that eval
|
|
10
|
+
# expressions using `$F`/`$_` work the same way as in omq-cli, and
|
|
11
|
+
# unwraps to a bare String at the send boundary.
|
|
12
|
+
class BaseRunner
|
|
13
|
+
# @return [Config] frozen CLI configuration
|
|
14
|
+
# @return [Object] the NNQ socket instance
|
|
15
|
+
attr_reader :config, :sock
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# @param config [Config] frozen CLI configuration
|
|
19
|
+
# @param socket_class [Class] NNQ socket class to instantiate (e.g. NNQ::PUSH)
|
|
20
|
+
def initialize(config, socket_class)
|
|
21
|
+
@config = config
|
|
22
|
+
@klass = socket_class
|
|
23
|
+
@fmt = Formatter.new(config.format, compress: config.compress)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Runs the full lifecycle: socket setup, peer wait, BEGIN/END blocks, and the main loop.
|
|
28
|
+
#
|
|
29
|
+
# @param task [Async::Task] the parent async task
|
|
30
|
+
# @return [void]
|
|
31
|
+
def call(task)
|
|
32
|
+
set_process_title
|
|
33
|
+
setup_socket
|
|
34
|
+
start_event_monitor if config.verbose >= 2
|
|
35
|
+
maybe_start_transient_monitor(task)
|
|
36
|
+
sleep(config.delay) if config.delay && config.recv_only?
|
|
37
|
+
wait_for_peer if needs_peer_wait?
|
|
38
|
+
run_begin_blocks
|
|
39
|
+
run_loop(task)
|
|
40
|
+
run_end_blocks
|
|
41
|
+
ensure
|
|
42
|
+
@sock&.close
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Subclasses override this.
|
|
50
|
+
def run_loop(task)
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# -- Socket creation ---------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def setup_socket
|
|
59
|
+
@sock = create_socket
|
|
60
|
+
attach_endpoints
|
|
61
|
+
setup_subscriptions
|
|
62
|
+
compile_expr
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_socket
|
|
67
|
+
SocketSetup.build(@klass, config)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def attach_endpoints
|
|
72
|
+
SocketSetup.attach(@sock, config, verbose: config.verbose)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# -- Transient disconnect monitor --------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def maybe_start_transient_monitor(task)
|
|
80
|
+
return unless config.transient
|
|
81
|
+
@transient_monitor = TransientMonitor.new(@sock, config, task, method(:log))
|
|
82
|
+
Async::Task.current.yield # let monitor start waiting
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def transient_ready!
|
|
87
|
+
@transient_monitor&.ready!
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# -- BEGIN / END blocks ------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def run_begin_blocks
|
|
95
|
+
@sock.instance_exec(&@send_begin_proc) if @send_begin_proc
|
|
96
|
+
@sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_end_blocks
|
|
101
|
+
@sock.instance_exec(&@send_end_proc) if @send_end_proc
|
|
102
|
+
@sock.instance_exec(&@recv_end_proc) if @recv_end_proc
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# -- Peer wait with grace period ---------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def needs_peer_wait?
|
|
110
|
+
return false if config.recv_only?
|
|
111
|
+
return true if config.connects.any?
|
|
112
|
+
|
|
113
|
+
# Bind-mode senders with a bounded or scheduled send plan:
|
|
114
|
+
# wait for the first peer so a one-shot `-d` / `-E` doesn't
|
|
115
|
+
# just queue into HWM and then exit before anyone is
|
|
116
|
+
# listening. Interactive stdin still goes through unwaited
|
|
117
|
+
# so typing isn't gated on a peer.
|
|
118
|
+
config.binds.any? && bounded_or_scheduled_send?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def bounded_or_scheduled_send?
|
|
123
|
+
config.interval || config.data || config.file || @send_eval_proc
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def wait_for_peer
|
|
128
|
+
wait_body = proc do
|
|
129
|
+
@sock.peer_connected.wait
|
|
130
|
+
log "peer connected"
|
|
131
|
+
apply_grace_period
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if config.timeout
|
|
135
|
+
Fiber.scheduler.with_timeout(config.timeout, &wait_body)
|
|
136
|
+
else
|
|
137
|
+
wait_body.call
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Grace period: when multiple peers may be connecting (bind or
|
|
143
|
+
# multiple connect URLs), wait one reconnect interval so
|
|
144
|
+
# latecomers finish their handshake before we start sending.
|
|
145
|
+
def apply_grace_period
|
|
146
|
+
return unless config.binds.any? || config.connects.size > 1
|
|
147
|
+
ri = @sock.options.reconnect_interval
|
|
148
|
+
sleep(ri.is_a?(Range) ? ri.begin : ri)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# -- Socket setup ------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def setup_subscriptions
|
|
156
|
+
SocketSetup.setup_subscriptions(@sock, config)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# -- Shared loop bodies ------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_send_logic
|
|
164
|
+
n = config.count
|
|
165
|
+
sleep(config.delay) if config.delay
|
|
166
|
+
if config.interval
|
|
167
|
+
run_interval_send(n)
|
|
168
|
+
elsif config.data || config.file
|
|
169
|
+
# One-shot from -d/-f. --count N fires the same payload N times.
|
|
170
|
+
msg = eval_send_expr(read_next)
|
|
171
|
+
(n && n > 0 ? n : 1).times { send_msg(msg) } if msg
|
|
172
|
+
elsif stdin_ready?
|
|
173
|
+
run_stdin_send(n)
|
|
174
|
+
elsif @send_eval_proc
|
|
175
|
+
# Pure generator: -e/-E with no stdin input. Fire once by
|
|
176
|
+
# default, --count N fires N times.
|
|
177
|
+
(n && n > 0 ? n : 1).times do
|
|
178
|
+
msg = eval_send_expr(nil)
|
|
179
|
+
send_msg(msg) if msg
|
|
180
|
+
end
|
|
181
|
+
elsif config.stdin_is_tty
|
|
182
|
+
# Bare interactive invocation on a terminal: read lines from
|
|
183
|
+
# the tty until the user hits ^D.
|
|
184
|
+
run_stdin_send(n)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_interval_send(n)
|
|
190
|
+
i = send_tick
|
|
191
|
+
return if @send_tick_eof || (n && n > 0 && i >= n)
|
|
192
|
+
Async::Loop.quantized(interval: config.interval) do
|
|
193
|
+
i += send_tick
|
|
194
|
+
break if @send_tick_eof || (n && n > 0 && i >= n)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run_stdin_send(n)
|
|
200
|
+
i = 0
|
|
201
|
+
loop do
|
|
202
|
+
msg = read_next
|
|
203
|
+
break unless msg
|
|
204
|
+
msg = eval_send_expr(msg)
|
|
205
|
+
send_msg(msg) if msg
|
|
206
|
+
i += 1
|
|
207
|
+
break if n && n > 0 && i >= n
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def send_tick
|
|
213
|
+
raw = read_next_or_nil
|
|
214
|
+
if raw.nil?
|
|
215
|
+
if @send_eval_proc && !@stdin_ready
|
|
216
|
+
# Pure generator mode: no stdin, eval produces output from nothing.
|
|
217
|
+
msg = eval_send_expr(nil)
|
|
218
|
+
send_msg(msg) if msg
|
|
219
|
+
return 1
|
|
220
|
+
end
|
|
221
|
+
@send_tick_eof = true
|
|
222
|
+
return 0
|
|
223
|
+
end
|
|
224
|
+
msg = eval_send_expr(raw)
|
|
225
|
+
send_msg(msg) if msg
|
|
226
|
+
1
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def run_recv_logic
|
|
231
|
+
n = config.count
|
|
232
|
+
i = 0
|
|
233
|
+
if config.interval
|
|
234
|
+
run_interval_recv(n)
|
|
235
|
+
else
|
|
236
|
+
loop do
|
|
237
|
+
msg = recv_msg
|
|
238
|
+
break if msg.nil?
|
|
239
|
+
msg = eval_recv_expr(msg)
|
|
240
|
+
output(msg)
|
|
241
|
+
i += 1
|
|
242
|
+
break if n && n > 0 && i >= n
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def run_interval_recv(n)
|
|
249
|
+
i = recv_tick
|
|
250
|
+
return if i == 0
|
|
251
|
+
return if n && n > 0 && i >= n
|
|
252
|
+
Async::Loop.quantized(interval: config.interval) do
|
|
253
|
+
i += recv_tick
|
|
254
|
+
break if @recv_tick_eof || (n && n > 0 && i >= n)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def recv_tick
|
|
260
|
+
msg = recv_msg
|
|
261
|
+
if msg.nil?
|
|
262
|
+
@recv_tick_eof = true
|
|
263
|
+
return 0
|
|
264
|
+
end
|
|
265
|
+
msg = eval_recv_expr(msg)
|
|
266
|
+
output(msg)
|
|
267
|
+
1
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def wait_for_loops(receiver, sender)
|
|
272
|
+
if config.data || config.file || config.send_expr || config.recv_expr
|
|
273
|
+
sender.wait
|
|
274
|
+
receiver.stop
|
|
275
|
+
elsif config.count && config.count > 0
|
|
276
|
+
receiver.wait
|
|
277
|
+
sender.stop
|
|
278
|
+
else
|
|
279
|
+
sender.wait
|
|
280
|
+
receiver.stop
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# -- Message I/O -------------------------------------------------
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# msg: 1-element Array. nnq sockets take a bare String body.
|
|
289
|
+
def send_msg(msg)
|
|
290
|
+
return if msg.empty?
|
|
291
|
+
msg = [Marshal.dump(msg.first)] if config.format == :marshal
|
|
292
|
+
msg = @fmt.compress(msg)
|
|
293
|
+
@sock.send(msg.first)
|
|
294
|
+
transient_ready!
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# @return [Array<String>, nil] 1-element Array (body), or nil on close.
|
|
299
|
+
def recv_msg
|
|
300
|
+
raw = @sock.receive
|
|
301
|
+
return nil if raw.nil?
|
|
302
|
+
msg = @fmt.decompress([raw])
|
|
303
|
+
msg = [Marshal.load(msg.first)] if config.format == :marshal
|
|
304
|
+
transient_ready!
|
|
305
|
+
msg
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def read_next
|
|
310
|
+
config.data || config.file ? read_inline_data : read_stdin_input
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def read_inline_data
|
|
315
|
+
if config.data
|
|
316
|
+
@fmt.decode(config.data + "\n")
|
|
317
|
+
else
|
|
318
|
+
@file_data ||= (config.file == "-" ? $stdin.read : File.read(config.file)).chomp
|
|
319
|
+
@fmt.decode(@file_data + "\n")
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def read_stdin_input
|
|
325
|
+
case config.format
|
|
326
|
+
when :msgpack
|
|
327
|
+
@fmt.decode_msgpack($stdin)
|
|
328
|
+
when :marshal
|
|
329
|
+
@fmt.decode_marshal($stdin)
|
|
330
|
+
when :raw
|
|
331
|
+
data = $stdin.read
|
|
332
|
+
data.nil? || data.empty? ? nil : [data]
|
|
333
|
+
else
|
|
334
|
+
line = $stdin.gets
|
|
335
|
+
line.nil? ? nil : @fmt.decode(line)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def stdin_ready?
|
|
341
|
+
return @stdin_ready unless @stdin_ready.nil?
|
|
342
|
+
|
|
343
|
+
@stdin_ready = !$stdin.closed? &&
|
|
344
|
+
!config.stdin_is_tty &&
|
|
345
|
+
IO.select([$stdin], nil, nil, 0.01) &&
|
|
346
|
+
!$stdin.eof?
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def read_next_or_nil
|
|
351
|
+
if config.data || config.file
|
|
352
|
+
read_next
|
|
353
|
+
elsif stdin_ready?
|
|
354
|
+
read_stdin_input
|
|
355
|
+
else
|
|
356
|
+
nil
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def output(msg)
|
|
362
|
+
return if config.quiet || msg.nil?
|
|
363
|
+
$stdout.write(@fmt.encode(msg))
|
|
364
|
+
$stdout.flush
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# -- Eval --------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def compile_expr
|
|
372
|
+
@send_evaluator = compile_evaluator(config.send_expr, fallback: NNQ.outgoing_proc)
|
|
373
|
+
@recv_evaluator = compile_evaluator(config.recv_expr, fallback: NNQ.incoming_proc)
|
|
374
|
+
assign_send_aliases
|
|
375
|
+
assign_recv_aliases
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def compile_evaluator(src, fallback:)
|
|
380
|
+
ExpressionEvaluator.new(src, format: config.format, fallback_proc: fallback)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def assign_send_aliases
|
|
385
|
+
# Keep ivar aliases -- subclasses check these directly
|
|
386
|
+
@send_begin_proc = @send_evaluator.begin_proc
|
|
387
|
+
@send_eval_proc = @send_evaluator.eval_proc
|
|
388
|
+
@send_end_proc = @send_evaluator.end_proc
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def assign_recv_aliases
|
|
393
|
+
@recv_begin_proc = @recv_evaluator.begin_proc
|
|
394
|
+
@recv_eval_proc = @recv_evaluator.eval_proc
|
|
395
|
+
@recv_end_proc = @recv_evaluator.end_proc
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def eval_send_expr(msg)
|
|
400
|
+
@send_evaluator.call(msg, @sock)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def eval_recv_expr(msg)
|
|
405
|
+
@recv_evaluator.call(msg, @sock)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
SENT = ExpressionEvaluator::SENT
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# -- Process title -------------------------------------------------
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def set_process_title(endpoints: nil)
|
|
416
|
+
eps = endpoints || config.endpoints
|
|
417
|
+
title = ["nnq", config.type_name]
|
|
418
|
+
title << "-z" if config.compress
|
|
419
|
+
title << "-P#{config.parallel}" if config.parallel
|
|
420
|
+
eps.each do |ep|
|
|
421
|
+
title << (ep.respond_to?(:url) ? ep.url : ep.to_s)
|
|
422
|
+
end
|
|
423
|
+
Process.setproctitle(title.join(" "))
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# -- Logging -----------------------------------------------------
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def log(msg)
|
|
431
|
+
return unless config.verbose >= 1
|
|
432
|
+
$stderr.write("#{Term.log_prefix(config.verbose)}nnq: #{msg}\n")
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# -vv: log connect/disconnect/retry/timeout events via Socket#monitor
|
|
437
|
+
# -vvv: also log message sent/received traces
|
|
438
|
+
# -vvvv: prepend ISO8601 timestamps
|
|
439
|
+
def start_event_monitor
|
|
440
|
+
verbose = config.verbose >= 3
|
|
441
|
+
v = config.verbose
|
|
442
|
+
@sock.monitor(verbose: verbose) do |event|
|
|
443
|
+
CLI::Term.write_event(event, v)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
data/lib/nnq/cli/bus.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
module CLI
|
|
5
|
+
# Runner for BUS sockets (bidirectional mesh).
|
|
6
|
+
class BusRunner < BaseRunner
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_loop(task)
|
|
11
|
+
receiver = recv_async(task)
|
|
12
|
+
sender = task.async { run_send_logic }
|
|
13
|
+
wait_for_loops(receiver, sender)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def recv_async(task)
|
|
18
|
+
task.async do
|
|
19
|
+
n = config.count
|
|
20
|
+
i = 0
|
|
21
|
+
loop do
|
|
22
|
+
msg = recv_msg
|
|
23
|
+
break if msg.nil?
|
|
24
|
+
msg = eval_recv_expr(msg)
|
|
25
|
+
output(msg)
|
|
26
|
+
i += 1
|
|
27
|
+
break if n && n > 0 && i >= n
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|