omq-cli 0.3.1 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 887b1bd4a69ddbbcc0d4885c05c277f32285253e7d7163407ace5451ff87ba08
4
- data.tar.gz: 241d25d7b9e9be0a9ea59efb8a1f92c3f9667fafd73087763347a337c84d9299
3
+ metadata.gz: 1e8af323d98f0d387b86918d92e0720acfeffc27d0d9880f51fe9a3bc27d364b
4
+ data.tar.gz: a0c3c26481fbb2c4267438dbbf5ba48bd5805079b834efa289538f79eb42b5a0
5
5
  SHA512:
6
- metadata.gz: d536665684b7c77cf5a724d057bf534b93dc19e5d0f2f9fa5035d384131353fd930b453b5e6417ca90bfa77a5a304a6759a6ae85de807e402b1ee07e5d78b033
7
- data.tar.gz: 214a3d68093f7ac1e04a2a3b7e0438459ce826907dd0ee5baab2ed86c6bc5f571b26eb332d91383e6935e893f41344f5e656d4286e930022a20ae43a48b61ac0
6
+ metadata.gz: bc936835411067ac766d7c7926926a5ebee961c03f2faf151ae4714e44ff5bbf7f95472a2ee5d6cb9ddf3e066544904b9cc535573ce07e7988ded44c706159f9
7
+ data.tar.gz: a362663c0f58b056ef2a753eca530c71e01f2911cabc77536b246852220dbd140c9e5f982c28800c8fffed8cc50f2b133ee0f0b9f3c2f7a0249902874d552b99
data/CHANGELOG.md CHANGED
@@ -1,9 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-04-07
4
+
5
+ ### Added
6
+
7
+ - **`--sndbuf` / `--rcvbuf` options** — set `SO_SNDBUF` and `SO_RCVBUF` kernel
8
+ buffer sizes. Accepts plain bytes or suffixed values (`4K`, `1M`).
9
+ - **Pipe FIFO ordering system test** — verifies sequential source batches are
10
+ never interleaved through a pipe.
11
+ - **Pipe producer-first system test** — verifies messages are delivered when
12
+ the producer finishes before the consumer connects.
13
+
14
+ ### Changed
15
+
16
+ - **Message traces moved to monitor events** — `-vvv` traces now use
17
+ `Socket#monitor(verbose: true)` instead of inline `trace_send`/`trace_recv`
18
+ calls, ensuring correct ordering with connection lifecycle events.
19
+
20
+ ### Fixed
21
+
22
+ - **Test helper `make_config`** — added missing `send_hwm`, `recv_hwm`,
23
+ `sndbuf`, `rcvbuf` fields and changed `verbose` default from `false` to `0`.
24
+
3
25
  ## 0.3.1 — 2026-04-07
4
26
 
27
+ ### Added
28
+
29
+ - **`--send-hwm` / `--recv-hwm` options** — set send and receive high water
30
+ marks from the command line (default 1000, 0 = unbounded).
31
+ - **`OMQ_DEBUG` env var** — starts async-debug web UI on
32
+ `https://localhost:5050` (or custom port via `OMQ_DEBUG=PORT`).
33
+ - **Multi-level verbosity** — `-v` prints endpoints, `-vv` logs all
34
+ connection events (connect/disconnect/retry/timeout) via socket monitor,
35
+ `-vvv` also traces first 10 bytes of every sent/received message.
36
+
5
37
  ### Fixed
6
38
 
39
+ - **`omq pipe` slow reconnection** — sequential `peer_connected.wait` calls
40
+ blocked receiving until both PULL and PUSH peers connected in order. Now
41
+ waits concurrently using `Kernel#Barrier`.
7
42
  - **`-i` on recv-only sockets** — `pull -i 0.2` rate-limits receiving to
8
43
  one message every 200 ms using `Async::Loop.quantized`. Works on all
9
44
  recv-only socket types (pull, sub, gather, dish).
@@ -25,6 +25,7 @@ module OMQ
25
25
  # @return [void]
26
26
  def call(task)
27
27
  setup_socket
28
+ start_event_monitor if config.verbose >= 2
28
29
  maybe_start_transient_monitor(task)
29
30
  sleep(config.delay) if config.delay && config.recv_only?
30
31
  wait_for_peer if needs_peer_wait?
@@ -63,7 +64,7 @@ module OMQ
63
64
 
64
65
 
65
66
  def attach_endpoints
66
- SocketSetup.attach(@sock, config, verbose: config.verbose)
67
+ SocketSetup.attach(@sock, config, verbose: config.verbose >= 1)
67
68
  end
68
69
 
69
70
 
@@ -424,7 +425,42 @@ module OMQ
424
425
 
425
426
 
426
427
  def log(msg)
427
- $stderr.puts(msg) if config.verbose
428
+ $stderr.write("#{msg}\n") if config.verbose >= 1
429
+ end
430
+
431
+
432
+ # -vv: log connect/disconnect/retry/timeout events via Socket#monitor
433
+ # -vvv: also log message sent/received traces
434
+ def start_event_monitor
435
+ verbose = config.verbose >= 3
436
+ @sock.monitor(verbose: verbose) do |event|
437
+ case event.type
438
+ when :message_sent
439
+ $stderr.write("omq: >> #{msg_preview(event.detail[:parts])}\n")
440
+ when :message_received
441
+ $stderr.write("omq: << #{msg_preview(event.detail[:parts])}\n")
442
+ else
443
+ ep = event.endpoint ? " #{event.endpoint}" : ""
444
+ detail = event.detail ? " #{event.detail}" : ""
445
+ $stderr.write("omq: #{event.type}#{ep}#{detail}\n")
446
+ end
447
+ end
448
+ end
449
+
450
+
451
+ def msg_preview(parts)
452
+ parts.map { |p| preview_bytes(p) }.join(" | ")
453
+ end
454
+
455
+
456
+ def preview_bytes(str)
457
+ bytes = str.b
458
+ preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
459
+ if bytes.bytesize > 10
460
+ "#{preview}... (#{bytes.bytesize}B)"
461
+ else
462
+ preview
463
+ end
428
464
  end
429
465
  end
430
466
  end
@@ -213,13 +213,17 @@ module OMQ
213
213
  linger: 5,
214
214
  reconnect_ivl: nil,
215
215
  heartbeat_ivl: nil,
216
+ send_hwm: nil,
217
+ recv_hwm: nil,
218
+ sndbuf: nil,
219
+ rcvbuf: nil,
216
220
  conflate: false,
217
221
  compress: false,
218
222
  send_expr: nil,
219
223
  recv_expr: nil,
220
224
  parallel: nil,
221
225
  transient: false,
222
- verbose: false,
226
+ verbose: 0,
223
227
  quiet: false,
224
228
  echo: false,
225
229
  scripts: [],
@@ -336,6 +340,10 @@ module OMQ
336
340
  }
337
341
  o.on("--heartbeat-ivl SECS", Float, "ZMTP heartbeat interval (detects dead peers)") { |v| opts[:heartbeat_ivl] = v }
338
342
  o.on("--recv-maxsz COUNT", Integer, "Max inbound message size in bytes (larger messages dropped)") { |v| opts[:recv_maxsz] = v }
343
+ o.on("--send-hwm N", Integer, "Send high water mark (default 1000, 0=unbounded)") { |v| opts[:send_hwm] = v }
344
+ o.on("--recv-hwm N", Integer, "Recv high water mark (default 1000, 0=unbounded)") { |v| opts[:recv_hwm] = v }
345
+ o.on("--sndbuf N", "SO_SNDBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:sndbuf] = parse_byte_size(v) }
346
+ o.on("--rcvbuf N", "SO_RCVBUF kernel buffer size (e.g. 4K, 1M)") { |v| opts[:rcvbuf] = parse_byte_size(v) }
339
347
 
340
348
  o.separator "\nDelivery:"
341
349
  o.on("--conflate", "Keep only last message per subscriber (PUB/RADIO)") { opts[:conflate] = true }
@@ -363,7 +371,7 @@ module OMQ
363
371
  o.separator " OMQ_CURVE_CRYPTO (backend: rbnacl or nuckle)"
364
372
 
365
373
  o.separator "\nOther:"
366
- o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
374
+ o.on("-v", "--verbose", "Verbosity: -v endpoints, -vv events, -vvv messages") { opts[:verbose] += 1 }
367
375
  o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
368
376
  o.on( "--transient", "Exit when all peers disconnect") { opts[:transient] = true }
369
377
  o.on("-V", "--version") {
@@ -413,6 +421,25 @@ module OMQ
413
421
  end
414
422
 
415
423
 
424
+ # Parses a byte size string with optional K/M suffix.
425
+ #
426
+ # @param str [String] e.g. "4096", "4K", "1M"
427
+ # @return [Integer] size in bytes
428
+ #
429
+ def parse_byte_size(str)
430
+ case str
431
+ when /\A(\d+)[kK]\z/
432
+ $1.to_i * 1024
433
+ when /\A(\d+)[mM]\z/
434
+ $1.to_i * 1024 * 1024
435
+ when /\A\d+\z/
436
+ str.to_i
437
+ else
438
+ abort "invalid byte size: #{str} (use e.g. 4096, 4K, 1M)"
439
+ end
440
+ end
441
+
442
+
416
443
  # Validates option combinations, aborting on invalid combos.
417
444
  #
418
445
  # @param opts [Hash] parsed options from {#parse}
@@ -464,6 +491,14 @@ module OMQ
464
491
  (opts[:connects] + opts[:binds]).each do |url|
465
492
  abort "inproc not supported, use tcp:// or ipc://" if url.include?("inproc://")
466
493
  end
494
+
495
+ all_urls = if type_name == "pipe"
496
+ (opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]).map(&:url)
497
+ else
498
+ opts[:connects] + opts[:binds]
499
+ end
500
+ dups = all_urls.tally.select { |_, n| n > 1 }.keys
501
+ abort "duplicate endpoint: #{dups.first}" if dups.any?
467
502
  end
468
503
  end
469
504
  end
@@ -38,6 +38,10 @@ module OMQ
38
38
  :linger,
39
39
  :reconnect_ivl,
40
40
  :heartbeat_ivl,
41
+ :send_hwm,
42
+ :recv_hwm,
43
+ :sndbuf,
44
+ :rcvbuf,
41
45
  :conflate,
42
46
  :compress,
43
47
  :send_expr,
data/lib/omq/cli/pipe.rb CHANGED
@@ -42,7 +42,7 @@ module OMQ
42
42
 
43
43
 
44
44
  def attach_endpoints(sock, endpoints)
45
- SocketSetup.attach_endpoints(sock, endpoints)
45
+ SocketSetup.attach_endpoints(sock, endpoints, verbose: config.verbose >= 1)
46
46
  end
47
47
 
48
48
 
@@ -58,9 +58,12 @@ module OMQ
58
58
  )
59
59
  compile_expr
60
60
  @sock = @pull # for eval instance_exec
61
+ start_event_monitors if config.verbose >= 2
61
62
  with_timeout(config.timeout) do
62
- @push.peer_connected.wait
63
- @pull.peer_connected.wait
63
+ Barrier do |barrier|
64
+ barrier.async(annotation: "wait push peer") { @push.peer_connected.wait }
65
+ barrier.async(annotation: "wait pull peer") { @pull.peer_connected.wait }
66
+ end
64
67
  end
65
68
  setup_sequential_transient(task)
66
69
  @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
@@ -72,17 +75,21 @@ module OMQ
72
75
  end
73
76
 
74
77
 
75
- def apply_socket_intervals(sock)
78
+ def apply_socket_options(sock)
76
79
  sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
77
80
  sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
81
+ sock.send_hwm = config.send_hwm if config.send_hwm
82
+ sock.recv_hwm = config.recv_hwm if config.recv_hwm
83
+ sock.sndbuf = config.sndbuf if config.sndbuf
84
+ sock.rcvbuf = config.rcvbuf if config.rcvbuf
78
85
  end
79
86
 
80
87
 
81
88
  def build_pull_push(pull_opts, push_opts, in_eps, out_eps)
82
89
  pull = OMQ::PULL.new(**pull_opts)
83
90
  push = OMQ::PUSH.new(**push_opts)
84
- apply_socket_intervals(pull)
85
- apply_socket_intervals(push)
91
+ apply_socket_options(pull)
92
+ apply_socket_options(push)
86
93
  attach_endpoints(pull, in_eps)
87
94
  attach_endpoints(push, out_eps)
88
95
  [pull, push]
@@ -107,7 +114,10 @@ module OMQ
107
114
  break if parts.nil?
108
115
  parts = @fmt.decompress(parts)
109
116
  parts = eval_recv_expr(parts)
110
- @push.send(@fmt.compress(parts)) if parts && !parts.empty?
117
+ if parts && !parts.empty?
118
+ out = @fmt.compress(parts)
119
+ @push.send(out)
120
+ end
111
121
  i += 1
112
122
  break if n && n > 0 && i >= n
113
123
  end
@@ -143,9 +153,11 @@ module OMQ
143
153
 
144
154
  def wait_for_pairs(pairs)
145
155
  with_timeout(config.timeout) do
146
- pairs.each do |pull, push|
147
- push.peer_connected.wait
148
- pull.peer_connected.wait
156
+ Barrier do |barrier|
157
+ pairs.each do |pull, push|
158
+ barrier.async(annotation: "wait push peer") { push.peer_connected.wait }
159
+ barrier.async(annotation: "wait pull peer") { pull.peer_connected.wait }
160
+ end
149
161
  end
150
162
  end
151
163
  end
@@ -245,7 +257,7 @@ module OMQ
245
257
  workers.each do |w|
246
258
  w.value
247
259
  rescue Ractor::RemoteError => e
248
- $stderr.puts "omq: Ractor error: #{e.cause&.message || e.message}"
260
+ $stderr.write("omq: Ractor error: #{e.cause&.message || e.message}\n")
249
261
  end
250
262
  end
251
263
 
@@ -277,7 +289,35 @@ module OMQ
277
289
 
278
290
 
279
291
  def log(msg)
280
- $stderr.puts(msg) if config.verbose
292
+ $stderr.write("#{msg}\n") if config.verbose >= 1
293
+ end
294
+
295
+
296
+ def start_event_monitors
297
+ verbose = config.verbose >= 3
298
+ [@pull, @push].each do |sock|
299
+ sock.monitor(verbose: verbose) do |event|
300
+ case event.type
301
+ when :message_sent
302
+ $stderr.write("omq: >> #{msg_preview(event.detail[:parts])}\n")
303
+ when :message_received
304
+ $stderr.write("omq: << #{msg_preview(event.detail[:parts])}\n")
305
+ else
306
+ ep = event.endpoint ? " #{event.endpoint}" : ""
307
+ detail = event.detail ? " #{event.detail}" : ""
308
+ $stderr.write("omq: #{event.type}#{ep}#{detail}\n")
309
+ end
310
+ end
311
+ end
312
+ end
313
+
314
+
315
+ def msg_preview(parts)
316
+ parts.map { |p|
317
+ bytes = p.b
318
+ preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
319
+ bytes.bytesize > 10 ? "#{preview}... (#{bytes.bytesize}B)" : preview
320
+ }.join(" | ")
281
321
  end
282
322
  end
283
323
  end
@@ -15,6 +15,10 @@ module OMQ
15
15
  sock.recv_timeout = config.timeout if config.timeout
16
16
  sock.send_timeout = config.timeout if config.timeout
17
17
  sock.max_message_size = config.recv_maxsz if config.recv_maxsz
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
18
22
  sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
19
23
  sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
20
24
  sock.identity = config.identity if config.identity
@@ -40,8 +44,16 @@ module OMQ
40
44
  # Bind/connect +sock+ from an Array of Endpoint objects.
41
45
  # Used by PipeRunner, which works with structured endpoint lists.
42
46
  #
43
- def self.attach_endpoints(sock, endpoints)
44
- endpoints.each { |ep| ep.bind? ? sock.bind(ep.url) : sock.connect(ep.url) }
47
+ def self.attach_endpoints(sock, endpoints, verbose: false)
48
+ endpoints.each do |ep|
49
+ if ep.bind?
50
+ sock.bind(ep.url)
51
+ $stderr.puts "Bound to #{sock.last_endpoint}" if verbose
52
+ else
53
+ sock.connect(ep.url)
54
+ $stderr.puts "Connecting to #{ep.url}" if verbose
55
+ end
56
+ end
45
57
  end
46
58
 
47
59
 
@@ -66,7 +78,7 @@ module OMQ
66
78
 
67
79
  return unless server_key_z85 || server_mode
68
80
 
69
- crypto = CLI.load_curve_crypto(config.curve_crypto || ENV["OMQ_CURVE_CRYPTO"], verbose: config.verbose)
81
+ crypto = CLI.load_curve_crypto(config.curve_crypto || ENV["OMQ_CURVE_CRYPTO"], verbose: config.verbose >= 1)
70
82
  require "protocol/zmtp/mechanism/curve"
71
83
 
72
84
  if server_key_z85
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.3.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/omq/cli.rb CHANGED
@@ -204,11 +204,19 @@ module OMQ
204
204
  trap("INT") { Process.exit!(0) }
205
205
  trap("TERM") { Process.exit!(0) }
206
206
 
207
- Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose
207
+ Console.logger = Console::Logger.new(Console::Output::Null.new) unless config.verbose >= 1
208
+
209
+ debug_ep = nil
210
+
211
+ if ENV["OMQ_DEBUG_URI"]
212
+ require "async/debug"
213
+ debug_ep = Async::HTTP::Endpoint.parse ENV["OMQ_DEBUG_URI"]
214
+ end
208
215
 
209
216
  if config.type_name.nil?
210
217
  Object.include(OMQ) unless Object.include?(OMQ)
211
- Async do
218
+ Async annotation: 'omq' do
219
+ Async::Debug.serve(endpoint: debug_ep) if debug_ep
212
220
  config.scripts.each { |s| load_script(s) }
213
221
  rescue => e
214
222
  $stderr.puts "omq: #{e.message}"
@@ -219,7 +227,8 @@ module OMQ
219
227
 
220
228
  runner_class, socket_sym = RUNNER_MAP.fetch(config.type_name)
221
229
 
222
- Async do |task|
230
+ Async annotation: "omq #{config.type_name}" do |task|
231
+ Async::Debug.serve(endpoint: debug_ep) if debug_ep
223
232
  config.scripts.each { |s| load_script(s) }
224
233
  runner = if socket_sym
225
234
  runner_class.new(config, OMQ.const_get(socket_sym))
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.14'
18
+ version: '0.15'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.14'
25
+ version: '0.15'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: omq-ractor
28
28
  requirement: !ruby/object:Gem::Requirement