omq 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f01c18a7a887e0f2811abff2251f1b2c9b201091e800248fb852b92bedbcaf06
4
- data.tar.gz: d32b3e07223d906a3bb25fc8542409bb5c05af36b509f7fe70c91419d0836cf9
3
+ metadata.gz: 19b283aa20e543617cd7be9c899d2954c219631fd57417536be4fb26798e6e85
4
+ data.tar.gz: a39d1c870e6171726a00a5f1809556e3ce7c3a8c0e4b0e84722eae15f83f93b6
5
5
  SHA512:
6
- metadata.gz: c0b26ad39e26fe586cec53371bc4b1beeaf357b51f32e90ad8d889a8413f62c08c13be2d8f787e3528d4fca6bccc1ceae21454474465aff25f9418c37460d957
7
- data.tar.gz: b8d3acedee187c9c757268af173b55bd28daf502f8e054f92ef670c59a1781e2b2f73267425c239d27acc7cb036b0030ab562a12918c7db205dc34f4bcf8e38d
6
+ metadata.gz: 7a020d02b2a1da25dd37f6ff92836348985fb7bcd7b8c17392bacb4c23d0ee443b2dd294f8e30d5df5724e11049462d428efb6d2b5a3313b479ab7a3e7e71388
7
+ data.tar.gz: 3b9724342594f22aabe00d1b13d7aa71bfe652ca21954c6b4789556e95743fd03d1c62214a164be8cad717405766a5d0acdbb4c3fd73a0f95aa14738cfb17d81
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.2 — 2026-04-13
4
+
5
+ ### Added
6
+
7
+ - **`:disconnected` monitor events carry the underlying error.** When
8
+ a connection drops due to a `Protocol::ZMTP::Error` (oversized
9
+ frame, bad framing, zstd bytebomb, nonce exhaustion, …) or a
10
+ `CONNECTION_LOST` error, the `:disconnected` event's `detail` hash
11
+ now includes `error:` (the exception instance) and `reason:` (its
12
+ message). Peer tooling can match on `detail[:error].is_a?(...)` to
13
+ enforce its own policy — e.g. `omq-cli` terminates the command on
14
+ `Protocol::ZMTP::Error`, while the library keeps the libzmq-parity
15
+ behavior of silently dropping the offending connection and
16
+ reconnecting.
17
+ - **`OMQ::Socket#engine` public reader.** The socket's engine is now
18
+ a documented (if low-level) accessor for peer tooling that needs
19
+ to reach into internals — notably so `omq-cli`'s monitor callback
20
+ can call `sock.engine.signal_fatal_error(error)` without
21
+ `instance_variable_get`. Not part of the stable user API.
22
+
23
+ ### Fixed
24
+
25
+ - **`signal_fatal_error` preserves the underlying cause.** The
26
+ resulting `SocketDeadError` now chains back to the original error
27
+ via `Exception#cause` regardless of whether `signal_fatal_error`
28
+ is called from inside a rescue block or from a monitor callback
29
+ (where `$!` is `nil`). Uses a raise-in-rescue helper to force the
30
+ cause chain. The wrapped error's message also includes the
31
+ original reason so tooling that only logs the top-level message
32
+ still shows what happened.
33
+
34
+ ## 0.19.1 — 2026-04-13
35
+
36
+ ### Fixed
37
+
38
+ - **Send-queue batch accounting tolerates non-string parts.**
39
+ `Routing::RoundRobin#drain_send_queue_capped` previously called
40
+ `#bytesize` directly on each message part for the fairness cap, which
41
+ crashed when a connection wrapper enqueued structured parts for later
42
+ transformation (notably `OMQ::Ractor`'s `MarshalConnection`, which
43
+ hands off live Ruby objects and marshals them in `#write_messages`).
44
+ The fairness cap now skips parts that don't respond to `#bytesize`.
45
+
3
46
  ## 0.19.0 — 2026-04-12
4
47
 
5
48
  ### 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
@@ -106,10 +106,12 @@ module OMQ
106
106
  task.yield
107
107
  end
108
108
  rescue Async::Stop, Async::Cancel
109
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
110
- # expected disconnect — supervisor will trigger teardown
109
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
110
+ # expected disconnect — stash reason for the :disconnected
111
+ # monitor event, let the lifecycle reconnect as usual
112
+ engine.connections[conn]&.record_disconnect_reason(error)
111
113
  rescue => error
112
- @engine.signal_fatal_error(error)
114
+ engine.signal_fatal_error(error)
113
115
  end
114
116
  end
115
117
 
@@ -136,10 +138,12 @@ module OMQ
136
138
  task.yield
137
139
  end
138
140
  rescue Async::Stop, Async::Cancel
139
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
140
- # expected disconnect — supervisor will trigger teardown
141
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
142
+ # expected disconnect — stash reason for the :disconnected
143
+ # monitor event, let the lifecycle reconnect as usual
144
+ engine.connections[conn]&.record_disconnect_reason(error)
141
145
  rescue => error
142
- @engine.signal_fatal_error(error)
146
+ engine.signal_fatal_error(error)
143
147
  end
144
148
  end
145
149
 
data/lib/omq/engine.rb CHANGED
@@ -386,8 +386,12 @@ module OMQ
386
386
 
387
387
  lifecycle.barrier.async(transient: true, annotation: annotation) do
388
388
  yield
389
- rescue Async::Stop, Async::Cancel, Protocol::ZMTP::Error, *CONNECTION_LOST
390
- # normal shutdown / expected disconnect / sibling tore us down
389
+ rescue Async::Stop, Async::Cancel
390
+ # normal shutdown / sibling tore us down
391
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
392
+ # expected disconnect — stash reason for the :disconnected
393
+ # monitor event, then let the lifecycle reconnect as usual
394
+ lifecycle.record_disconnect_reason(error)
391
395
  rescue => error
392
396
  signal_fatal_error(error)
393
397
  end
@@ -395,25 +399,35 @@ module OMQ
395
399
 
396
400
 
397
401
  # 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.
402
+ # unblocks any callers waiting on the recv queue. The original
403
+ # error is preserved as +#cause+ so callers can surface the real
404
+ # reason.
402
405
  #
403
406
  # @param error [Exception]
404
407
  #
405
408
  def signal_fatal_error(error)
406
409
  return unless @lifecycle.open?
407
- @fatal_error = begin
408
- raise SocketDeadError, "internal error killed #{@socket_type} socket"
409
- rescue => wrapped
410
- wrapped
411
- end
410
+ @fatal_error = build_fatal_error(error)
412
411
  routing.recv_queue.push(nil) rescue nil
413
412
  @lifecycle.peer_connected.resolve(nil) rescue nil
414
413
  end
415
414
 
416
415
 
416
+ # Constructs a SocketDeadError whose +cause+ is +error+. Uses the
417
+ # raise-in-rescue idiom because Ruby only sets +cause+ on an
418
+ # exception when it is raised from inside a rescue block -- works
419
+ # regardless of the original caller's +$!+ state.
420
+ def build_fatal_error(error)
421
+ raise error
422
+ rescue
423
+ begin
424
+ raise SocketDeadError, "#{@socket_type} socket killed: #{error.message}"
425
+ rescue SocketDeadError => wrapped
426
+ wrapped
427
+ end
428
+ end
429
+
430
+
417
431
  # Captures the socket's task tree root and starts the socket-level
418
432
  # maintenance task. If +parent+ is given, it is used as the parent
419
433
  # for every task spawned under this socket (connection supervisors,
@@ -147,16 +147,26 @@ module OMQ
147
147
  BATCH_BYTE_CAP = 512 * 1024
148
148
 
149
149
  def drain_send_queue_capped(batch)
150
- bytes = batch[0].sum(&:bytesize)
150
+ bytes = batch_bytes(batch[0])
151
151
  while batch.size < BATCH_MSG_CAP && bytes < BATCH_BYTE_CAP
152
152
  msg = @send_queue.dequeue(timeout: 0)
153
153
  break unless msg
154
154
  batch << msg
155
- bytes += msg.sum(&:bytesize)
155
+ bytes += batch_bytes(msg)
156
156
  end
157
157
  end
158
158
 
159
159
 
160
+ # Byte accounting for send-queue batching. Connection wrappers
161
+ # (e.g. OMQ::Ractor's MarshalConnection) may enqueue non-string
162
+ # parts that get transformed at write time — skip those for the
163
+ # fairness cap rather than crashing on #bytesize.
164
+ #
165
+ def batch_bytes(parts)
166
+ parts.sum { |p| p.respond_to?(:bytesize) ? p.bytesize : 0 }
167
+ end
168
+
169
+
160
170
  def write_batch(conn, batch)
161
171
  if batch.size == 1
162
172
  conn.send_message(transform_send(batch[0]))
data/lib/omq/socket.rb CHANGED
@@ -16,6 +16,13 @@ module OMQ
16
16
  attr_reader :last_tcp_port
17
17
 
18
18
 
19
+ # @return [Engine] the socket's engine. Exposed for peer tooling
20
+ # (omq-cli, omq-ffi, omq-ractor) that needs to reach into the
21
+ # socket's internals — not part of the stable user API.
22
+ #
23
+ attr_reader :engine
24
+
25
+
19
26
  # Delegate socket option accessors to @options.
20
27
  #
21
28
  extend Forwardable
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.0"
4
+ VERSION = "0.19.2"
5
5
  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.0
4
+ version: 0.19.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger