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.
@@ -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
@@ -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