omq 0.6.5 → 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.
data/lib/omq/cli/pipe.rb CHANGED
@@ -24,9 +24,22 @@ module OMQ
24
24
  private
25
25
 
26
26
 
27
+ def resolve_endpoints
28
+ if config.in_endpoints.any?
29
+ [config.in_endpoints, config.out_endpoints]
30
+ else
31
+ [[config.endpoints[0]], [config.endpoints[1]]]
32
+ end
33
+ end
34
+
35
+
36
+ def attach_endpoints(sock, endpoints)
37
+ endpoints.each { |ep| ep.bind? ? sock.bind(ep.url) : sock.connect(ep.url) }
38
+ end
39
+
40
+
27
41
  def run_sequential(task)
28
- pull_ep = config.endpoints[0]
29
- push_ep = config.endpoints[1]
42
+ in_eps, out_eps = resolve_endpoints
30
43
 
31
44
  @pull = OMQ::PULL.new(linger: config.linger, recv_timeout: config.timeout)
32
45
  @push = OMQ::PUSH.new(linger: config.linger, send_timeout: config.timeout)
@@ -35,11 +48,11 @@ module OMQ
35
48
  @pull.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
36
49
  @push.heartbeat_interval = config.heartbeat_ivl if config.heartbeat_ivl
37
50
 
38
- pull_ep.bind? ? @pull.bind(pull_ep.url) : @pull.connect(pull_ep.url)
39
- push_ep.bind? ? @push.bind(push_ep.url) : @push.connect(push_ep.url)
51
+ attach_endpoints(@pull, in_eps)
52
+ attach_endpoints(@push, out_eps)
40
53
 
41
54
  compile_expr
42
- @sock = @pull # for eval_expr instance_exec
55
+ @sock = @pull # for eval instance_exec
43
56
 
44
57
  with_timeout(config.timeout) do
45
58
  @push.peer_connected.wait
@@ -54,7 +67,7 @@ module OMQ
54
67
  end
55
68
  end
56
69
 
57
- @sock.instance_exec(&@begin_proc) if @begin_proc
70
+ @sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
58
71
 
59
72
  n = config.count
60
73
  i = 0
@@ -62,7 +75,7 @@ module OMQ
62
75
  parts = @pull.receive
63
76
  break if parts.nil?
64
77
  parts = @fmt.decompress(parts)
65
- parts = eval_expr(parts)
78
+ parts = eval_recv_expr(parts)
66
79
  if parts && !parts.empty?
67
80
  @push.send(@fmt.compress(parts))
68
81
  end
@@ -70,7 +83,7 @@ module OMQ
70
83
  break if n && n > 0 && i >= n
71
84
  end
72
85
 
73
- @sock.instance_exec(&@end_proc) if @end_proc
86
+ @sock.instance_exec(&@recv_end_proc) if @recv_end_proc
74
87
  ensure
75
88
  @pull&.close
76
89
  @push&.close
@@ -86,7 +99,7 @@ module OMQ
86
99
  Sync do |task|
87
100
  # Parse BEGIN/END blocks and per-message expression
88
101
  begin_proc = end_proc = eval_proc = nil
89
- if cfg.expr
102
+ if cfg.recv_expr
90
103
  extract = ->(src, kw) {
91
104
  s = src.index(/#{kw}\s*\{/)
92
105
  return [src, nil] unless s
@@ -97,7 +110,7 @@ module OMQ
97
110
  end
98
111
  [src[0...s] + src[j..], src[(i + 1)..(j - 2)]]
99
112
  }
100
- expr, begin_body = extract.(cfg.expr, "BEGIN")
113
+ expr, begin_body = extract.(cfg.recv_expr, "BEGIN")
101
114
  expr, end_body = extract.(expr, "END")
102
115
  begin_proc = eval("proc { #{begin_body} }") if begin_body
103
116
  end_proc = eval("proc { #{end_body} }") if end_body
@@ -115,8 +128,10 @@ module OMQ
115
128
  push.reconnect_interval = cfg.reconnect_ivl if cfg.reconnect_ivl
116
129
  pull.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
117
130
  push.heartbeat_interval = cfg.heartbeat_ivl if cfg.heartbeat_ivl
118
- pull.connect(cfg.endpoints[0].url)
119
- push.connect(cfg.endpoints[1].url)
131
+ in_eps = cfg.in_endpoints.any? ? cfg.in_endpoints : [cfg.endpoints[0]]
132
+ out_eps = cfg.out_endpoints.any? ? cfg.out_endpoints : [cfg.endpoints[1]]
133
+ in_eps.each { |ep| pull.connect(ep.url) }
134
+ out_eps.each { |ep| push.connect(ep.url) }
120
135
 
121
136
  if cfg.timeout
122
137
  task.with_timeout(cfg.timeout) do
@@ -187,11 +202,12 @@ module OMQ
187
202
 
188
203
 
189
204
  def compile_expr
190
- return unless config.expr
191
- expr, begin_body, end_body = extract_blocks(config.expr)
192
- @begin_proc = eval("proc { #{begin_body} }") if begin_body
193
- @end_proc = eval("proc { #{end_body} }") if end_body
194
- @eval_proc = eval("proc { $_ = $F&.first; #{expr} }") if expr && !expr.strip.empty?
205
+ src = config.recv_expr
206
+ return unless src
207
+ expr, begin_body, end_body = extract_blocks(src)
208
+ @recv_begin_proc = eval("proc { #{begin_body} }") if begin_body
209
+ @recv_end_proc = eval("proc { #{end_body} }") if end_body
210
+ @recv_eval_proc = eval("proc { $_ = $F&.first; #{expr} }") if expr && !expr.strip.empty?
195
211
  end
196
212
 
197
213
 
@@ -224,10 +240,10 @@ module OMQ
224
240
  end
225
241
 
226
242
 
227
- def eval_expr(parts)
228
- return parts unless @eval_proc
243
+ def eval_recv_expr(parts)
244
+ return parts unless @recv_eval_proc
229
245
  $F = parts
230
- result = @sock.instance_exec(&@eval_proc)
246
+ result = @sock.instance_exec(&@recv_eval_proc)
231
247
  return nil if result.nil? || result.equal?(@sock)
232
248
  return [result] if config.format == :marshal
233
249
  case result
@@ -236,7 +252,7 @@ module OMQ
236
252
  else [result.to_str]
237
253
  end
238
254
  rescue => e
239
- $stderr.puts "omq: -e error: #{e.message} (#{e.class})"
255
+ $stderr.puts "omq: eval error: #{e.message} (#{e.class})"
240
256
  exit 3
241
257
  end
242
258
 
@@ -14,10 +14,12 @@ module OMQ
14
14
  loop do
15
15
  parts = read_next
16
16
  break unless parts
17
+ parts = eval_send_expr(parts)
18
+ next unless parts
17
19
  send_msg(parts)
18
20
  reply = recv_msg
19
21
  break if reply.nil?
20
- reply = eval_expr(reply)
22
+ reply = eval_recv_expr(reply)
21
23
  output(reply)
22
24
  i += 1
23
25
  break if n && n > 0 && i >= n
@@ -29,10 +31,12 @@ module OMQ
29
31
  loop do
30
32
  parts = read_next
31
33
  break unless parts
34
+ parts = eval_send_expr(parts)
35
+ next unless parts
32
36
  send_msg(parts)
33
37
  reply = recv_msg
34
38
  break if reply.nil?
35
- reply = eval_expr(reply)
39
+ reply = eval_recv_expr(reply)
36
40
  output(reply)
37
41
  i += 1
38
42
  break if n && n > 0 && i >= n
@@ -53,8 +57,8 @@ module OMQ
53
57
  loop do
54
58
  msg = recv_msg
55
59
  break if msg.nil?
56
- if config.expr
57
- reply = eval_expr(msg)
60
+ if config.recv_expr || @recv_eval_proc
61
+ reply = eval_recv_expr(msg)
58
62
  unless reply.equal?(SENT)
59
63
  output(reply)
60
64
  send_msg(reply || [""])
@@ -20,7 +20,7 @@ module OMQ
20
20
  identity = parts.shift
21
21
  parts.shift if parts.first == ""
22
22
  parts = @fmt.decompress(parts)
23
- result = eval_expr([display_routing_id(identity), *parts])
23
+ result = eval_recv_expr([display_routing_id(identity), *parts])
24
24
  output(result)
25
25
  i += 1
26
26
  break if n && n > 0 && i >= n
@@ -35,18 +35,18 @@ module OMQ
35
35
  Async::Loop.quantized(interval: config.interval) do
36
36
  parts = read_next
37
37
  break unless parts
38
- send_targeted_or_plain(parts)
38
+ send_targeted_or_eval(parts)
39
39
  i += 1
40
40
  break if n && n > 0 && i >= n
41
41
  end
42
42
  elsif config.data || config.file
43
43
  parts = read_next
44
- send_targeted_or_plain(parts) if parts
44
+ send_targeted_or_eval(parts) if parts
45
45
  else
46
46
  loop do
47
47
  parts = read_next
48
48
  break unless parts
49
- send_targeted_or_plain(parts)
49
+ send_targeted_or_eval(parts)
50
50
  i += 1
51
51
  break if n && n > 0 && i >= n
52
52
  end
@@ -57,8 +57,14 @@ module OMQ
57
57
  end
58
58
 
59
59
 
60
- def send_targeted_or_plain(parts)
61
- if config.target
60
+ def send_targeted_or_eval(parts)
61
+ if @send_eval_proc
62
+ parts = eval_send_expr(parts)
63
+ return unless parts
64
+ identity = resolve_target(parts.shift)
65
+ payload = @fmt.compress(parts)
66
+ @sock.send([identity, "", *payload])
67
+ elsif config.target
62
68
  payload = @fmt.compress(parts)
63
69
  @sock.send([resolve_target(config.target), "", *payload])
64
70
  else
data/lib/omq/cli.rb CHANGED
@@ -17,6 +17,15 @@ require_relative "cli/peer"
17
17
  require_relative "cli/pipe"
18
18
 
19
19
  module OMQ
20
+
21
+ class << self
22
+ attr_reader :outgoing_proc, :incoming_proc
23
+
24
+ def outgoing(&block) = @outgoing_proc = block
25
+ def incoming(&block) = @incoming_proc = block
26
+ end
27
+
28
+
20
29
  module CLI
21
30
  SOCKET_TYPE_NAMES = %w[
22
31
  req rep pub sub push pull pair dealer router
@@ -56,7 +65,7 @@ module OMQ
56
65
  └─────┘ "HELLO" └─────┘
57
66
 
58
67
  # terminal 1: echo server
59
- omq rep --bind tcp://:5555 --eval '$F.map(&:upcase)'
68
+ omq rep --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
60
69
 
61
70
  # terminal 2: send a request
62
71
  echo "hello" | omq req --connect tcp://localhost:5555
@@ -79,7 +88,7 @@ module OMQ
79
88
 
80
89
  ── Periodic Publish ───────────────────────────────────────────
81
90
 
82
- ┌─────┐ "tick 1" ┌─────┐
91
+ ┌─────┐ "tick 1" ┌─────┐
83
92
  │ PUB │──(every 1s)─→│ SUB │
84
93
  └─────┘ └─────┘
85
94
 
@@ -121,27 +130,31 @@ module OMQ
121
130
 
122
131
  # terminal 2: worker — uppercase each message
123
132
  omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
124
-
125
133
  # terminal 3: collector
126
134
  omq pull --bind ipc://@sink
127
135
 
128
136
  # 4 Ractor workers in a single process (-P)
129
- omq pipe -c ipc://@work -c ipc://@sink -P 4 \
130
- -r ./fib.rb -e 'fib(Integer($_)).to_s'
137
+ omq pipe -c ipc://@work -c ipc://@sink -P4 -r./fib -e 'fib(Integer($_)).to_s'
131
138
 
132
139
  # exit when producer disconnects (--transient)
133
- omq pipe -c ipc://@work -c ipc://@sink --transient \
134
- -e '$F.map(&:upcase)'
140
+ omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F.map(&:upcase)'
141
+
142
+ # fan-in: multiple sources → one sink
143
+ omq pipe --in -c ipc://@work1 -c ipc://@work2 \
144
+ --out -c ipc://@sink -e '$F.map(&:upcase)'
145
+
146
+ # fan-out: one source → multiple sinks (round-robin)
147
+ omq pipe --in -b tcp://:5555 --out -c ipc://@sink1 -c ipc://@sink2 -e '$F'
135
148
 
136
149
  ── CLIENT / SERVER (draft) ──────────────────────────────────
137
150
 
138
151
  ┌────────┐ "hello" ┌────────┐
139
- │ CLIENT │───────────→│ SERVER │ --eval '$F.map(&:upcase)'
152
+ │ CLIENT │───────────→│ SERVER │ --recv-eval '$F.map(&:upcase)'
140
153
  │ │←───────────│ │
141
154
  └────────┘ "HELLO" └────────┘
142
155
 
143
156
  # terminal 1: upcasing server
144
- omq server --bind tcp://:5555 --eval '$F.map(&:upcase)'
157
+ omq server --bind tcp://:5555 --recv-eval '$F.map(&:upcase)'
145
158
 
146
159
  # terminal 2: client
147
160
  echo "hello" | omq client --connect tcp://localhost:5555
@@ -187,30 +200,45 @@ module OMQ
187
200
  omq router --bind tcp://:5555
188
201
 
189
202
  # terminal 2: dealer with identity
190
- echo "hello" | omq dealer --connect tcp://localhost:5555 \
191
- --identity worker-1
203
+ echo "hello" | omq dealer --connect tcp://localhost:5555 --identity worker-1
192
204
 
193
205
  ── Ruby Eval ────────────────────────────────────────────────
194
206
 
195
- # filter: only pass messages containing "error"
196
- omq pull --bind tcp://:5557 \
197
- --eval '$F.first.include?("error") ? $F : nil'
207
+ # filter incoming: only pass messages containing "error"
208
+ omq pull -b tcp://:5557 --recv-eval '$F.first.include?("error") ? $F : nil'
198
209
 
199
- # transform with gems
200
- omq sub --connect tcp://localhost:5556 --require json \
201
- --eval 'JSON.parse($F.first)["temperature"]'
210
+ # transform incoming with gems
211
+ omq sub -c tcp://localhost:5556 -rjson -e 'JSON.parse($F.first)["temperature"]'
202
212
 
203
- # require a local file, use its methods in --eval
204
- omq rep --bind tcp://:5555 --require ./transform.rb \
205
- --eval 'upcase_all($F)'
213
+ # require a local file, use its methods
214
+ omq rep --bind tcp://:5555 --require ./transform.rb -e 'upcase_all($F)'
206
215
 
207
216
  # next skips, break stops — regexps match against $_
208
- omq pull --bind tcp://:5557 \
209
- --eval 'next if /^#/; break if /quit/; $F'
217
+ omq pull -b tcp://:5557 -e 'next if /^#/; break if /quit/; $F'
210
218
 
211
219
  # BEGIN/END blocks (like awk) — accumulate and summarize
212
- omq pull --bind tcp://:5557 \
213
- --eval 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
220
+ omq pull -b tcp://:5557 -e 'BEGIN{@sum = 0} @sum += Integer($_); nil END{puts @sum}'
221
+
222
+ # transform outgoing messages
223
+ echo hello | omq push -c tcp://localhost:5557 --send-eval '$F.map(&:upcase)'
224
+
225
+ # REQ: transform request and reply independently
226
+ echo hello | omq req -c tcp://localhost:5555 -E '$F.map(&:upcase)' -e '$_'
227
+
228
+ ── Script Handlers (-r) ────────────────────────────────────
229
+
230
+ # handler.rb — register transforms from a file
231
+ # db = PG.connect("dbname=app")
232
+ # OMQ.incoming { |first_part, _| db.exec(first_part).values.flatten }
233
+ # at_exit { db.close }
234
+ omq pull --bind tcp://:5557 -r./handler.rb
235
+
236
+ # combine script handlers with inline eval
237
+ omq req -c tcp://localhost:5555 -r./handler.rb -E '$F.map(&:upcase)'
238
+
239
+ # OMQ.outgoing { |msg| ... } — registered outgoing transform
240
+ # OMQ.incoming { |msg| ... } — registered incoming transform
241
+ # CLI flags (-e/-E) override registered handlers
214
242
  TEXT
215
243
 
216
244
  module_function
@@ -299,6 +327,8 @@ module OMQ
299
327
  endpoints: [],
300
328
  connects: [],
301
329
  binds: [],
330
+ in_endpoints: [],
331
+ out_endpoints: [],
302
332
  data: nil,
303
333
  file: nil,
304
334
  format: :ascii,
@@ -316,7 +346,8 @@ module OMQ
316
346
  heartbeat_ivl: nil,
317
347
  conflate: false,
318
348
  compress: false,
319
- expr: nil,
349
+ send_expr: nil,
350
+ recv_expr: nil,
320
351
  parallel: nil,
321
352
  transient: false,
322
353
  verbose: false,
@@ -326,6 +357,8 @@ module OMQ
326
357
  curve_server_key: nil,
327
358
  }
328
359
 
360
+ pipe_side = nil # nil = legacy positional mode; :in/:out = modal
361
+
329
362
  parser = OptionParser.new do |o|
330
363
  o.banner = "Usage: omq TYPE [options]\n\n" \
331
364
  "Types: req, rep, pub, sub, push, pull, pair, dealer, router\n" \
@@ -333,8 +366,24 @@ module OMQ
333
366
  "Virtual: pipe (PULL → eval → PUSH)\n\n"
334
367
 
335
368
  o.separator "Connection:"
336
- o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, false); opts[:connects] << v }
337
- o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:endpoints] << Endpoint.new(v, true); opts[:binds] << v }
369
+ o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v|
370
+ ep = Endpoint.new(v, false)
371
+ case pipe_side
372
+ when :in then opts[:in_endpoints] << ep
373
+ when :out then opts[:out_endpoints] << ep
374
+ else opts[:endpoints] << ep; opts[:connects] << v
375
+ end
376
+ }
377
+ o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v|
378
+ ep = Endpoint.new(v, true)
379
+ case pipe_side
380
+ when :in then opts[:in_endpoints] << ep
381
+ when :out then opts[:out_endpoints] << ep
382
+ else opts[:endpoints] << ep; opts[:binds] << v
383
+ end
384
+ }
385
+ o.on("--in", "Pipe: subsequent -b/-c attach to input (PULL) side") { pipe_side = :in }
386
+ o.on("--out", "Pipe: subsequent -b/-c attach to output (PUSH) side") { pipe_side = :out }
338
387
 
339
388
  o.separator "\nData source (REP: reply source):"
340
389
  o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
@@ -380,9 +429,11 @@ module OMQ
380
429
  o.separator "\nCompression:"
381
430
  o.on("-z", "--compress", "Zstandard compression per frame") { opts[:compress] = true }
382
431
 
383
- o.separator "\nProcessing:"
384
- o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
385
- o.on("-r", "--require LIB", "Require library or file (-r./lib.rb)") { |v|
432
+ o.separator "\nProcessing (-e = incoming, -E = outgoing):"
433
+ o.on("-e", "--recv-eval EXPR", "Eval Ruby for each incoming message ($F = parts)") { |v| opts[:recv_expr] = v }
434
+ o.on("-E", "--send-eval EXPR", "Eval Ruby for each outgoing message ($F = parts)") { |v| opts[:send_expr] = v }
435
+ o.on("-r", "--require LIB", "Require lib/file; scripts can register OMQ.outgoing/incoming") { |v|
436
+ require_relative "../omq" unless defined?(OMQ::VERSION)
386
437
  v.start_with?("./", "../") ? require(File.expand_path(v)) : require(v)
387
438
  }
388
439
  o.on("-P", "--parallel [N]", Integer, "Parallel Ractor workers (pipe only, default: nproc)") { |v|
@@ -421,10 +472,13 @@ module OMQ
421
472
 
422
473
  opts[:type_name] = type_name.downcase
423
474
 
424
- normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
475
+ normalize = ->(url) { url.sub(%r{\Atcp://\*:}, "tcp://0.0.0.0:").sub(%r{\Atcp://:}, "tcp://localhost:") }
476
+ normalize_ep = ->(ep) { Endpoint.new(normalize.call(ep.url), ep.bind?) }
425
477
  opts[:binds].map!(&normalize)
426
478
  opts[:connects].map!(&normalize)
427
- opts[:endpoints].map! { |ep| Endpoint.new(normalize.call(ep.url), ep.bind?) }
479
+ opts[:endpoints].map!(&normalize_ep)
480
+ opts[:in_endpoints].map!(&normalize_ep)
481
+ opts[:out_endpoints].map!(&normalize_ep)
428
482
 
429
483
  opts
430
484
  end
@@ -436,8 +490,16 @@ module OMQ
436
490
  type_name = opts[:type_name]
437
491
 
438
492
  if type_name == "pipe"
439
- abort "pipe requires exactly 2 endpoints (pull-side and push-side)" if opts[:endpoints].size != 2
493
+ has_in_out = opts[:in_endpoints].any? || opts[:out_endpoints].any?
494
+ if has_in_out
495
+ abort "pipe --in requires at least one endpoint" if opts[:in_endpoints].empty?
496
+ abort "pipe --out requires at least one endpoint" if opts[:out_endpoints].empty?
497
+ abort "pipe: don't mix --in/--out with bare -b/-c endpoints" unless opts[:endpoints].empty?
498
+ else
499
+ abort "pipe requires exactly 2 endpoints (pull-side and push-side), or use --in/--out" if opts[:endpoints].size != 2
500
+ end
440
501
  else
502
+ abort "--in/--out are only valid for pipe" if opts[:in_endpoints].any? || opts[:out_endpoints].any?
441
503
  abort "At least one --connect or --bind is required" if opts[:connects].empty? && opts[:binds].empty?
442
504
  end
443
505
  abort "--data and --file are mutually exclusive" if opts[:data] && opts[:file]
@@ -447,11 +509,15 @@ module OMQ
447
509
  abort "--identity is only valid for DEALER/ROUTER" if opts[:identity] && !%w[dealer router].include?(type_name)
448
510
  abort "--target is only valid for ROUTER/SERVER/PEER" if opts[:target] && !%w[router server peer].include?(type_name)
449
511
  abort "--conflate is only valid for PUB/RADIO" if opts[:conflate] && !%w[pub radio].include?(type_name)
512
+ abort "--recv-eval is not valid for send-only sockets (use --send-eval / -E)" if opts[:recv_expr] && SEND_ONLY.include?(type_name)
513
+ abort "--send-eval is not valid for recv-only sockets (use --recv-eval / -e)" if opts[:send_expr] && RECV_ONLY.include?(type_name)
514
+ abort "--send-eval and --target are mutually exclusive" if opts[:send_expr] && opts[:target]
450
515
 
451
516
  if opts[:parallel]
452
517
  abort "-P/--parallel is only valid for pipe" unless type_name == "pipe"
453
518
  abort "-P/--parallel must be >= 2" if opts[:parallel] < 2
454
- abort "-P/--parallel requires both endpoints to use --connect (not --bind)" if opts[:endpoints].any?(&:bind?)
519
+ all_pipe_eps = opts[:in_endpoints] + opts[:out_endpoints] + opts[:endpoints]
520
+ abort "-P/--parallel requires all endpoints to use --connect (not --bind)" if all_pipe_eps.any?(&:bind?)
455
521
  end
456
522
 
457
523
  (opts[:connects] + opts[:binds]).each do |url|
data/lib/omq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.6.5"
4
+ VERSION = "0.8.0"
5
5
  end
@@ -215,29 +215,50 @@ module OMQ
215
215
  # fast path when the connection is a DirectPipe.
216
216
  #
217
217
  # @param conn [Connection, Transport::Inproc::DirectPipe]
218
+ # Starts a recv pump that dequeues messages from a connection
219
+ # and enqueues them into the routing strategy's recv queue.
220
+ #
221
+ # When a block is given, each message is yielded for transformation
222
+ # before enqueueing. The block is compiled at the call site, giving
223
+ # YJIT a monomorphic call per routing strategy instead of a shared
224
+ # megamorphic `transform.call` dispatch.
225
+ #
226
+ # @param conn [Connection, Transport::Inproc::DirectPipe]
218
227
  # @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
219
- # @param transform [#call, nil] optional message transform
228
+ # @yield [msg] optional per-message transform
220
229
  # @return [#stop, nil] pump task handle, or nil for DirectPipe bypass
221
230
  #
222
- def start_recv_pump(conn, recv_queue, transform: nil)
231
+ def start_recv_pump(conn, recv_queue, &transform)
223
232
  if conn.is_a?(Transport::Inproc::DirectPipe) && conn.peer
224
233
  conn.peer.direct_recv_queue = recv_queue
225
234
  conn.peer.direct_recv_transform = transform
226
235
  return nil
227
236
  end
228
237
 
229
- Reactor.spawn_pump(annotation: "recv pump") do
230
- loop do
231
- msg = conn.receive_message
232
- msg = transform ? transform.call(msg).freeze : msg
233
- recv_queue.enqueue(msg)
238
+ if transform
239
+ Reactor.spawn_pump(annotation: "recv pump") do
240
+ loop do
241
+ msg = conn.receive_message
242
+ msg = transform.call(msg).freeze
243
+ recv_queue.enqueue(msg)
244
+ end
245
+ rescue Async::Stop
246
+ rescue ProtocolError, *CONNECTION_LOST
247
+ connection_lost(conn)
248
+ rescue => error
249
+ signal_fatal_error(error)
250
+ end
251
+ else
252
+ Reactor.spawn_pump(annotation: "recv pump") do
253
+ loop do
254
+ recv_queue.enqueue(conn.receive_message)
255
+ end
256
+ rescue Async::Stop
257
+ rescue ProtocolError, *CONNECTION_LOST
258
+ connection_lost(conn)
259
+ rescue => error
260
+ signal_fatal_error(error)
234
261
  end
235
- rescue Async::Stop
236
- # normal shutdown
237
- rescue ProtocolError, *CONNECTION_LOST
238
- connection_lost(conn)
239
- rescue => error
240
- signal_fatal_error(error)
241
262
  end
242
263
  end
243
264
 
@@ -411,17 +432,14 @@ module OMQ
411
432
  def setup_connection(io, as_server:, endpoint: nil, done: nil)
412
433
  conn = Connection.new(
413
434
  io,
414
- socket_type: @socket_type.to_s,
415
- identity: @options.identity,
416
- as_server: as_server,
417
- mechanism: @options.mechanism,
418
- heartbeat_interval: @options.heartbeat_interval,
419
- heartbeat_ttl: @options.heartbeat_ttl,
420
- heartbeat_timeout: @options.heartbeat_timeout,
421
- max_message_size: @options.max_message_size,
435
+ socket_type: @socket_type.to_s,
436
+ identity: @options.identity,
437
+ as_server: as_server,
438
+ mechanism: @options.mechanism&.dup,
439
+ max_message_size: @options.max_message_size,
422
440
  )
423
441
  conn.handshake!
424
- conn.start_heartbeat
442
+ start_heartbeat(conn)
425
443
  @connections << conn
426
444
  @connection_endpoints[conn] = endpoint if endpoint
427
445
  @connection_promises[conn] = done if done
@@ -433,6 +451,35 @@ module OMQ
433
451
  end
434
452
 
435
453
 
454
+ # Spawns a heartbeat task for the connection.
455
+ # The connection only tracks timestamps — the engine drives the loop.
456
+ #
457
+ # @param conn [Connection]
458
+ # @return [void]
459
+ #
460
+ def start_heartbeat(conn)
461
+ interval = @options.heartbeat_interval
462
+ return unless interval
463
+
464
+ ttl = @options.heartbeat_ttl || interval
465
+ timeout = @options.heartbeat_timeout || interval
466
+ conn.touch_heartbeat
467
+
468
+ @tasks << Reactor.spawn_pump(annotation: "heartbeat") do
469
+ loop do
470
+ sleep interval
471
+ conn.send_command(Codec::Command.ping(ttl: ttl, context: "".b))
472
+ if conn.heartbeat_expired?(timeout)
473
+ conn.close
474
+ break
475
+ end
476
+ end
477
+ rescue *CONNECTION_LOST
478
+ # connection closed
479
+ end
480
+ end
481
+
482
+
436
483
  # Spawns a background task that reconnects to the given endpoint
437
484
  # with exponential back-off based on the reconnect_interval option.
438
485
  #
@@ -34,8 +34,9 @@ module OMQ
34
34
  routing_id = SecureRandom.bytes(4)
35
35
  @connections_by_routing_id[routing_id] = connection
36
36
 
37
- task = @engine.start_recv_pump(connection, @recv_queue,
38
- transform: ->(msg) { [routing_id, *msg] })
37
+ task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
38
+ [routing_id, *msg]
39
+ end
39
40
  @tasks << task if task
40
41
 
41
42
  start_send_pump unless @send_pump_started
@@ -29,14 +29,13 @@ module OMQ
29
29
  # @param connection [Connection]
30
30
  #
31
31
  def connection_added(connection)
32
- transform = ->(msg) {
32
+ task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
33
33
  delimiter = msg.index(&:empty?) || msg.size
34
34
  envelope = msg[0, delimiter]
35
35
  body = msg[(delimiter + 1)..] || []
36
36
  @pending_replies << { conn: connection, envelope: envelope }
37
37
  body
38
- }
39
- task = @engine.start_recv_pump(connection, @recv_queue, transform: transform)
38
+ end
40
39
  @tasks << task if task
41
40
  start_send_pump unless @send_pump_started
42
41
  end
@@ -28,8 +28,9 @@ module OMQ
28
28
  def connection_added(connection)
29
29
  @connections << connection
30
30
  signal_connection_available
31
- task = @engine.start_recv_pump(connection, @recv_queue,
32
- transform: method(:transform_recv))
31
+ task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
32
+ msg.first&.empty? ? msg[1..] : msg
33
+ end
33
34
  @tasks << task if task
34
35
  start_send_pump unless @send_pump_started
35
36
  end
@@ -58,11 +59,6 @@ module OMQ
58
59
  #
59
60
  def transform_send(parts) = ["".b, *parts]
60
61
 
61
- # REQ strips the leading empty delimiter frame on receive.
62
- #
63
- def transform_recv(msg)
64
- msg.first&.empty? ? msg[1..] : msg
65
- end
66
62
  end
67
63
  end
68
64
  end