omq 0.19.2 → 0.19.3

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: 19b283aa20e543617cd7be9c899d2954c219631fd57417536be4fb26798e6e85
4
- data.tar.gz: a39d1c870e6171726a00a5f1809556e3ce7c3a8c0e4b0e84722eae15f83f93b6
3
+ metadata.gz: 77ae2b0a529e29f7c7c48478db4b9b0d81955b28dca48cd17c4f5dfb1dbfd118
4
+ data.tar.gz: 2ec4f56b9893527d6458e366835878902d3ec254893de2fa0e7579d52be73438
5
5
  SHA512:
6
- metadata.gz: 7a020d02b2a1da25dd37f6ff92836348985fb7bcd7b8c17392bacb4c23d0ee443b2dd294f8e30d5df5724e11049462d428efb6d2b5a3313b479ab7a3e7e71388
7
- data.tar.gz: 3b9724342594f22aabe00d1b13d7aa71bfe652ca21954c6b4789556e95743fd03d1c62214a164be8cad717405766a5d0acdbb4c3fd73a0f95aa14738cfb17d81
6
+ metadata.gz: af73f9a2031c776167a616fe2e63775cd036c6f02f24cdb65059a4ffb81572e2747a9a4f662b96bd7574ecac3d48114f7ea3569bfbc1f6f3bc0f571a523df1ca
7
+ data.tar.gz: e919e6bce582848096c692a76e63081b038fb77a9796e914f2265c1825d36017278d46b014b639bbc100935be3ae82af1ac1bfde29951e49e2643ac942d6c251
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.3 — 2026-04-13
4
+
5
+ ### Changed
6
+
7
+ - Engine no longer reaches into `routing.recv_queue` directly.
8
+ Routing strategies now expose `#dequeue_recv` and `#unblock_recv`
9
+ as the engine-facing recv contract. `FairRecv` provides the
10
+ shared implementation for fair-queued sockets; sub/xsub/xpub
11
+ delegate inline; write-only push/pub raise on dequeue and no-op
12
+ on unblock. Sharpens the routing interface and keeps Engine out
13
+ of queue internals.
14
+ - `Writable#freeze_message` collapsed: single `all?` predicate
15
+ check drives three outcomes (already-frozen-array fast path,
16
+ freeze-in-place, convert-via-map/map!) instead of mirrored
17
+ fast/slow branches that each repeated the predicate.
18
+ - Hot-path optimized. Avoid the overhead of `parts.sum(&:bytesize)`
19
+ and use `parts.sum { |p| p.bytesize }` instead.
20
+
3
21
  ## 0.19.2 — 2026-04-13
4
22
 
5
23
  ### Added
@@ -91,18 +91,22 @@ module OMQ
91
91
  loop do
92
92
  count = 0
93
93
  bytes = 0
94
+
94
95
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
95
96
  msg = conn.receive_message
96
97
  msg = transform.call(msg).freeze
98
+
97
99
  # Emit the verbose trace BEFORE enqueueing so the monitor
98
100
  # fiber is woken before the application fiber -- the
99
101
  # async scheduler is FIFO on the ready list, so this
100
102
  # preserves log-before-stdout ordering for -vvv traces.
101
103
  engine.emit_verbose_msg_received(conn, msg)
102
104
  recv_queue.enqueue(msg)
105
+
103
106
  count += 1
104
- bytes += msg.sum(&:bytesize) if count_bytes
107
+ bytes += msg.sum { |part| part.bytesize } if count_bytes
105
108
  end
109
+
106
110
  task.yield
107
111
  end
108
112
  rescue Async::Stop, Async::Cancel
@@ -133,7 +137,7 @@ module OMQ
133
137
  engine.emit_verbose_msg_received(conn, msg)
134
138
  recv_queue.enqueue(msg)
135
139
  count += 1
136
- bytes += msg.sum(&:bytesize) if count_bytes
140
+ bytes += msg.sum { |part| part.bytesize } if count_bytes
137
141
  end
138
142
  task.yield
139
143
  end
data/lib/omq/engine.rb CHANGED
@@ -80,6 +80,7 @@ module OMQ
80
80
  #
81
81
  attr_reader :connections, :tasks, :lifecycle
82
82
 
83
+
83
84
  # @!attribute [w] monitor_queue
84
85
  # @param value [Async::Queue, nil] queue for monitor events
85
86
  #
@@ -116,7 +117,9 @@ module OMQ
116
117
  def spawn_inproc_retry(endpoint)
117
118
  ri = @options.reconnect_interval
118
119
  ivl = ri.is_a?(Range) ? ri.begin : ri
119
- @tasks << @lifecycle.barrier.async(transient: true, annotation: "inproc reconnect #{endpoint}") do
120
+ ann = "inproc reconnect #{endpoint}"
121
+
122
+ @tasks << @lifecycle.barrier.async(transient: true, annotation: ann) do
120
123
  yield ivl
121
124
  rescue Async::Stop, Async::Cancel
122
125
  end
@@ -134,7 +137,9 @@ module OMQ
134
137
  capture_parent_task(parent: parent)
135
138
  transport = transport_for(endpoint)
136
139
  listener = transport.bind(endpoint, self)
140
+
137
141
  start_accept_loops(listener)
142
+
138
143
  @listeners << listener
139
144
  @last_endpoint = listener.endpoint
140
145
  @last_tcp_port = listener.respond_to?(:port) ? listener.port : nil
@@ -155,6 +160,7 @@ module OMQ
155
160
  capture_parent_task(parent: parent)
156
161
  validate_endpoint!(endpoint)
157
162
  @dialed.add(endpoint)
163
+
158
164
  if endpoint.start_with?("inproc://")
159
165
  # Inproc connect is synchronous and instant
160
166
  transport = transport_for(endpoint)
@@ -188,6 +194,7 @@ module OMQ
188
194
  def unbind(endpoint)
189
195
  listener = @listeners.find { |l| l.endpoint == endpoint }
190
196
  return unless listener
197
+
191
198
  listener.stop
192
199
  @listeners.delete(listener)
193
200
  close_connections_at(endpoint)
@@ -235,8 +242,10 @@ module OMQ
235
242
  #
236
243
  def dequeue_recv
237
244
  raise @fatal_error if @fatal_error
238
- msg = routing.recv_queue.dequeue
245
+
246
+ msg = routing.dequeue_recv
239
247
  raise @fatal_error if msg.nil? && @fatal_error
248
+
240
249
  msg
241
250
  end
242
251
 
@@ -245,7 +254,7 @@ module OMQ
245
254
  # pending {#dequeue_recv} with a nil return value.
246
255
  #
247
256
  def dequeue_recv_sentinel
248
- routing.recv_queue.push(nil)
257
+ routing.unblock_recv
249
258
  end
250
259
 
251
260
 
@@ -274,6 +283,7 @@ module OMQ
274
283
  # pumps when the connection is lost.
275
284
  parent = @connections[conn]&.barrier || @lifecycle.barrier
276
285
  task = RecvPump.start(parent, conn, recv_queue, self, transform)
286
+
277
287
  @tasks << task if task
278
288
  task
279
289
  end
@@ -316,11 +326,20 @@ module OMQ
316
326
  #
317
327
  def close
318
328
  return unless @lifecycle.open?
329
+
319
330
  @lifecycle.start_closing!
320
331
  stop_listeners unless @connections.empty?
321
- drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
332
+
333
+ if @options.linger.nil? || @options.linger > 0
334
+ drain_send_queues(@options.linger)
335
+ end
336
+
322
337
  @lifecycle.finish_closing!
323
- Reactor.untrack_linger(@options.linger) if @lifecycle.on_io_thread
338
+
339
+ if @lifecycle.on_io_thread
340
+ Reactor.untrack_linger(@options.linger)
341
+ end
342
+
324
343
  stop_listeners
325
344
  tear_down_barrier
326
345
  routing.stop rescue nil
@@ -337,9 +356,14 @@ module OMQ
337
356
  #
338
357
  def stop
339
358
  return unless @lifecycle.alive?
359
+
340
360
  @lifecycle.start_closing! if @lifecycle.open?
341
361
  @lifecycle.finish_closing!
342
- Reactor.untrack_linger(@options.linger) if @lifecycle.on_io_thread
362
+
363
+ if @lifecycle.on_io_thread
364
+ Reactor.untrack_linger(@options.linger)
365
+ end
366
+
343
367
  stop_listeners
344
368
  tear_down_barrier
345
369
  routing.stop rescue nil
@@ -407,8 +431,9 @@ module OMQ
407
431
  #
408
432
  def signal_fatal_error(error)
409
433
  return unless @lifecycle.open?
434
+
410
435
  @fatal_error = build_fatal_error(error)
411
- routing.recv_queue.push(nil) rescue nil
436
+ routing.unblock_recv rescue nil
412
437
  @lifecycle.peer_connected.resolve(nil) rescue nil
413
438
  end
414
439
 
@@ -442,7 +467,10 @@ module OMQ
442
467
  # @param parent [#async, nil] optional Async parent
443
468
  #
444
469
  def capture_parent_task(parent: nil)
445
- return unless @lifecycle.capture_parent_task(parent: parent, linger: @options.linger)
470
+ task = @lifecycle.capture_parent_task(parent: parent, linger: @options.linger)
471
+
472
+ return unless task
473
+
446
474
  Maintenance.start(@lifecycle.barrier, @options.mechanism, @tasks)
447
475
  end
448
476
 
@@ -491,8 +519,13 @@ module OMQ
491
519
  # +last_wire_size_in+.
492
520
  def emit_verbose_msg_received(conn, parts)
493
521
  return unless @verbose_monitor
522
+
494
523
  detail = { parts: parts }
495
- detail[:wire_size] = conn.last_wire_size_in if conn.respond_to?(:last_wire_size_in)
524
+
525
+ if conn.respond_to?(:last_wire_size_in)
526
+ detail[:wire_size] = conn.last_wire_size_in
527
+ end
528
+
496
529
  emit_monitor_event(:message_received, detail: detail)
497
530
  end
498
531
 
@@ -504,9 +537,14 @@ module OMQ
504
537
  # @raise [ArgumentError] if the scheme is not registered
505
538
  #
506
539
  def transport_for(endpoint)
507
- scheme = endpoint[/\A([^:]+):\/\//, 1]
508
- self.class.transports[scheme] or
540
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
541
+ transport = self.class.transports[scheme]
542
+
543
+ unless transport
509
544
  raise ArgumentError, "unsupported transport: #{endpoint}"
545
+ end
546
+
547
+ transport
510
548
  end
511
549
 
512
550
 
@@ -528,6 +566,7 @@ module OMQ
528
566
  ensure
529
567
  lifecycle&.close!
530
568
  end
569
+
531
570
  @tasks << task if task
532
571
  end
533
572
 
@@ -539,7 +578,11 @@ module OMQ
539
578
  # every routing strategy, so it is flagged rather than fixed here.
540
579
  def drain_send_queues(timeout)
541
580
  return unless @routing.respond_to?(:send_queues_drained?)
542
- deadline = timeout ? Async::Clock.now + timeout : nil
581
+
582
+ if timeout
583
+ deadline = Async::Clock.now + timeout
584
+ end
585
+
543
586
  until @routing.send_queues_drained?
544
587
  break if deadline && (deadline - Async::Clock.now) <= 0
545
588
  sleep 0.001
@@ -554,12 +597,16 @@ module OMQ
554
597
 
555
598
  def validate_endpoint!(endpoint)
556
599
  transport = transport_for(endpoint)
557
- transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
600
+
601
+ if transport.respond_to?(:validate_endpoint!)
602
+ transport.validate_endpoint!(endpoint)
603
+ end
558
604
  end
559
605
 
560
606
 
561
607
  def start_accept_loops(listener)
562
608
  return unless listener.respond_to?(:start_accept_loops)
609
+
563
610
  listener.start_accept_loops(@lifecycle.barrier) do |io|
564
611
  handle_accepted(io, endpoint: listener.endpoint)
565
612
  end
@@ -37,6 +37,7 @@ module OMQ
37
37
  tasks << task
38
38
  task
39
39
  end
40
+
40
41
  end
41
42
  end
42
43
  end
@@ -11,6 +11,11 @@ module OMQ
11
11
  include FairRecv
12
12
 
13
13
 
14
+ # @return [FairQueue]
15
+ #
16
+ attr_reader :recv_queue
17
+
18
+
14
19
  # @param engine [Engine]
15
20
  #
16
21
  def initialize(engine)
@@ -21,11 +26,6 @@ module OMQ
21
26
  end
22
27
 
23
28
 
24
- # @return [FairQueue]
25
- #
26
- attr_reader :recv_queue
27
-
28
-
29
29
  # @param connection [Connection]
30
30
  #
31
31
  def connection_added(connection)
@@ -58,6 +58,7 @@ module OMQ
58
58
  @tasks.each(&:stop)
59
59
  @tasks.clear
60
60
  end
61
+
61
62
  end
62
63
  end
63
64
  end
@@ -74,7 +74,9 @@ module OMQ
74
74
  return try_dequeue if timeout == 0
75
75
 
76
76
  loop do
77
- return nil if @closed && @drain.empty? && @queues.all?(&:empty?)
77
+ if @closed && @drain.empty? && @queues.all? { |q| q.empty? }
78
+ return nil
79
+ end
78
80
 
79
81
  msg = try_dequeue
80
82
  return msg if msg
@@ -101,7 +103,7 @@ module OMQ
101
103
  # @return [Boolean]
102
104
  #
103
105
  def empty?
104
- @drain.empty? && @queues.all?(&:empty?)
106
+ @drain.empty? && @queues.all? { |q| q.empty? }
105
107
  end
106
108
 
107
109
 
@@ -132,6 +134,7 @@ module OMQ
132
134
  msg = q.dequeue(timeout: 0)
133
135
  return msg if msg
134
136
  end
137
+
135
138
  nil
136
139
  end
137
140
  end
@@ -7,6 +7,26 @@ module OMQ
7
7
  # Including classes must have @engine, @recv_queue (FairQueue), and @tasks.
8
8
  #
9
9
  module FairRecv
10
+ # Dequeues the next received message. Blocks until one is available.
11
+ # Engine-facing contract — Engine must not touch @recv_queue directly.
12
+ #
13
+ # @return [Array<String>, nil]
14
+ #
15
+ def dequeue_recv
16
+ @recv_queue.dequeue
17
+ end
18
+
19
+
20
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel. Called by
21
+ # Engine on close (close_read) or fatal-error propagation.
22
+ #
23
+ # @return [void]
24
+ #
25
+ def unblock_recv
26
+ @recv_queue.push(nil)
27
+ end
28
+
29
+
10
30
  private
11
31
 
12
32
  # Creates a per-connection recv queue, registers it with @recv_queue,
@@ -18,10 +38,16 @@ module OMQ
18
38
  def add_fair_recv_connection(conn, &transform)
19
39
  conn_q = Routing.build_queue(@engine.options.recv_hwm, :block)
20
40
  signaling = SignalingQueue.new(conn_q, @recv_queue)
41
+
21
42
  @recv_queue.add_queue(conn, conn_q)
22
- task = @engine.start_recv_pump(conn, signaling, &transform)
23
- @tasks << task if task
43
+
44
+ task = @engine.start_recv_pump(conn, signaling, &transform)
45
+
46
+ if task
47
+ @tasks << task
48
+ end
24
49
  end
50
+
25
51
  end
26
52
  end
27
53
  end
@@ -134,7 +134,9 @@ module OMQ
134
134
  loop do
135
135
  frame = conn.read_frame
136
136
  next unless frame.command?
137
+
137
138
  cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
139
+
138
140
  case cmd.name
139
141
  when "SUBSCRIBE"
140
142
  on_subscribe(conn, cmd.data)
@@ -157,7 +159,13 @@ module OMQ
157
159
  #
158
160
  def start_conn_send_pump(conn, q)
159
161
  use_wire = conn.respond_to?(:write_wire) && !conn.encrypted?
160
- task = @conflate ? start_conn_send_pump_conflate(conn, q) : start_conn_send_pump_normal(conn, q, use_wire)
162
+
163
+ if @conflate
164
+ task = start_conn_send_pump_conflate(conn, q)
165
+ else
166
+ task = start_conn_send_pump_normal(conn, q, use_wire)
167
+ end
168
+
161
169
  @conn_send_tasks[conn] = task
162
170
  @tasks << task
163
171
  end
@@ -177,6 +185,7 @@ module OMQ
177
185
  loop do
178
186
  batch = [q.dequeue]
179
187
  Routing.drain_send_queue(q, batch)
188
+
180
189
  if write_matching_batch(conn, batch, use_wire)
181
190
  conn.flush
182
191
  batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
@@ -193,15 +202,19 @@ module OMQ
193
202
  #
194
203
  def write_matching_batch(conn, batch, use_wire)
195
204
  sent = false
205
+
196
206
  batch.each do |parts|
197
207
  next unless subscribed?(conn, parts.first || EMPTY_BINARY)
208
+
198
209
  if use_wire
199
210
  conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts))
200
211
  else
201
212
  conn.write_message(parts)
202
213
  end
214
+
203
215
  sent = true
204
216
  end
217
+
205
218
  sent
206
219
  end
207
220
 
@@ -12,6 +12,12 @@ module OMQ
12
12
  class Pair
13
13
  include FairRecv
14
14
 
15
+
16
+ # @return [FairQueue]
17
+ #
18
+ attr_reader :recv_queue
19
+
20
+
15
21
  # @param engine [Engine]
16
22
  #
17
23
  def initialize(engine)
@@ -24,10 +30,6 @@ module OMQ
24
30
  end
25
31
 
26
32
 
27
- # @return [FairQueue]
28
- #
29
- attr_reader :recv_queue
30
-
31
33
  # @param connection [Connection]
32
34
  # @raise [RuntimeError] if a connection already exists
33
35
  #
@@ -83,24 +85,32 @@ module OMQ
83
85
  @send_queue.empty?
84
86
  end
85
87
 
88
+
86
89
  private
87
90
 
91
+
88
92
  def start_send_pump(conn)
89
93
  @send_pump = @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
90
94
  loop do
91
95
  batch = [@send_queue.dequeue]
92
96
  Routing.drain_send_queue(@send_queue, batch)
97
+
93
98
  if batch.size == 1
94
99
  conn.write_message(batch[0])
95
100
  else
96
101
  conn.write_messages(batch)
97
102
  end
103
+
98
104
  conn.flush
99
- batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
105
+ batch.each do |parts|
106
+ @engine.emit_verbose_msg_sent(conn, parts)
107
+ end
100
108
  end
101
109
  end
110
+
102
111
  @tasks << @send_pump
103
112
  end
113
+
104
114
  end
105
115
  end
106
116
  end
@@ -20,13 +20,23 @@ module OMQ
20
20
  end
21
21
 
22
22
 
23
- # PUB is write-only.
23
+ # PUB is write-only. Engine-facing recv contract: dequeue raises,
24
+ # unblock is a no-op (fatal-error propagation still calls it).
24
25
  #
25
26
  def recv_queue
26
27
  raise "PUB sockets cannot receive"
27
28
  end
28
29
 
29
30
 
31
+ def dequeue_recv
32
+ raise "PUB sockets cannot receive"
33
+ end
34
+
35
+
36
+ def unblock_recv
37
+ end
38
+
39
+
30
40
  # @param connection [Connection]
31
41
  #
32
42
  def connection_added(connection)
@@ -61,6 +71,7 @@ module OMQ
61
71
  @tasks.each(&:stop)
62
72
  @tasks.clear
63
73
  end
74
+
64
75
  end
65
76
  end
66
77
  end
@@ -6,6 +6,8 @@ module OMQ
6
6
  #
7
7
  class Pull
8
8
  include FairRecv
9
+
10
+
9
11
  # @param engine [Engine]
10
12
  #
11
13
  def initialize(engine)
@@ -19,6 +21,7 @@ module OMQ
19
21
  #
20
22
  attr_reader :recv_queue
21
23
 
24
+
22
25
  # @param connection [Connection]
23
26
  #
24
27
  def connection_added(connection)
@@ -49,6 +52,7 @@ module OMQ
49
52
  @tasks.each(&:stop)
50
53
  @tasks.clear
51
54
  end
55
+
52
56
  end
53
57
  end
54
58
  end
@@ -7,6 +7,7 @@ module OMQ
7
7
  class Push
8
8
  include RoundRobin
9
9
 
10
+
10
11
  # @param engine [Engine]
11
12
  #
12
13
  def initialize(engine)
@@ -16,13 +17,23 @@ module OMQ
16
17
  end
17
18
 
18
19
 
19
- # PUSH is write-only.
20
+ # PUSH is write-only. Engine-facing recv contract: dequeue raises,
21
+ # unblock is a no-op (fatal-error propagation still calls it).
20
22
  #
21
23
  def recv_queue
22
24
  raise "PUSH sockets cannot receive"
23
25
  end
24
26
 
25
27
 
28
+ def dequeue_recv
29
+ raise "PUSH sockets cannot receive"
30
+ end
31
+
32
+
33
+ def unblock_recv
34
+ end
35
+
36
+
26
37
  # @param connection [Connection]
27
38
  #
28
39
  def connection_added(connection)
@@ -53,6 +64,7 @@ module OMQ
53
64
  @tasks.clear
54
65
  end
55
66
 
67
+
56
68
  private
57
69
 
58
70
 
@@ -66,6 +78,7 @@ module OMQ
66
78
  conn.receive_message # blocks until peer disconnects; then exits
67
79
  end
68
80
  end
81
+
69
82
  end
70
83
  end
71
84
  end
@@ -14,6 +14,11 @@ module OMQ
14
14
  EMPTY_FRAME = "".b.freeze
15
15
 
16
16
 
17
+ # @return [FairQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
17
22
  # @param engine [Engine]
18
23
  #
19
24
  def initialize(engine)
@@ -26,17 +31,14 @@ module OMQ
26
31
  end
27
32
 
28
33
 
29
- # @return [FairQueue]
30
- #
31
- attr_reader :recv_queue
32
-
33
34
  # @param connection [Connection]
34
35
  #
35
36
  def connection_added(connection)
36
37
  add_fair_recv_connection(connection) do |msg|
37
- delimiter = msg.index(&:empty?) || msg.size
38
+ delimiter = msg.index { |p| p.empty? } || msg.size
38
39
  envelope = msg[0, delimiter]
39
40
  body = msg[(delimiter + 1)..] || []
41
+
40
42
  @pending_replies << { conn: connection, envelope: envelope }
41
43
  body
42
44
  end
@@ -14,6 +14,11 @@ module OMQ
14
14
  EMPTY_BINARY = ::Protocol::ZMTP::Codec::EMPTY_BINARY
15
15
 
16
16
 
17
+ # @return [FairQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
17
22
  # @param engine [Engine]
18
23
  #
19
24
  def initialize(engine)
@@ -25,11 +30,6 @@ module OMQ
25
30
  end
26
31
 
27
32
 
28
- # @return [FairQueue]
29
- #
30
- attr_reader :recv_queue
31
-
32
-
33
33
  # @param connection [Connection]
34
34
  #
35
35
  def connection_added(connection)
@@ -19,6 +19,9 @@ module OMQ
19
19
  # their #initialize.
20
20
  #
21
21
  module RoundRobin
22
+ BATCH_MSG_CAP = 256
23
+ BATCH_BYTE_CAP = 512 * 1024
24
+
22
25
  # @return [Boolean] true when the shared send queue is empty
23
26
  # and no pump fiber is mid-write with a dequeued batch.
24
27
  #
@@ -26,8 +29,10 @@ module OMQ
26
29
  @send_queue.empty? && @in_flight == 0
27
30
  end
28
31
 
32
+
29
33
  private
30
34
 
35
+
31
36
  # Initializes the shared send queue for the including class.
32
37
  #
33
38
  # @param engine [Engine]
@@ -83,6 +88,7 @@ module OMQ
83
88
  #
84
89
  def enqueue_round_robin(parts)
85
90
  pipe = @direct_pipe
91
+
86
92
  if pipe&.direct_recv_queue
87
93
  pipe.send_message(transform_send(parts))
88
94
  else
@@ -97,7 +103,9 @@ module OMQ
97
103
  # @param parts [Array<String>]
98
104
  # @return [Array<String>]
99
105
  #
100
- def transform_send(parts) = parts
106
+ def transform_send(parts)
107
+ parts
108
+ end
101
109
 
102
110
 
103
111
  # Spawns a send pump for one connection. Drains the shared send
@@ -129,23 +137,26 @@ module OMQ
129
137
  batch = [@send_queue.dequeue]
130
138
  drain_send_queue_capped(batch)
131
139
  @in_flight += batch.size
140
+
132
141
  begin
133
142
  write_batch(conn, batch)
134
143
  ensure
135
144
  @in_flight -= batch.size
136
145
  end
137
- batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
146
+
147
+ batch.each do |parts|
148
+ @engine.emit_verbose_msg_sent(conn, parts)
149
+ end
150
+
138
151
  Async::Task.current.yield
139
152
  end
140
153
  end
154
+
141
155
  @conn_send_tasks[conn] = task
142
156
  @tasks << task
143
157
  end
144
158
 
145
159
 
146
- BATCH_MSG_CAP = 256
147
- BATCH_BYTE_CAP = 512 * 1024
148
-
149
160
  def drain_send_queue_capped(batch)
150
161
  bytes = batch_bytes(batch[0])
151
162
  while batch.size < BATCH_MSG_CAP && bytes < BATCH_BYTE_CAP
@@ -175,6 +186,7 @@ module OMQ
175
186
  conn.flush
176
187
  end
177
188
  end
189
+
178
190
  end
179
191
  end
180
192
  end
@@ -12,6 +12,13 @@ module OMQ
12
12
  #
13
13
  class Router
14
14
  include FairRecv
15
+
16
+
17
+ # @return [FairQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
15
22
  # @param engine [Engine]
16
23
  #
17
24
  def initialize(engine)
@@ -25,10 +32,6 @@ module OMQ
25
32
  end
26
33
 
27
34
 
28
- # @return [FairQueue]
29
- #
30
- attr_reader :recv_queue
31
-
32
35
  # @param connection [Connection]
33
36
  #
34
37
  def connection_added(connection)
@@ -8,6 +8,11 @@ module OMQ
8
8
  #
9
9
  class Sub
10
10
 
11
+ # @return [FairQueue]
12
+ #
13
+ attr_reader :recv_queue
14
+
15
+
11
16
  # @param engine [Engine]
12
17
  #
13
18
  def initialize(engine)
@@ -19,9 +24,17 @@ module OMQ
19
24
  end
20
25
 
21
26
 
22
- # @return [FairQueue]
27
+ # Engine-facing recv contract. Delegates to the FairQueue.
23
28
  #
24
- attr_reader :recv_queue
29
+ def dequeue_recv
30
+ @recv_queue.dequeue
31
+ end
32
+
33
+
34
+ def unblock_recv
35
+ @recv_queue.push(nil)
36
+ end
37
+
25
38
 
26
39
  # @param connection [Connection]
27
40
  #
@@ -85,6 +98,7 @@ module OMQ
85
98
  @tasks.each(&:stop)
86
99
  @tasks.clear
87
100
  end
101
+
88
102
  end
89
103
  end
90
104
  end
@@ -14,6 +14,11 @@ module OMQ
14
14
  class XPub
15
15
  include FanOut
16
16
 
17
+ # @return [Async::LimitedQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+
17
22
  # @param engine [Engine]
18
23
  #
19
24
  def initialize(engine)
@@ -24,9 +29,17 @@ module OMQ
24
29
  end
25
30
 
26
31
 
27
- # @return [Async::LimitedQueue]
32
+ # Engine-facing recv contract. Delegates to the bounded queue.
28
33
  #
29
- attr_reader :recv_queue
34
+ def dequeue_recv
35
+ @recv_queue.dequeue
36
+ end
37
+
38
+
39
+ def unblock_recv
40
+ @recv_queue.push(nil)
41
+ end
42
+
30
43
 
31
44
  # @param connection [Connection]
32
45
  #
@@ -63,8 +76,10 @@ module OMQ
63
76
  @tasks.clear
64
77
  end
65
78
 
79
+
66
80
  private
67
81
 
82
+
68
83
  # Expose subscription to application as data message.
69
84
  #
70
85
  def on_subscribe(conn, prefix)
@@ -79,6 +94,7 @@ module OMQ
79
94
  super
80
95
  @recv_queue.enqueue(["\x00#{prefix}".b])
81
96
  end
97
+
82
98
  end
83
99
  end
84
100
  end
@@ -10,6 +10,11 @@ module OMQ
10
10
  #
11
11
  class XSub
12
12
 
13
+ # @return [FairQueue]
14
+ #
15
+ attr_reader :recv_queue
16
+
17
+
13
18
  # @param engine [Engine]
14
19
  #
15
20
  def initialize(engine)
@@ -22,9 +27,17 @@ module OMQ
22
27
  end
23
28
 
24
29
 
25
- # @return [FairQueue]
30
+ # Engine-facing recv contract. Delegates to the FairQueue.
26
31
  #
27
- attr_reader :recv_queue
32
+ def dequeue_recv
33
+ @recv_queue.dequeue
34
+ end
35
+
36
+
37
+ def unblock_recv
38
+ @recv_queue.push(nil)
39
+ end
40
+
28
41
 
29
42
  # @param connection [Connection]
30
43
  #
@@ -58,7 +71,9 @@ module OMQ
58
71
  # @param parts [Array<String>]
59
72
  #
60
73
  def enqueue(parts)
61
- @connections.each { |conn| @conn_queues[conn]&.enqueue(parts) }
74
+ @connections.each do |conn|
75
+ @conn_queues[conn]&.enqueue(parts)
76
+ end
62
77
  end
63
78
 
64
79
 
@@ -78,16 +93,21 @@ module OMQ
78
93
  @conn_queues.values.all?(&:empty?)
79
94
  end
80
95
 
96
+
81
97
  private
82
98
 
99
+
83
100
  def start_conn_send_pump(conn, q)
84
101
  task = @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
85
102
  loop do
86
103
  parts = q.dequeue
87
104
  frame = parts.first&.b
105
+
88
106
  next if frame.nil? || frame.empty?
107
+
89
108
  flag = frame.getbyte(0)
90
109
  prefix = frame.byteslice(1..) || "".b
110
+
91
111
  case flag
92
112
  when 0x01
93
113
  conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
@@ -96,9 +116,11 @@ module OMQ
96
116
  end
97
117
  end
98
118
  end
119
+
99
120
  @conn_send_tasks[conn] = task
100
121
  @tasks << task
101
122
  end
123
+
102
124
  end
103
125
  end
104
126
  end
data/lib/omq/routing.rb CHANGED
@@ -110,5 +110,6 @@ module OMQ
110
110
  @registry[socket_type] or raise ArgumentError, "unknown socket type: #{socket_type.inspect}"
111
111
  end
112
112
  end
113
+
113
114
  end
114
115
  end
@@ -19,5 +19,6 @@ module OMQ
19
19
  end
20
20
  super
21
21
  end
22
+
22
23
  end
23
24
  end
data/lib/omq/socket.rb CHANGED
@@ -6,6 +6,30 @@ module OMQ
6
6
  # Socket base class.
7
7
  #
8
8
  class Socket
9
+ extend Forwardable
10
+
11
+ # Creates a new socket and binds it to the given endpoint.
12
+ #
13
+ # @param endpoint [String]
14
+ # @param opts [Hash] keyword arguments forwarded to {#initialize}
15
+ # @return [Socket]
16
+ #
17
+ def self.bind(endpoint, **opts)
18
+ new("@#{endpoint}", **opts)
19
+ end
20
+
21
+
22
+ # Creates a new socket and connects it to the given endpoint.
23
+ #
24
+ # @param endpoint [String]
25
+ # @param opts [Hash] keyword arguments forwarded to {#initialize}
26
+ # @return [Socket]
27
+ #
28
+ def self.connect(endpoint, **opts)
29
+ new(">#{endpoint}", **opts)
30
+ end
31
+
32
+
9
33
  # @return [Options]
10
34
  #
11
35
  attr_reader :options
@@ -25,8 +49,6 @@ module OMQ
25
49
 
26
50
  # Delegate socket option accessors to @options.
27
51
  #
28
- extend Forwardable
29
-
30
52
  def_delegators :@options,
31
53
  :send_hwm, :send_hwm=,
32
54
  :recv_hwm, :recv_hwm=,
@@ -49,28 +71,6 @@ module OMQ
49
71
  :mechanism, :mechanism=
50
72
 
51
73
 
52
- # Creates a new socket and binds it to the given endpoint.
53
- #
54
- # @param endpoint [String]
55
- # @param opts [Hash] keyword arguments forwarded to {#initialize}
56
- # @return [Socket]
57
- #
58
- def self.bind(endpoint, **opts)
59
- new("@#{endpoint}", **opts)
60
- end
61
-
62
-
63
- # Creates a new socket and connects it to the given endpoint.
64
- #
65
- # @param endpoint [String]
66
- # @param opts [Hash] keyword arguments forwarded to {#initialize}
67
- # @return [Socket]
68
- #
69
- def self.connect(endpoint, **opts)
70
- new(">#{endpoint}", **opts)
71
- end
72
-
73
-
74
74
  # @param endpoints [String, nil] optional endpoint with prefix convention
75
75
  # (+@+ for bind, +>+ for connect, plain uses subclass default)
76
76
  # @param linger [Integer] linger period in seconds (default 0)
@@ -141,19 +141,27 @@ module OMQ
141
141
 
142
142
 
143
143
  # @return [Async::Promise] resolves when first peer completes handshake
144
- def peer_connected = @engine.peer_connected
144
+ def peer_connected
145
+ @engine.peer_connected
146
+ end
145
147
 
146
148
 
147
149
  # @return [Async::Promise] resolves when first subscriber joins (PUB/XPUB only)
148
- def subscriber_joined = @engine.routing.subscriber_joined
150
+ def subscriber_joined
151
+ @engine.routing.subscriber_joined
152
+ end
149
153
 
150
154
 
151
155
  # @return [Async::Promise] resolves when all peers disconnect (after having had peers)
152
- def all_peers_gone = @engine.all_peers_gone
156
+ def all_peers_gone
157
+ @engine.all_peers_gone
158
+ end
153
159
 
154
160
 
155
161
  # @return [Integer] current number of peer connections
156
- def connection_count = @engine.connections.size
162
+ def connection_count
163
+ @engine.connections.size
164
+ end
157
165
 
158
166
 
159
167
  # Signals end-of-stream on the receive side. A subsequent
@@ -270,6 +278,7 @@ module OMQ
270
278
  #
271
279
  def attach_endpoints(endpoints, default:)
272
280
  return unless endpoints
281
+
273
282
  case endpoints
274
283
  when /\A@(.+)\z/
275
284
  bind($1)
@@ -318,5 +327,6 @@ module OMQ
318
327
  def ensure_parent_task(parent: nil)
319
328
  @engine.capture_parent_task(parent: parent)
320
329
  end
330
+
321
331
  end
322
332
  end
@@ -110,14 +110,18 @@ module OMQ
110
110
 
111
111
  # @return [Boolean] always false; inproc pipes are never encrypted
112
112
  #
113
- def encrypted? = false
113
+ def encrypted?
114
+ false
115
+ end
114
116
 
115
117
 
116
118
  # No-op — inproc has no IO buffer to flush.
117
119
  #
118
120
  # @return [nil]
119
121
  #
120
- def flush = nil
122
+ def flush
123
+ nil
124
+ end
121
125
 
122
126
 
123
127
  # Receives a multi-frame message.
@@ -210,6 +210,7 @@ module OMQ
210
210
  @tasks.each(&:stop)
211
211
  @servers.each { |s| s.close rescue nil }
212
212
  end
213
+
213
214
  end
214
215
  end
215
216
  end
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.19.2"
4
+ VERSION = "0.19.3"
5
5
  end
data/lib/omq/writable.rb CHANGED
@@ -7,6 +7,11 @@ module OMQ
7
7
  #
8
8
  module Writable
9
9
  include QueueWritable
10
+
11
+
12
+ EMPTY_PART = "".b.freeze
13
+
14
+
10
15
  # Sends a message.
11
16
  #
12
17
  # @param message [String, Array<String>] message parts
@@ -33,8 +38,20 @@ module OMQ
33
38
  send(message)
34
39
  end
35
40
 
41
+
42
+ # Waits until the socket is writable.
43
+ #
44
+ # @param timeout [Numeric, nil] timeout in seconds
45
+ # @return [true]
46
+ #
47
+ def wait_writable(timeout = @options.write_timeout)
48
+ true
49
+ end
50
+
51
+
36
52
  private
37
53
 
54
+
38
55
  # Converts a message into a frozen array of frozen binary strings.
39
56
  #
40
57
  # @param message [String, Array<String>]
@@ -44,21 +61,23 @@ module OMQ
44
61
  parts = message.is_a?(Array) ? message : [message]
45
62
  raise ArgumentError, "message has no parts" if parts.empty?
46
63
 
47
- # Fast path: skip map when all parts are already frozen binary.
64
+ all_ready = parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
65
+
66
+ # Already a frozen array of frozen binary strings → return as-is.
67
+ return parts if all_ready && parts.frozen?
68
+
69
+ # Items are ready; just freeze the outer array.
70
+ return parts.freeze if all_ready
71
+
72
+ # Items need conversion. Mutate in place when we can.
48
73
  if parts.frozen?
49
- return parts if parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
50
- parts = parts.map { |p| frozen_binary(p) }
74
+ parts.map { |p| frozen_binary(p) }.freeze
51
75
  else
52
- unless parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
53
- parts.map! { |p| frozen_binary(p) }
54
- end
76
+ parts.map! { |p| frozen_binary(p) }.freeze
55
77
  end
56
- parts.freeze
57
78
  end
58
79
 
59
80
 
60
- EMPTY_PART = "".b.freeze
61
-
62
81
  def frozen_binary(obj)
63
82
  return EMPTY_PART if obj.nil?
64
83
  s = obj.to_s
@@ -66,15 +85,5 @@ module OMQ
66
85
  s.b.freeze
67
86
  end
68
87
 
69
- public
70
-
71
- # Waits until the socket is writable.
72
- #
73
- # @param timeout [Numeric, nil] timeout in seconds
74
- # @return [true]
75
- #
76
- def wait_writable(timeout = @options.write_timeout)
77
- true
78
- end
79
88
  end
80
89
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.2
4
+ version: 0.19.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger