omq 0.19.0 → 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: f01c18a7a887e0f2811abff2251f1b2c9b201091e800248fb852b92bedbcaf06
4
- data.tar.gz: d32b3e07223d906a3bb25fc8542409bb5c05af36b509f7fe70c91419d0836cf9
3
+ metadata.gz: 77ae2b0a529e29f7c7c48478db4b9b0d81955b28dca48cd17c4f5dfb1dbfd118
4
+ data.tar.gz: 2ec4f56b9893527d6458e366835878902d3ec254893de2fa0e7579d52be73438
5
5
  SHA512:
6
- metadata.gz: c0b26ad39e26fe586cec53371bc4b1beeaf357b51f32e90ad8d889a8413f62c08c13be2d8f787e3528d4fca6bccc1ceae21454474465aff25f9418c37460d957
7
- data.tar.gz: b8d3acedee187c9c757268af173b55bd28daf502f8e054f92ef670c59a1781e2b2f73267425c239d27acc7cb036b0030ab562a12918c7db205dc34f4bcf8e38d
6
+ metadata.gz: af73f9a2031c776167a616fe2e63775cd036c6f02f24cdb65059a4ffb81572e2747a9a4f662b96bd7574ecac3d48114f7ea3569bfbc1f6f3bc0f571a523df1ca
7
+ data.tar.gz: e919e6bce582848096c692a76e63081b038fb77a9796e914f2265c1825d36017278d46b014b639bbc100935be3ae82af1ac1bfde29951e49e2643ac942d6c251
data/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
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
+
21
+ ## 0.19.2 — 2026-04-13
22
+
23
+ ### Added
24
+
25
+ - **`:disconnected` monitor events carry the underlying error.** When
26
+ a connection drops due to a `Protocol::ZMTP::Error` (oversized
27
+ frame, bad framing, zstd bytebomb, nonce exhaustion, …) or a
28
+ `CONNECTION_LOST` error, the `:disconnected` event's `detail` hash
29
+ now includes `error:` (the exception instance) and `reason:` (its
30
+ message). Peer tooling can match on `detail[:error].is_a?(...)` to
31
+ enforce its own policy — e.g. `omq-cli` terminates the command on
32
+ `Protocol::ZMTP::Error`, while the library keeps the libzmq-parity
33
+ behavior of silently dropping the offending connection and
34
+ reconnecting.
35
+ - **`OMQ::Socket#engine` public reader.** The socket's engine is now
36
+ a documented (if low-level) accessor for peer tooling that needs
37
+ to reach into internals — notably so `omq-cli`'s monitor callback
38
+ can call `sock.engine.signal_fatal_error(error)` without
39
+ `instance_variable_get`. Not part of the stable user API.
40
+
41
+ ### Fixed
42
+
43
+ - **`signal_fatal_error` preserves the underlying cause.** The
44
+ resulting `SocketDeadError` now chains back to the original error
45
+ via `Exception#cause` regardless of whether `signal_fatal_error`
46
+ is called from inside a rescue block or from a monitor callback
47
+ (where `$!` is `nil`). Uses a raise-in-rescue helper to force the
48
+ cause chain. The wrapped error's message also includes the
49
+ original reason so tooling that only logs the top-level message
50
+ still shows what happened.
51
+
52
+ ## 0.19.1 — 2026-04-13
53
+
54
+ ### Fixed
55
+
56
+ - **Send-queue batch accounting tolerates non-string parts.**
57
+ `Routing::RoundRobin#drain_send_queue_capped` previously called
58
+ `#bytesize` directly on each message part for the fairness cap, which
59
+ crashed when a connection wrapper enqueued structured parts for later
60
+ transformation (notably `OMQ::Ractor`'s `MarshalConnection`, which
61
+ hands off live Ruby objects and marshals them in `#write_messages`).
62
+ The fairness cap now skips parts that don't respond to `#bytesize`.
63
+
3
64
  ## 0.19.0 — 2026-04-12
4
65
 
5
66
  ### Added
@@ -133,8 +133,18 @@ module OMQ
133
133
  # routing removal, monitor event, reconnect scheduling.
134
134
  # Idempotent: a no-op if already :closed.
135
135
  #
136
- def lost!
137
- tear_down!(reconnect: true)
136
+ def lost!(reason: nil)
137
+ tear_down!(reconnect: true, reason: reason || @disconnect_reason)
138
+ end
139
+
140
+
141
+ # Records the exception that took down a pump task so that the
142
+ # supervisor can surface it in the :disconnected monitor event.
143
+ # First writer wins — subsequent pumps unwinding on the same
144
+ # teardown don't overwrite the original cause.
145
+ #
146
+ def record_disconnect_reason(error)
147
+ @disconnect_reason ||= error
138
148
  end
139
149
 
140
150
 
@@ -187,21 +197,20 @@ module OMQ
187
197
  end
188
198
  rescue Async::Stop, Async::Cancel
189
199
  # socket or supervisor cancelled externally (socket closing)
190
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
191
- # expected pump exit on disconnect
192
200
  ensure
193
201
  lost!
194
202
  end
195
203
  end
196
204
 
197
205
 
198
- def tear_down!(reconnect:)
206
+ def tear_down!(reconnect:, reason: nil)
199
207
  return if @state == :closed
200
208
  transition!(:closed)
201
209
  @engine.connections.delete(@conn)
202
210
  @engine.routing.connection_removed(@conn) if @conn
203
211
  @conn&.close rescue nil
204
- @engine.emit_monitor_event(:disconnected, endpoint: @endpoint)
212
+ detail = reason ? { error: reason, reason: reason.message } : nil
213
+ @engine.emit_monitor_event(:disconnected, endpoint: @endpoint, detail: detail)
205
214
  @done&.resolve(true)
206
215
  @engine.resolve_all_peers_gone_if_empty
207
216
  @engine.maybe_reconnect(@endpoint) if reconnect
@@ -91,25 +91,31 @@ 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
109
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
110
- # expected disconnect — supervisor will trigger teardown
113
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
114
+ # expected disconnect — stash reason for the :disconnected
115
+ # monitor event, let the lifecycle reconnect as usual
116
+ engine.connections[conn]&.record_disconnect_reason(error)
111
117
  rescue => error
112
- @engine.signal_fatal_error(error)
118
+ engine.signal_fatal_error(error)
113
119
  end
114
120
  end
115
121
 
@@ -131,15 +137,17 @@ module OMQ
131
137
  engine.emit_verbose_msg_received(conn, msg)
132
138
  recv_queue.enqueue(msg)
133
139
  count += 1
134
- bytes += msg.sum(&:bytesize) if count_bytes
140
+ bytes += msg.sum { |part| part.bytesize } if count_bytes
135
141
  end
136
142
  task.yield
137
143
  end
138
144
  rescue Async::Stop, Async::Cancel
139
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
140
- # expected disconnect — supervisor will trigger teardown
145
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
146
+ # expected disconnect — stash reason for the :disconnected
147
+ # monitor event, let the lifecycle reconnect as usual
148
+ engine.connections[conn]&.record_disconnect_reason(error)
141
149
  rescue => error
142
- @engine.signal_fatal_error(error)
150
+ engine.signal_fatal_error(error)
143
151
  end
144
152
  end
145
153
 
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
@@ -386,8 +410,12 @@ module OMQ
386
410
 
387
411
  lifecycle.barrier.async(transient: true, annotation: annotation) do
388
412
  yield
389
- rescue Async::Stop, Async::Cancel, Protocol::ZMTP::Error, *CONNECTION_LOST
390
- # normal shutdown / expected disconnect / sibling tore us down
413
+ rescue Async::Stop, Async::Cancel
414
+ # normal shutdown / sibling tore us down
415
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
416
+ # expected disconnect — stash reason for the :disconnected
417
+ # monitor event, then let the lifecycle reconnect as usual
418
+ lifecycle.record_disconnect_reason(error)
391
419
  rescue => error
392
420
  signal_fatal_error(error)
393
421
  end
@@ -395,22 +423,33 @@ module OMQ
395
423
 
396
424
 
397
425
  # Wraps an unexpected pump error as {OMQ::SocketDeadError} and
398
- # unblocks any callers waiting on the recv queue.
399
- #
400
- # Must be called from inside a rescue block so that +error+ is
401
- # +$!+ and Ruby sets it as +#cause+ on the new exception.
426
+ # unblocks any callers waiting on the recv queue. The original
427
+ # error is preserved as +#cause+ so callers can surface the real
428
+ # reason.
402
429
  #
403
430
  # @param error [Exception]
404
431
  #
405
432
  def signal_fatal_error(error)
406
433
  return unless @lifecycle.open?
407
- @fatal_error = begin
408
- raise SocketDeadError, "internal error killed #{@socket_type} socket"
409
- rescue => wrapped
434
+
435
+ @fatal_error = build_fatal_error(error)
436
+ routing.unblock_recv rescue nil
437
+ @lifecycle.peer_connected.resolve(nil) rescue nil
438
+ end
439
+
440
+
441
+ # Constructs a SocketDeadError whose +cause+ is +error+. Uses the
442
+ # raise-in-rescue idiom because Ruby only sets +cause+ on an
443
+ # exception when it is raised from inside a rescue block -- works
444
+ # regardless of the original caller's +$!+ state.
445
+ def build_fatal_error(error)
446
+ raise error
447
+ rescue
448
+ begin
449
+ raise SocketDeadError, "#{@socket_type} socket killed: #{error.message}"
450
+ rescue SocketDeadError => wrapped
410
451
  wrapped
411
452
  end
412
- routing.recv_queue.push(nil) rescue nil
413
- @lifecycle.peer_connected.resolve(nil) rescue nil
414
453
  end
415
454
 
416
455
 
@@ -428,7 +467,10 @@ module OMQ
428
467
  # @param parent [#async, nil] optional Async parent
429
468
  #
430
469
  def capture_parent_task(parent: nil)
431
- 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
+
432
474
  Maintenance.start(@lifecycle.barrier, @options.mechanism, @tasks)
433
475
  end
434
476
 
@@ -477,8 +519,13 @@ module OMQ
477
519
  # +last_wire_size_in+.
478
520
  def emit_verbose_msg_received(conn, parts)
479
521
  return unless @verbose_monitor
522
+
480
523
  detail = { parts: parts }
481
- 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
+
482
529
  emit_monitor_event(:message_received, detail: detail)
483
530
  end
484
531
 
@@ -490,9 +537,14 @@ module OMQ
490
537
  # @raise [ArgumentError] if the scheme is not registered
491
538
  #
492
539
  def transport_for(endpoint)
493
- scheme = endpoint[/\A([^:]+):\/\//, 1]
494
- self.class.transports[scheme] or
540
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
541
+ transport = self.class.transports[scheme]
542
+
543
+ unless transport
495
544
  raise ArgumentError, "unsupported transport: #{endpoint}"
545
+ end
546
+
547
+ transport
496
548
  end
497
549
 
498
550
 
@@ -514,6 +566,7 @@ module OMQ
514
566
  ensure
515
567
  lifecycle&.close!
516
568
  end
569
+
517
570
  @tasks << task if task
518
571
  end
519
572
 
@@ -525,7 +578,11 @@ module OMQ
525
578
  # every routing strategy, so it is flagged rather than fixed here.
526
579
  def drain_send_queues(timeout)
527
580
  return unless @routing.respond_to?(:send_queues_drained?)
528
- deadline = timeout ? Async::Clock.now + timeout : nil
581
+
582
+ if timeout
583
+ deadline = Async::Clock.now + timeout
584
+ end
585
+
529
586
  until @routing.send_queues_drained?
530
587
  break if deadline && (deadline - Async::Clock.now) <= 0
531
588
  sleep 0.001
@@ -540,12 +597,16 @@ module OMQ
540
597
 
541
598
  def validate_endpoint!(endpoint)
542
599
  transport = transport_for(endpoint)
543
- 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
544
604
  end
545
605
 
546
606
 
547
607
  def start_accept_loops(listener)
548
608
  return unless listener.respond_to?(:start_accept_loops)
609
+
549
610
  listener.start_accept_loops(@lifecycle.barrier) do |io|
550
611
  handle_accepted(io, endpoint: listener.endpoint)
551
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