omq-cli 0.7.1 → 0.8.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,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module CLI
5
+ # Worker that runs inside a Ractor for parallel socket modes (-P).
6
+ # Each worker owns its own Async reactor and socket instance.
7
+ #
8
+ # Supported socket types:
9
+ # - pull, gather (recv-only)
10
+ # - rep (recv-reply with echo/data/eval)
11
+ #
12
+ class ParallelWorker
13
+ def initialize(config, socket_sym, endpoints, output_port, log_port)
14
+ @config = config
15
+ @socket_sym = socket_sym
16
+ @endpoints = endpoints
17
+ @output_port = output_port
18
+ @log_port = log_port
19
+ end
20
+
21
+
22
+ def call
23
+ Async do
24
+ setup_socket
25
+ log_endpoints
26
+ start_monitors
27
+ wait_for_peer
28
+ compile_expr
29
+ run_loop
30
+ run_end_block
31
+ ensure
32
+ @sock&.close
33
+ end
34
+ end
35
+
36
+
37
+ private
38
+
39
+
40
+ def setup_socket
41
+ @sock = OMQ.const_get(@socket_sym).new
42
+ OMQ::CLI::SocketSetup.apply_options(@sock, @config)
43
+ @sock.identity = @config.identity if @config.identity
44
+ OMQ::CLI::SocketSetup.attach_endpoints(@sock, @endpoints, verbose: false)
45
+ end
46
+
47
+
48
+ def log_endpoints
49
+ return unless @config.verbose >= 1
50
+ @endpoints.each do |ep|
51
+ @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}")
52
+ end
53
+ end
54
+
55
+
56
+ def start_monitors
57
+ return unless @config.verbose >= 2
58
+ trace = @config.verbose >= 3
59
+ @sock.monitor(verbose: trace) do |event|
60
+ @log_port.send(format_event(event))
61
+ end
62
+ end
63
+
64
+
65
+ def format_event(event)
66
+ case event.type
67
+ when :message_sent
68
+ "omq: >> #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
69
+ when :message_received
70
+ "omq: << #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
71
+ else
72
+ ep = event.endpoint ? " #{event.endpoint}" : ""
73
+ detail = event.detail ? " #{event.detail}" : ""
74
+ "omq: #{event.type}#{ep}#{detail}"
75
+ end
76
+ end
77
+
78
+
79
+ def wait_for_peer
80
+ if @config.timeout
81
+ Fiber.scheduler.with_timeout(@config.timeout) do
82
+ @sock.peer_connected.wait
83
+ end
84
+ else
85
+ @sock.peer_connected.wait
86
+ end
87
+ rescue IO::TimeoutError, Async::TimeoutError
88
+ # Proceed anyway -- recv will timeout if no messages arrive
89
+ end
90
+
91
+
92
+ def compile_expr
93
+ @begin_proc, @end_proc, @eval_proc =
94
+ OMQ::CLI::ExpressionEvaluator.compile_inside_ractor(@config.recv_expr)
95
+ @fmt = OMQ::CLI::Formatter.new(@config.format, compress: @config.compress)
96
+ @ctx = Object.new
97
+ @ctx.instance_exec(&@begin_proc) if @begin_proc
98
+ end
99
+
100
+
101
+ def run_loop
102
+ case @config.type_name
103
+ when "pull", "gather"
104
+ run_recv_loop
105
+ when "rep"
106
+ run_rep_loop
107
+ end
108
+ end
109
+
110
+
111
+ # -- Recv-only loop (PULL, GATHER) -----------------------------------
112
+
113
+
114
+ def run_recv_loop
115
+ n = @config.count
116
+ i = 0
117
+ loop do
118
+ parts = @sock.receive
119
+ break if parts.nil?
120
+ parts = @fmt.decompress(parts)
121
+ if @eval_proc
122
+ parts = normalize(
123
+ @ctx.instance_exec(parts, &@eval_proc)
124
+ )
125
+ next if parts.nil?
126
+ end
127
+ output(parts) unless parts.empty?
128
+ i += 1
129
+ break if n && n > 0 && i >= n
130
+ end
131
+ rescue IO::TimeoutError, Async::TimeoutError
132
+ # recv timed out -- fall through to END block
133
+ end
134
+
135
+
136
+ # -- REP loop (recv request, process, send reply) --------------------
137
+
138
+
139
+ def run_rep_loop
140
+ n = @config.count
141
+ i = 0
142
+ loop do
143
+ parts = @sock.receive
144
+ break if parts.nil?
145
+ parts = @fmt.decompress(parts)
146
+ reply = compute_reply(parts)
147
+ output(reply)
148
+ @sock.send(@fmt.compress(reply))
149
+ i += 1
150
+ break if n && n > 0 && i >= n
151
+ end
152
+ rescue IO::TimeoutError, Async::TimeoutError
153
+ # recv timed out -- fall through to END block
154
+ end
155
+
156
+
157
+ def compute_reply(parts)
158
+ if @eval_proc
159
+ normalize(@ctx.instance_exec(parts, &@eval_proc)) || [""]
160
+ elsif @config.echo
161
+ parts
162
+ elsif @config.data
163
+ @fmt.decode(@config.data + "\n")
164
+ else
165
+ parts
166
+ end
167
+ end
168
+
169
+
170
+ # -- Output and helpers ----------------------------------------------
171
+
172
+
173
+ def output(parts)
174
+ return if @config.quiet || parts.nil?
175
+ @output_port.send(@fmt.encode(parts))
176
+ end
177
+
178
+
179
+ def normalize(result)
180
+ OMQ::CLI::ExpressionEvaluator.normalize_result(result)
181
+ end
182
+
183
+
184
+ def run_end_block
185
+ return unless @end_proc
186
+ out = normalize(@ctx.instance_exec(&@end_proc))
187
+ output(out) if out && !out.empty?
188
+ end
189
+ end
190
+ end
191
+ end
data/lib/omq/cli/pipe.rb CHANGED
@@ -5,14 +5,18 @@ module OMQ
5
5
  # Runner for the virtual "pipe" socket type (PULL -> eval -> PUSH).
6
6
  # Supports sequential and parallel (Ractor-based) processing modes.
7
7
  class PipeRunner
8
+ # Default HWM for pipe sockets when the user hasn't set one.
9
+ # Much lower than the socket default (1000) to bound memory
10
+ # with large messages in pipeline stages.
11
+ PIPE_HWM = 64
12
+
8
13
  # @return [Config] frozen CLI configuration
9
14
  attr_reader :config
10
15
 
11
16
 
12
17
  # @param config [Config] frozen CLI configuration
13
18
  def initialize(config)
14
- @config = config
15
- @fmt = Formatter.new(config.format, compress: config.compress)
19
+ @config = config
16
20
  @fmt_in = Formatter.new(config.format, compress: config.compress_in || config.compress)
17
21
  @fmt_out = Formatter.new(config.format, compress: config.compress_out || config.compress)
18
22
  end
@@ -43,30 +47,28 @@ module OMQ
43
47
  end
44
48
 
45
49
 
46
- def attach_endpoints(sock, endpoints)
47
- SocketSetup.attach_endpoints(sock, endpoints, verbose: config.verbose >= 1)
48
- end
49
-
50
-
51
- # ── Sequential ───────────────────────────────────────────────────
50
+ # -- Sequential ---------------------------------------------------
52
51
 
53
52
 
54
53
  def run_sequential(task)
54
+ set_pipe_process_title
55
55
  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
- )
56
+ @pull, @push = build_pull_push(in_eps, out_eps)
61
57
  compile_expr
62
58
  @sock = @pull # for eval instance_exec
63
59
  start_event_monitors if config.verbose >= 2
64
- with_timeout(config.timeout) do
60
+ wait_body = proc do
65
61
  Barrier do |barrier|
66
62
  barrier.async(annotation: "wait push peer") { @push.peer_connected.wait }
67
63
  barrier.async(annotation: "wait pull peer") { @pull.peer_connected.wait }
68
64
  end
69
65
  end
66
+
67
+ if config.timeout
68
+ Fiber.scheduler.with_timeout(config.timeout, &wait_body)
69
+ else
70
+ wait_body.call
71
+ end
70
72
  setup_sequential_transient(task)
71
73
  @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
72
74
  sequential_message_loop
@@ -77,23 +79,15 @@ module OMQ
77
79
  end
78
80
 
79
81
 
80
- def apply_socket_options(sock)
81
- sock.reconnect_interval = config.reconnect_ivl if config.reconnect_ivl
82
- sock.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
83
- sock.send_hwm = config.send_hwm if config.send_hwm
84
- sock.recv_hwm = config.recv_hwm if config.recv_hwm
85
- sock.sndbuf = config.sndbuf if config.sndbuf
86
- sock.rcvbuf = config.rcvbuf if config.rcvbuf
87
- end
88
-
89
-
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)
93
- apply_socket_options(pull)
94
- apply_socket_options(push)
95
- attach_endpoints(pull, in_eps)
96
- attach_endpoints(push, out_eps)
82
+ def build_pull_push(in_eps, out_eps)
83
+ pull = OMQ::PULL.new
84
+ push = OMQ::PUSH.new
85
+ SocketSetup.apply_options(pull, config)
86
+ SocketSetup.apply_options(push, config)
87
+ pull.recv_hwm = PIPE_HWM unless config.recv_hwm
88
+ push.send_hwm = PIPE_HWM unless config.send_hwm
89
+ SocketSetup.attach_endpoints(pull, in_eps, verbose: config.verbose >= 1)
90
+ SocketSetup.attach_endpoints(push, out_eps, verbose: config.verbose >= 1)
97
91
  [pull, push]
98
92
  end
99
93
 
@@ -117,8 +111,7 @@ module OMQ
117
111
  parts = @fmt_in.decompress(parts)
118
112
  parts = eval_recv_expr(parts)
119
113
  if parts && !parts.empty?
120
- out = @fmt_out.compress(parts)
121
- @push.send(out)
114
+ @push.send(@fmt_out.compress(parts))
122
115
  end
123
116
  i += 1
124
117
  break if n && n > 0 && i >= n
@@ -126,149 +119,47 @@ module OMQ
126
119
  end
127
120
 
128
121
 
129
- # ── Parallel ─────────────────────────────────────────────────────
122
+ # -- Parallel -----------------------------------------------------
130
123
 
131
124
 
132
125
  def run_parallel(task)
126
+ set_pipe_process_title
133
127
  OMQ.freeze_for_ractors!
134
128
  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)
129
+ in_eps = RactorHelpers.preresolve_tcp(in_eps)
130
+ out_eps = RactorHelpers.preresolve_tcp(out_eps)
131
+ log_port, log_thread = RactorHelpers.start_log_consumer
132
+ workers = config.parallel.times.map do
133
+ ::Ractor.new(config, in_eps, out_eps, log_port) do |cfg, ins, outs, lport|
134
+ PipeWorker.new(cfg, ins, outs, lport).call
135
+ end
136
+ end
138
137
  workers.each do |w|
139
138
  w.join
140
139
  rescue ::Ractor::RemoteError => e
141
140
  $stderr.write("omq: Ractor error: #{e.cause&.message || e.message}\n")
142
141
  end
142
+ ensure
143
+ RactorHelpers.stop_consumer(log_port, log_thread) if log_port
143
144
  end
144
145
 
145
146
 
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 ────────────────────────────────────────────────
147
+ # -- Process title -------------------------------------------------
243
148
 
244
149
 
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
150
+ def set_pipe_process_title
151
+ in_eps, out_eps = resolve_endpoints
152
+ title = ["omq pipe"]
153
+ title << "-z" if config.compress || config.compress_in || config.compress_out
154
+ title << "-P#{config.parallel}" if config.parallel
155
+ title.concat(in_eps.map(&:url))
156
+ title << "->"
157
+ title.concat(out_eps.map(&:url))
158
+ Process.setproctitle(title.join(" "))
262
159
  end
263
160
 
264
161
 
265
- def with_timeout(seconds)
266
- if seconds
267
- Async::Task.current.with_timeout(seconds) { yield }
268
- else
269
- yield
270
- end
271
- end
162
+ # -- Expression eval ----------------------------------------------
272
163
 
273
164
 
274
165
  def compile_expr
@@ -285,9 +176,7 @@ module OMQ
285
176
  end
286
177
 
287
178
 
288
- def log(msg)
289
- $stderr.write("#{msg}\n") if config.verbose >= 1
290
- end
179
+ # -- Event monitoring ---------------------------------------------
291
180
 
292
181
 
293
182
  def start_event_monitors
@@ -296,9 +185,9 @@ module OMQ
296
185
  sock.monitor(verbose: verbose) do |event|
297
186
  case event.type
298
187
  when :message_sent
299
- $stderr.write("omq: >> #{msg_preview(event.detail[:parts])}\n")
188
+ $stderr.write("omq: >> #{Formatter.preview(event.detail[:parts])}\n")
300
189
  when :message_received
301
- $stderr.write("omq: << #{msg_preview(event.detail[:parts])}\n")
190
+ $stderr.write("omq: << #{Formatter.preview(event.detail[:parts])}\n")
302
191
  else
303
192
  ep = event.endpoint ? " #{event.endpoint}" : ""
304
193
  detail = event.detail ? " #{event.detail}" : ""
@@ -307,15 +196,6 @@ module OMQ
307
196
  end
308
197
  end
309
198
  end
310
-
311
-
312
- def msg_preview(parts)
313
- parts.map { |p|
314
- bytes = p.b
315
- preview = bytes[0, 10].gsub(/[^[:print:]]/, ".")
316
- bytes.bytesize > 10 ? "#{preview}... (#{bytes.bytesize}B)" : preview
317
- }.join(" | ")
318
- end
319
199
  end
320
200
  end
321
201
  end
@@ -0,0 +1,149 @@
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
+ def initialize(config, in_eps, out_eps, log_port)
10
+ @config = config
11
+ @in_eps = in_eps
12
+ @out_eps = out_eps
13
+ @log_port = log_port
14
+ end
15
+
16
+
17
+ def call
18
+ Async do
19
+ setup_sockets
20
+ log_endpoints if @config.verbose >= 1
21
+ start_monitors if @config.verbose >= 2
22
+ wait_for_peers
23
+ compile_expr
24
+ run_message_loop
25
+ run_end_block
26
+ ensure
27
+ @pull&.close
28
+ @push&.close
29
+ end
30
+ end
31
+
32
+
33
+ private
34
+
35
+
36
+ def setup_sockets
37
+ @pull = OMQ::PULL.new
38
+ @push = OMQ::PUSH.new
39
+ OMQ::CLI::SocketSetup.apply_options(@pull, @config)
40
+ OMQ::CLI::SocketSetup.apply_options(@push, @config)
41
+ @pull.recv_hwm = PipeRunner::PIPE_HWM unless @config.recv_hwm
42
+ @push.send_hwm = PipeRunner::PIPE_HWM unless @config.send_hwm
43
+ OMQ::CLI::SocketSetup.attach_endpoints(@pull, @in_eps, verbose: false)
44
+ OMQ::CLI::SocketSetup.attach_endpoints(@push, @out_eps, verbose: false)
45
+ end
46
+
47
+
48
+ def log_endpoints
49
+ @in_eps.each { |ep| @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}") }
50
+ @out_eps.each { |ep| @log_port.send(ep.bind? ? "Bound to #{ep.url}" : "Connecting to #{ep.url}") }
51
+ end
52
+
53
+
54
+ def start_monitors
55
+ trace = @config.verbose >= 3
56
+ [@pull, @push].each do |sock|
57
+ sock.monitor(verbose: trace) do |event|
58
+ @log_port.send(format_event(event))
59
+ end
60
+ end
61
+ end
62
+
63
+
64
+ def format_event(event)
65
+ case event.type
66
+ when :message_sent
67
+ "omq: >> #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
68
+ when :message_received
69
+ "omq: << #{OMQ::CLI::Formatter.preview(event.detail[:parts])}"
70
+ else
71
+ ep = event.endpoint ? " #{event.endpoint}" : ""
72
+ detail = event.detail ? " #{event.detail}" : ""
73
+ "omq: #{event.type}#{ep}#{detail}"
74
+ end
75
+ end
76
+
77
+
78
+ def wait_for_peers
79
+ Barrier do |barrier|
80
+ barrier.async { @pull.peer_connected.wait }
81
+ barrier.async { @push.peer_connected.wait }
82
+ end
83
+ end
84
+
85
+
86
+ def compile_expr
87
+ @begin_proc, @end_proc, @eval_proc =
88
+ OMQ::CLI::ExpressionEvaluator.compile_inside_ractor(@config.recv_expr)
89
+ @fmt_in = OMQ::CLI::Formatter.new(@config.format, compress: @config.compress_in || @config.compress)
90
+ @fmt_out = OMQ::CLI::Formatter.new(@config.format, compress: @config.compress_out || @config.compress)
91
+ @ctx = Object.new
92
+ @ctx.instance_exec(&@begin_proc) if @begin_proc
93
+ end
94
+
95
+
96
+ def run_message_loop
97
+ n = @config.count
98
+ if @eval_proc
99
+ if n && n > 0
100
+ n.times do
101
+ parts = @pull.receive
102
+ break if parts.nil?
103
+ parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
104
+ @ctx.instance_exec(@fmt_in.decompress(parts), &@eval_proc)
105
+ )
106
+ next if parts.nil?
107
+ @push << @fmt_out.compress(parts) unless parts.empty?
108
+ end
109
+ else
110
+ loop do
111
+ parts = @pull.receive
112
+ break if parts.nil?
113
+ parts = OMQ::CLI::ExpressionEvaluator.normalize_result(
114
+ @ctx.instance_exec(@fmt_in.decompress(parts), &@eval_proc)
115
+ )
116
+ next if parts.nil?
117
+ @push << @fmt_out.compress(parts) unless parts.empty?
118
+ end
119
+ end
120
+ else
121
+ if n && n > 0
122
+ n.times do
123
+ parts = @pull.receive
124
+ break if parts.nil?
125
+ @push << @fmt_out.compress(@fmt_in.decompress(parts))
126
+ end
127
+ else
128
+ loop do
129
+ parts = @pull.receive
130
+ break if parts.nil?
131
+ @push << @fmt_out.compress(@fmt_in.decompress(parts))
132
+ end
133
+ end
134
+ end
135
+ rescue IO::TimeoutError, Async::TimeoutError
136
+ # recv timed out -- fall through to END block
137
+ end
138
+
139
+
140
+ def run_end_block
141
+ return unless @end_proc
142
+ out = OMQ::CLI::ExpressionEvaluator.normalize_result(
143
+ @ctx.instance_exec(&@end_proc)
144
+ )
145
+ @push << @fmt_out.compress(out) if out && !out.empty?
146
+ end
147
+ end
148
+ end
149
+ end
@@ -10,6 +10,14 @@ module OMQ
10
10
 
11
11
  # Runner for PULL sockets (receive-only pipeline consumer).
12
12
  class PullRunner < BaseRunner
13
+ def call(task)
14
+ config.parallel ? run_parallel_workers(:PULL) : super
15
+ end
16
+
17
+
18
+ private
19
+
20
+
13
21
  def run_loop(task) = run_recv_logic
14
22
  end
15
23
  end