omq-cli 0.7.1 → 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: a03141c60a8af670aa4de42bc5b5d286fbf3a975c5a455e9276bc2d4b87a0fb8
4
- data.tar.gz: 2755cb1054df95a64b5faebe481e93ac2e3f391e878f54b5a30a17514dde399a
3
+ metadata.gz: 5af8706d0afd5ad199301ec7b37a47a232c5732a930eada3696a9f60a31aecc4
4
+ data.tar.gz: 6e5b15d9bb2af20181cccbc71d5b4d432938660a8e052d62598c18386ddf2a5d
5
5
  SHA512:
6
- metadata.gz: f245e7f524d745382074347852f1d34e5e1f46e633fe024b7888c05942a399639e69e876ae92c5ace5172800b0dc6a3bd8cd088d3b52f389ea0fb212e0f8da58
7
- data.tar.gz: 34992fe5962fd2639b0c3e92597e3d2d6a134f3ac4fecadfa3d64b1d566994cf9f8ef36ce08fc929ab97bfcf7a7b4dd7f9c1de241b8c87e2a207ce17a838c7d2
6
+ metadata.gz: 8d35e218e4327b8a0c22c6ce15d9d205f62feffeec2fa2afd21cda6fcef96724645ea76922f0d0dd67099883b2795a0443211bbabf729c4c857e77e1a3b4299b
7
+ data.tar.gz: 34af31e1dc6605c6b90bc7dcb3f519755d13a12e71f4417de7a5d07f71617610fc6d728f57f1be5f51bda6923d49fc1a6fc8cb1cbc6234d31c37ad392b8c3452
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
3
14
  ## 0.7.1 — 2026-04-07
4
15
 
5
16
  ### Fixed
@@ -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,143 +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
- in_eps = preresolve_tcp(in_eps)
136
- out_eps = preresolve_tcp(out_eps)
137
- 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
138
138
  workers.each do |w|
139
139
  w.join
140
140
  rescue ::Ractor::RemoteError => e
141
141
  $stderr.write("omq: Ractor error: #{e.cause&.message || e.message}\n")
142
142
  end
143
+ ensure
144
+ log_port.close
145
+ log_thread.join
143
146
  end
144
147
 
145
148
 
146
- def spawn_workers(config, in_eps, out_eps)
147
- config.parallel.times.map do
148
- ::Ractor.new(config, in_eps, out_eps) do |cfg, ins, outs|
149
- Async do
150
- pull = OMQ::PULL.new(linger: cfg.linger)
151
- push = OMQ::PUSH.new(linger: cfg.linger)
152
- pull.recv_timeout = cfg.timeout if cfg.timeout
153
- push.send_timeout = cfg.timeout if cfg.timeout
154
- pull.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
155
- push.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
156
- pull.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
157
- push.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
158
- pull.send_hwm = cfg.send_hwm if cfg.send_hwm
159
- pull.recv_hwm = cfg.recv_hwm if cfg.recv_hwm
160
- push.send_hwm = cfg.send_hwm if cfg.send_hwm
161
- push.recv_hwm = cfg.recv_hwm if cfg.recv_hwm
162
- pull.sndbuf = cfg.sndbuf if cfg.sndbuf
163
- pull.rcvbuf = cfg.rcvbuf if cfg.rcvbuf
164
- push.sndbuf = cfg.sndbuf if cfg.sndbuf
165
- push.rcvbuf = cfg.rcvbuf if cfg.rcvbuf
166
-
167
- OMQ::CLI::SocketSetup.attach_endpoints(pull, ins, verbose: cfg.verbose >= 1)
168
- OMQ::CLI::SocketSetup.attach_endpoints(push, outs, verbose: cfg.verbose >= 1)
169
-
170
- Barrier do |barrier|
171
- barrier.async { pull.peer_connected.wait }
172
- barrier.async { push.peer_connected.wait }
173
- end
174
-
175
- begin_proc, end_proc, eval_proc =
176
- OMQ::CLI::ExpressionEvaluator.compile_inside_ractor(cfg.recv_expr)
177
-
178
- fmt_in = OMQ::CLI::Formatter.new(cfg.format, compress: cfg.compress_in || cfg.compress)
179
- fmt_out = OMQ::CLI::Formatter.new(cfg.format, compress: cfg.compress_out || cfg.compress)
180
-
181
- _ctx = Object.new
182
- _ctx.instance_exec(&begin_proc) if begin_proc
183
-
184
- n_count = cfg.count
185
- begin
186
- if eval_proc
187
- if n_count && n_count > 0
188
- n_count.times do
189
- parts = pull.receive
190
- break if parts.nil?
191
- parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
192
- _ctx.instance_exec(fmt_in.decompress(parts), &eval_proc)
193
- )
194
- next if parts.nil?
195
- push << fmt_out.compress(parts) unless parts.empty?
196
- end
197
- else
198
- loop do
199
- parts = pull.receive
200
- break if parts.nil?
201
- parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
202
- _ctx.instance_exec(fmt_in.decompress(parts), &eval_proc)
203
- )
204
- next if parts.nil?
205
- push << fmt_out.compress(parts) unless parts.empty?
206
- end
207
- end
208
- else
209
- if n_count && n_count > 0
210
- n_count.times do
211
- parts = pull.receive
212
- break if parts.nil?
213
- push << fmt_out.compress(fmt_in.decompress(parts))
214
- end
215
- else
216
- loop do
217
- parts = pull.receive
218
- break if parts.nil?
219
- push << fmt_out.compress(fmt_in.decompress(parts))
220
- end
221
- end
222
- end
223
- rescue IO::TimeoutError, Async::TimeoutError
224
- # recv timed out — fall through to END block
225
- end
226
-
227
- if end_proc
228
- out = OMQ::CLI::ExpressionEvaluator.normalize_result(
229
- _ctx.instance_exec(&end_proc)
230
- )
231
- push << fmt_out.compress(out) if out && !out.empty?
232
- end
233
- ensure
234
- pull&.close
235
- push&.close
236
- end
237
- end
238
- end
239
- end
240
-
241
-
242
- # ── Shared helpers ────────────────────────────────────────────────
243
-
244
-
245
- # Resolves TCP hostnames to IP addresses so Ractors don't touch
246
- # Resolv::DefaultResolver (which is not shareable).
247
- #
248
- def preresolve_tcp(endpoints)
249
- endpoints.flat_map do |ep|
250
- url = ep.url
251
- if url.start_with?("tcp://")
252
- host, port = OMQ::Transport::TCP.parse_endpoint(url)
253
- Addrinfo.getaddrinfo(host, port, nil, :STREAM).map do |addr|
254
- ip = addr.ip_address
255
- ip = "[#{ip}]" if ip.include?(":")
256
- Endpoint.new("tcp://#{ip}:#{addr.ip_port}", ep.bind?)
257
- end
258
- else
259
- ep
260
- end
261
- end
262
- end
263
-
264
-
265
- def with_timeout(seconds)
266
- if seconds
267
- Async::Task.current.with_timeout(seconds) { yield }
268
- else
269
- yield
270
- end
271
- end
149
+ # ── Expression eval ──────────────────────────────────────────────
272
150
 
273
151
 
274
152
  def compile_expr
@@ -285,9 +163,7 @@ module OMQ
285
163
  end
286
164
 
287
165
 
288
- def log(msg)
289
- $stderr.write("#{msg}\n") if config.verbose >= 1
290
- end
166
+ # ── Event monitoring ─────────────────────────────────────────────
291
167
 
292
168
 
293
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.1"
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.1
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