omq-cli 0.7.0 → 0.7.2

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: 8e3472d73b7d467c20072e00da82aa7222c1cc99cc99e4147f833cb010422e11
4
- data.tar.gz: 0d2f5fff5723cc45af838de506d92d47581dbb24feb302d63b297565393cd182
3
+ metadata.gz: 5af8706d0afd5ad199301ec7b37a47a232c5732a930eada3696a9f60a31aecc4
4
+ data.tar.gz: 6e5b15d9bb2af20181cccbc71d5b4d432938660a8e052d62598c18386ddf2a5d
5
5
  SHA512:
6
- metadata.gz: 10001c2717aded533e3c902c481d34bf5a6870e55207cec33b04c7043f7f19cfb71c68f99f467554ca619cebba352a6258bc634538d781caeeefb76cc0813b18
7
- data.tar.gz: 1fd287acb7bd0fb85d7c260a74c7ef32208c4893b819f780e1a9b05e3ea7219619a4b00e41847ae03a2a5cb1356348416e17d4e74f1a5cb4c8fcf42cd5741013
6
+ metadata.gz: 8d35e218e4327b8a0c22c6ce15d9d205f62feffeec2fa2afd21cda6fcef96724645ea76922f0d0dd67099883b2795a0443211bbabf729c4c857e77e1a3b4299b
7
+ data.tar.gz: 34af31e1dc6605c6b90bc7dcb3f519755d13a12e71f4417de7a5d07f71617610fc6d728f57f1be5f51bda6923d49fc1a6fc8cb1cbc6234d31c37ad392b8c3452
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2 — 2026-04-07
4
+
5
+ ### Changed
6
+
7
+ - **Cleaned up pipe.rb** — removed unused `@fmt` formatters, dead `log` method,
8
+ and `with_timeout` wrapper. Moved `preresolve_tcp` and `start_log_consumer` to
9
+ `PipeWorker` class methods.
10
+ - **Guard `with_timeout(nil)`** — `Fiber.scheduler.with_timeout(nil)` fires
11
+ immediately in Async; peer-wait and pipe sequential mode now skip the timeout
12
+ wrapper when `config.timeout` is nil.
13
+
14
+ ## 0.7.1 — 2026-04-07
15
+
16
+ ### Fixed
17
+
18
+ - **Pipe `-P` preresolves TCP hostnames** — DNS resolution happens on the
19
+ main thread before spawning Ractors, avoiding `Ractor::IsolationError`
20
+ on `Resolv::DefaultResolver`. All resolved addresses (IPv4 + IPv6) are
21
+ passed to workers.
22
+
3
23
  ## 0.7.0 — 2026-04-07
4
24
 
5
25
  ### Changed
@@ -107,12 +107,18 @@ module OMQ
107
107
 
108
108
 
109
109
  def wait_for_peer
110
- with_timeout(config.timeout) do
110
+ wait_body = proc do
111
111
  @sock.peer_connected.wait
112
112
  log "Peer connected"
113
113
  wait_for_subscriber
114
114
  apply_grace_period
115
115
  end
116
+
117
+ if config.timeout
118
+ Fiber.scheduler.with_timeout(config.timeout, &wait_body)
119
+ else
120
+ wait_body.call
121
+ end
116
122
  end
117
123
 
118
124
 
@@ -133,18 +139,6 @@ module OMQ
133
139
  end
134
140
 
135
141
 
136
- # ── Timeout helper ──────────────────────────────────────────────
137
-
138
-
139
- def with_timeout(seconds)
140
- if seconds
141
- Async::Task.current.with_timeout(seconds) { yield }
142
- else
143
- yield
144
- end
145
- end
146
-
147
-
148
142
  # ── Socket setup ────────────────────────────────────────────────
149
143
 
150
144
 
data/lib/omq/cli/pipe.rb CHANGED
@@ -11,8 +11,7 @@ module OMQ
11
11
 
12
12
  # @param config [Config] frozen CLI configuration
13
13
  def initialize(config)
14
- @config = config
15
- @fmt = Formatter.new(config.format, compress: config.compress)
14
+ @config = config
16
15
  @fmt_in = Formatter.new(config.format, compress: config.compress_in || config.compress)
17
16
  @fmt_out = Formatter.new(config.format, compress: config.compress_out || config.compress)
18
17
  end
@@ -43,30 +42,27 @@ module OMQ
43
42
  end
44
43
 
45
44
 
46
- def attach_endpoints(sock, endpoints)
47
- SocketSetup.attach_endpoints(sock, endpoints, verbose: config.verbose >= 1)
48
- end
49
-
50
-
51
45
  # ── Sequential ───────────────────────────────────────────────────
52
46
 
53
47
 
54
48
  def run_sequential(task)
55
49
  in_eps, out_eps = resolve_endpoints
56
- @pull, @push = build_pull_push(
57
- { linger: config.linger, recv_timeout: config.timeout },
58
- { linger: config.linger, send_timeout: config.timeout },
59
- in_eps, out_eps
60
- )
50
+ @pull, @push = build_pull_push(in_eps, out_eps)
61
51
  compile_expr
62
52
  @sock = @pull # for eval instance_exec
63
53
  start_event_monitors if config.verbose >= 2
64
- with_timeout(config.timeout) do
54
+ wait_body = proc do
65
55
  Barrier do |barrier|
66
56
  barrier.async(annotation: "wait push peer") { @push.peer_connected.wait }
67
57
  barrier.async(annotation: "wait pull peer") { @pull.peer_connected.wait }
68
58
  end
69
59
  end
60
+
61
+ if config.timeout
62
+ Fiber.scheduler.with_timeout(config.timeout, &wait_body)
63
+ else
64
+ wait_body.call
65
+ end
70
66
  setup_sequential_transient(task)
71
67
  @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
72
68
  sequential_message_loop
@@ -87,13 +83,13 @@ module OMQ
87
83
  end
88
84
 
89
85
 
90
- def build_pull_push(pull_opts, push_opts, in_eps, out_eps)
91
- pull = OMQ::PULL.new(**pull_opts)
92
- push = OMQ::PUSH.new(**push_opts)
86
+ def build_pull_push(in_eps, out_eps)
87
+ pull = OMQ::PULL.new(linger: config.linger, recv_timeout: config.timeout)
88
+ push = OMQ::PUSH.new(linger: config.linger, send_timeout: config.timeout)
93
89
  apply_socket_options(pull)
94
90
  apply_socket_options(push)
95
- attach_endpoints(pull, in_eps)
96
- attach_endpoints(push, out_eps)
91
+ SocketSetup.attach_endpoints(pull, in_eps, verbose: config.verbose >= 1)
92
+ SocketSetup.attach_endpoints(push, out_eps, verbose: config.verbose >= 1)
97
93
  [pull, push]
98
94
  end
99
95
 
@@ -117,8 +113,7 @@ module OMQ
117
113
  parts = @fmt_in.decompress(parts)
118
114
  parts = eval_recv_expr(parts)
119
115
  if parts && !parts.empty?
120
- out = @fmt_out.compress(parts)
121
- @push.send(out)
116
+ @push.send(@fmt_out.compress(parts))
122
117
  end
123
118
  i += 1
124
119
  break if n && n > 0 && i >= n
@@ -132,121 +127,26 @@ module OMQ
132
127
  def run_parallel(task)
133
128
  OMQ.freeze_for_ractors!
134
129
  in_eps, out_eps = resolve_endpoints
135
- workers = spawn_workers(config, in_eps, out_eps)
130
+ in_eps = PipeWorker.preresolve_tcp(in_eps)
131
+ out_eps = PipeWorker.preresolve_tcp(out_eps)
132
+ log_port, log_thread = PipeWorker.start_log_consumer
133
+ workers = config.parallel.times.map do
134
+ ::Ractor.new(config, in_eps, out_eps, log_port) do |cfg, ins, outs, lport|
135
+ PipeWorker.new(cfg, ins, outs, lport).call
136
+ end
137
+ end
136
138
  workers.each do |w|
137
139
  w.join
138
140
  rescue ::Ractor::RemoteError => e
139
141
  $stderr.write("omq: Ractor error: #{e.cause&.message || e.message}\n")
140
142
  end
143
+ ensure
144
+ log_port.close
145
+ log_thread.join
141
146
  end
142
147
 
143
148
 
144
- def spawn_workers(config, in_eps, out_eps)
145
- config.parallel.times.map do
146
- ::Ractor.new(config, in_eps, out_eps) do |cfg, ins, outs|
147
- Async do
148
- pull = OMQ::PULL.new(linger: cfg.linger)
149
- push = OMQ::PUSH.new(linger: cfg.linger)
150
- pull.recv_timeout = cfg.timeout if cfg.timeout
151
- push.send_timeout = cfg.timeout if cfg.timeout
152
- pull.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
153
- push.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
154
- pull.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
155
- push.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
156
- pull.send_hwm = cfg.send_hwm if cfg.send_hwm
157
- pull.recv_hwm = cfg.recv_hwm if cfg.recv_hwm
158
- push.send_hwm = cfg.send_hwm if cfg.send_hwm
159
- push.recv_hwm = cfg.recv_hwm if cfg.recv_hwm
160
- pull.sndbuf = cfg.sndbuf if cfg.sndbuf
161
- pull.rcvbuf = cfg.rcvbuf if cfg.rcvbuf
162
- push.sndbuf = cfg.sndbuf if cfg.sndbuf
163
- push.rcvbuf = cfg.rcvbuf if cfg.rcvbuf
164
-
165
- OMQ::CLI::SocketSetup.attach_endpoints(pull, ins, verbose: cfg.verbose >= 1)
166
- OMQ::CLI::SocketSetup.attach_endpoints(push, outs, verbose: cfg.verbose >= 1)
167
-
168
- Barrier do |barrier|
169
- barrier.async { pull.peer_connected.wait }
170
- barrier.async { push.peer_connected.wait }
171
- end
172
-
173
- begin_proc, end_proc, eval_proc =
174
- OMQ::CLI::ExpressionEvaluator.compile_inside_ractor(cfg.recv_expr)
175
-
176
- fmt_in = OMQ::CLI::Formatter.new(cfg.format, compress: cfg.compress_in || cfg.compress)
177
- fmt_out = OMQ::CLI::Formatter.new(cfg.format, compress: cfg.compress_out || cfg.compress)
178
-
179
- _ctx = Object.new
180
- _ctx.instance_exec(&begin_proc) if begin_proc
181
-
182
- n_count = cfg.count
183
- begin
184
- if eval_proc
185
- if n_count && n_count > 0
186
- n_count.times do
187
- parts = pull.receive
188
- break if parts.nil?
189
- parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
190
- _ctx.instance_exec(fmt_in.decompress(parts), &eval_proc)
191
- )
192
- next if parts.nil?
193
- push << fmt_out.compress(parts) unless parts.empty?
194
- end
195
- else
196
- loop do
197
- parts = pull.receive
198
- break if parts.nil?
199
- parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
200
- _ctx.instance_exec(fmt_in.decompress(parts), &eval_proc)
201
- )
202
- next if parts.nil?
203
- push << fmt_out.compress(parts) unless parts.empty?
204
- end
205
- end
206
- else
207
- if n_count && n_count > 0
208
- n_count.times do
209
- parts = pull.receive
210
- break if parts.nil?
211
- push << fmt_out.compress(fmt_in.decompress(parts))
212
- end
213
- else
214
- loop do
215
- parts = pull.receive
216
- break if parts.nil?
217
- push << fmt_out.compress(fmt_in.decompress(parts))
218
- end
219
- end
220
- end
221
- rescue IO::TimeoutError, Async::TimeoutError
222
- # recv timed out — fall through to END block
223
- end
224
-
225
- if end_proc
226
- out = OMQ::CLI::ExpressionEvaluator.normalize_result(
227
- _ctx.instance_exec(&end_proc)
228
- )
229
- push << fmt_out.compress(out) if out && !out.empty?
230
- end
231
- ensure
232
- pull&.close
233
- push&.close
234
- end
235
- end
236
- end
237
- end
238
-
239
-
240
- # ── Shared helpers ────────────────────────────────────────────────
241
-
242
-
243
- def with_timeout(seconds)
244
- if seconds
245
- Async::Task.current.with_timeout(seconds) { yield }
246
- else
247
- yield
248
- end
249
- end
149
+ # ── Expression eval ──────────────────────────────────────────────
250
150
 
251
151
 
252
152
  def compile_expr
@@ -263,9 +163,7 @@ module OMQ
263
163
  end
264
164
 
265
165
 
266
- def log(msg)
267
- $stderr.write("#{msg}\n") if config.verbose >= 1
268
- end
166
+ # ── Event monitoring ─────────────────────────────────────────────
269
167
 
270
168
 
271
169
  def start_event_monitors
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ # Worker that runs inside a Ractor for pipe -P parallel mode.
6
+ # Each worker owns its own Async reactor, PULL socket, and PUSH socket.
7
+ #
8
+ class PipeWorker
9
+ # Starts a Ractor::Port and a consumer thread that drains log
10
+ # messages to stderr sequentially. Returns [port, thread].
11
+ #
12
+ def self.start_log_consumer
13
+ port = Ractor::Port.new
14
+ thread = Thread.new(port) do |p|
15
+ loop do
16
+ $stderr.write("#{p.receive}\n")
17
+ rescue Ractor::ClosedError
18
+ break
19
+ end
20
+ end
21
+ [port, thread]
22
+ end
23
+
24
+
25
+ # Resolves TCP hostnames to IP addresses so Ractors don't touch
26
+ # Resolv::DefaultResolver (which is not shareable).
27
+ #
28
+ def self.preresolve_tcp(endpoints)
29
+ endpoints.flat_map do |ep|
30
+ url = ep.url
31
+ if url.start_with?("tcp://")
32
+ host, port = OMQ::Transport::TCP.parse_endpoint(url)
33
+ Addrinfo.getaddrinfo(host, port, nil, :STREAM).map do |addr|
34
+ ip = addr.ip_address
35
+ ip = "[#{ip}]" if ip.include?(":")
36
+ Endpoint.new("tcp://#{ip}:#{addr.ip_port}", ep.bind?)
37
+ end
38
+ else
39
+ ep
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ def initialize(config, in_eps, out_eps, log_port)
46
+ @config = config
47
+ @in_eps = in_eps
48
+ @out_eps = out_eps
49
+ @log_port = log_port
50
+ end
51
+
52
+
53
+ def call
54
+ Async do
55
+ setup_sockets
56
+ log_endpoints if @config.verbose >= 1
57
+ start_monitors if @config.verbose >= 2
58
+ wait_for_peers
59
+ compile_expr
60
+ run_message_loop
61
+ run_end_block
62
+ ensure
63
+ @pull&.close
64
+ @push&.close
65
+ end
66
+ end
67
+
68
+
69
+ private
70
+
71
+
72
+ def setup_sockets
73
+ @pull = OMQ::PULL.new(linger: @config.linger)
74
+ @push = OMQ::PUSH.new(linger: @config.linger)
75
+ @pull.recv_timeout = @config.timeout if @config.timeout
76
+ @push.send_timeout = @config.timeout if @config.timeout
77
+ apply_socket_options(@pull)
78
+ apply_socket_options(@push)
79
+ OMQ::CLI::SocketSetup.attach_endpoints(@pull, @in_eps, verbose: false)
80
+ OMQ::CLI::SocketSetup.attach_endpoints(@push, @out_eps, verbose: false)
81
+ end
82
+
83
+
84
+ def apply_socket_options(sock)
85
+ sock.reconnect_interval = @config.reconnect_ivl if @config.reconnect_ivl
86
+ sock.heartbeat_interval = @config.heartbeat_ivl if @config.heartbeat_ivl
87
+ sock.send_hwm = @config.send_hwm if @config.send_hwm
88
+ sock.recv_hwm = @config.recv_hwm if @config.recv_hwm
89
+ sock.sndbuf = @config.sndbuf if @config.sndbuf
90
+ sock.rcvbuf = @config.rcvbuf if @config.rcvbuf
91
+ end
92
+
93
+
94
+ def log_endpoints
95
+ @in_eps.each { |ep| @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}") }
96
+ @out_eps.each { |ep| @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}") }
97
+ end
98
+
99
+
100
+ def start_monitors
101
+ trace = @config.verbose >= 3
102
+ [@pull, @push].each do |sock|
103
+ sock.monitor(verbose: trace) do |event|
104
+ @log_port.send(format_event(event))
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ def format_event(event)
111
+ case event.type
112
+ when :message_sent
113
+ "omq: >> #{msg_preview(event.detail[:parts])}"
114
+ when :message_received
115
+ "omq: << #{msg_preview(event.detail[:parts])}"
116
+ else
117
+ ep = event.endpoint ? " #{event.endpoint}" : ""
118
+ detail = event.detail ? " #{event.detail}" : ""
119
+ "omq: #{event.type}#{ep}#{detail}"
120
+ end
121
+ end
122
+
123
+
124
+ def msg_preview(parts)
125
+ parts.map { |p|
126
+ bytes = p.b
127
+ preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
128
+ bytes.bytesize > 10 ? "#{preview}... (#{bytes.bytesize}B)" : preview
129
+ }.join(" | ")
130
+ end
131
+
132
+
133
+ def wait_for_peers
134
+ Barrier do |barrier|
135
+ barrier.async { @pull.peer_connected.wait }
136
+ barrier.async { @push.peer_connected.wait }
137
+ end
138
+ end
139
+
140
+
141
+ def compile_expr
142
+ @begin_proc, @end_proc, @eval_proc =
143
+ OMQ::CLI::ExpressionEvaluator.compile_inside_ractor(@config.recv_expr)
144
+ @fmt_in = OMQ::CLI::Formatter.new(@config.format, compress: @config.compress_in || @config.compress)
145
+ @fmt_out = OMQ::CLI::Formatter.new(@config.format, compress: @config.compress_out || @config.compress)
146
+ @ctx = Object.new
147
+ @ctx.instance_exec(&@begin_proc) if @begin_proc
148
+ end
149
+
150
+
151
+ def run_message_loop
152
+ n = @config.count
153
+ if @eval_proc
154
+ if n && n > 0
155
+ n.times do
156
+ parts = @pull.receive
157
+ break if parts.nil?
158
+ parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
159
+ @ctx.instance_exec(@fmt_in.decompress(parts), &@eval_proc)
160
+ )
161
+ next if parts.nil?
162
+ @push << @fmt_out.compress(parts) unless parts.empty?
163
+ end
164
+ else
165
+ loop do
166
+ parts = @pull.receive
167
+ break if parts.nil?
168
+ parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
169
+ @ctx.instance_exec(@fmt_in.decompress(parts), &@eval_proc)
170
+ )
171
+ next if parts.nil?
172
+ @push << @fmt_out.compress(parts) unless parts.empty?
173
+ end
174
+ end
175
+ else
176
+ if n && n > 0
177
+ n.times do
178
+ parts = @pull.receive
179
+ break if parts.nil?
180
+ @push << @fmt_out.compress(@fmt_in.decompress(parts))
181
+ end
182
+ else
183
+ loop do
184
+ parts = @pull.receive
185
+ break if parts.nil?
186
+ @push << @fmt_out.compress(@fmt_in.decompress(parts))
187
+ end
188
+ end
189
+ end
190
+ rescue IO::TimeoutError, Async::TimeoutError
191
+ # recv timed out — fall through to END block
192
+ end
193
+
194
+
195
+ def run_end_block
196
+ return unless @end_proc
197
+ out = OMQ::CLI::ExpressionEvaluator.normalize_result(
198
+ @ctx.instance_exec(&@end_proc)
199
+ )
200
+ @push << @fmt_out.compress(out) if out && !out.empty?
201
+ end
202
+ end
203
+ end
204
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module CLI
5
- VERSION = "0.7.0"
5
+ VERSION = "0.7.2"
6
6
  end
7
7
  end
data/lib/omq/cli.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "cli/req_rep"
18
18
  require_relative "cli/pair"
19
19
  require_relative "cli/router_dealer"
20
20
  require_relative "cli/client_server"
21
+ require_relative "cli/pipe_worker"
21
22
  require_relative "cli/pipe"
22
23
 
23
24
  module OMQ
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.7.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -166,6 +166,7 @@ files:
166
166
  - lib/omq/cli/formatter.rb
167
167
  - lib/omq/cli/pair.rb
168
168
  - lib/omq/cli/pipe.rb
169
+ - lib/omq/cli/pipe_worker.rb
169
170
  - lib/omq/cli/pub_sub.rb
170
171
  - lib/omq/cli/push_pull.rb
171
172
  - lib/omq/cli/radio_dish.rb