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 +4 -4
- data/CHANGELOG.md +43 -0
- data/lib/omq/engine/connection_lifecycle.rb +15 -6
- data/lib/omq/engine/recv_pump.rb +10 -6
- data/lib/omq/engine.rb +25 -11
- data/lib/omq/routing/round_robin.rb +12 -2
- data/lib/omq/socket.rb +7 -0
- data/lib/omq/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19b283aa20e543617cd7be9c899d2954c219631fd57417536be4fb26798e6e85
|
|
4
|
+
data.tar.gz: a39d1c870e6171726a00a5f1809556e3ce7c3a8c0e4b0e84722eae15f83f93b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
data/lib/omq/engine/recv_pump.rb
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
|
390
|
-
# normal shutdown /
|
|
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
|
-
#
|
|
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 =
|
|
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]
|
|
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
|
|
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