omq 0.18.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 +88 -0
- data/lib/omq/engine/connection_lifecycle.rb +15 -6
- data/lib/omq/engine/recv_pump.rb +16 -8
- data/lib/omq/engine.rb +47 -11
- data/lib/omq/routing/conn_send_pump.rb +1 -3
- data/lib/omq/routing/fan_out.rb +2 -2
- data/lib/omq/routing/pair.rb +1 -1
- data/lib/omq/routing/round_robin.rb +13 -3
- data/lib/omq/socket.rb +7 -0
- data/lib/omq/transport/ipc.rb +5 -6
- data/lib/omq/transport/tcp.rb +63 -25
- 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,93 @@
|
|
|
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
|
+
|
|
46
|
+
## 0.19.0 — 2026-04-12
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **Verbose-monitor helpers `Engine#emit_verbose_msg_sent` and
|
|
51
|
+
`#emit_verbose_msg_received`.** Used by `RecvPump` and every
|
|
52
|
+
send-pump routing strategy (`conn_send_pump`, `round_robin`,
|
|
53
|
+
`pair`, `fan_out`) to emit `:message_sent` / `:message_received`
|
|
54
|
+
monitor events with a connection reference. When the connection
|
|
55
|
+
exposes `#last_wire_size_out` / `#last_wire_size_in` (as the
|
|
56
|
+
`omq-rfc-zstd` `CompressionConnection` wrapper does), the event
|
|
57
|
+
detail includes `wire_size:` so verbose traces can annotate
|
|
58
|
+
compressed message previews with the post-compression byte count.
|
|
59
|
+
`RecvPump` now emits the trace *before* enqueueing the message
|
|
60
|
+
so the monitor fiber runs before the application fiber, which
|
|
61
|
+
preserves log-before-body ordering at `-vvv`.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **`OMQ::Transport::TCP` normalizes host shorthands.** `tcp://*:PORT`
|
|
66
|
+
now binds *dual-stack* (both `0.0.0.0` and `::` on the same port,
|
|
67
|
+
with `IPV6_V6ONLY` set) rather than IPv4-only `0.0.0.0`, matching
|
|
68
|
+
[Puma v8.0.0's behavior](https://github.com/puma/puma/releases/tag/v8.0.0).
|
|
69
|
+
`tcp://:PORT`, `tcp://localhost:PORT`, and `tcp://*:PORT` on the
|
|
70
|
+
connect side all normalize to the loopback host — `::1` on
|
|
71
|
+
IPv6-capable machines (at least one non-loopback, non-link-local
|
|
72
|
+
IPv6 address), otherwise `127.0.0.1`. Explicit addresses
|
|
73
|
+
(`0.0.0.0`, `::`, `127.0.0.1`, `::1`) pass through unchanged.
|
|
74
|
+
Documented in `GETTING_STARTED.md` under "TCP host shorthands".
|
|
75
|
+
This normalization previously lived in `omq-cli` and is now
|
|
76
|
+
shared by all callers.
|
|
77
|
+
|
|
78
|
+
- **TCP accept loop uses `Socket.tcp_server_sockets`** instead of
|
|
79
|
+
manually iterating `Addrinfo.getaddrinfo` + `TCPServer.new`.
|
|
80
|
+
`tcp_server_sockets` handles dual-stack port coordination and
|
|
81
|
+
`IPV6_V6ONLY` automatically. `Listener#servers` now holds
|
|
82
|
+
`Socket` instances rather than `TCPServer`; `#accept` returns
|
|
83
|
+
`[client, addrinfo]` pairs, which the accept loop destructures.
|
|
84
|
+
|
|
85
|
+
- **`Listener#start_accept_loops` uses `yield`** instead of capturing
|
|
86
|
+
the block as an explicit `&on_accepted` proc. The block is bound
|
|
87
|
+
to the enclosing method even when invoked from inside a spawned
|
|
88
|
+
`Async::Task`, so the explicit capture was unnecessary. Applies
|
|
89
|
+
to both TCP and IPC transports.
|
|
90
|
+
|
|
3
91
|
## 0.18.0 — 2026-04-12
|
|
4
92
|
|
|
5
93
|
### Changed
|
|
@@ -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
|
@@ -94,18 +94,24 @@ module OMQ
|
|
|
94
94
|
while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
|
|
95
95
|
msg = conn.receive_message
|
|
96
96
|
msg = transform.call(msg).freeze
|
|
97
|
+
# Emit the verbose trace BEFORE enqueueing so the monitor
|
|
98
|
+
# fiber is woken before the application fiber -- the
|
|
99
|
+
# async scheduler is FIFO on the ready list, so this
|
|
100
|
+
# preserves log-before-stdout ordering for -vvv traces.
|
|
101
|
+
engine.emit_verbose_msg_received(conn, msg)
|
|
97
102
|
recv_queue.enqueue(msg)
|
|
98
|
-
engine.emit_verbose_monitor_event(:message_received, parts: msg)
|
|
99
103
|
count += 1
|
|
100
104
|
bytes += msg.sum(&:bytesize) if count_bytes
|
|
101
105
|
end
|
|
102
106
|
task.yield
|
|
103
107
|
end
|
|
104
108
|
rescue Async::Stop, Async::Cancel
|
|
105
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
106
|
-
# 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)
|
|
107
113
|
rescue => error
|
|
108
|
-
|
|
114
|
+
engine.signal_fatal_error(error)
|
|
109
115
|
end
|
|
110
116
|
end
|
|
111
117
|
|
|
@@ -124,18 +130,20 @@ module OMQ
|
|
|
124
130
|
bytes = 0
|
|
125
131
|
while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
|
|
126
132
|
msg = conn.receive_message
|
|
133
|
+
engine.emit_verbose_msg_received(conn, msg)
|
|
127
134
|
recv_queue.enqueue(msg)
|
|
128
|
-
engine.emit_verbose_monitor_event(:message_received, parts: msg)
|
|
129
135
|
count += 1
|
|
130
136
|
bytes += msg.sum(&:bytesize) if count_bytes
|
|
131
137
|
end
|
|
132
138
|
task.yield
|
|
133
139
|
end
|
|
134
140
|
rescue Async::Stop, Async::Cancel
|
|
135
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
136
|
-
# 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)
|
|
137
145
|
rescue => error
|
|
138
|
-
|
|
146
|
+
engine.signal_fatal_error(error)
|
|
139
147
|
end
|
|
140
148
|
end
|
|
141
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,
|
|
@@ -461,6 +475,28 @@ module OMQ
|
|
|
461
475
|
end
|
|
462
476
|
|
|
463
477
|
|
|
478
|
+
# Emits a :message_sent verbose event and enriches it with the
|
|
479
|
+
# on-wire (post-compression) byte size if +conn+ exposes
|
|
480
|
+
# +last_wire_size_out+ (installed by ZMTP-Zstd etc.).
|
|
481
|
+
def emit_verbose_msg_sent(conn, parts)
|
|
482
|
+
return unless @verbose_monitor
|
|
483
|
+
detail = { parts: parts }
|
|
484
|
+
detail[:wire_size] = conn.last_wire_size_out if conn.respond_to?(:last_wire_size_out)
|
|
485
|
+
emit_monitor_event(:message_sent, detail: detail)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# Emits a :message_received verbose event and enriches it with the
|
|
490
|
+
# on-wire (pre-decompression) byte size if +conn+ exposes
|
|
491
|
+
# +last_wire_size_in+.
|
|
492
|
+
def emit_verbose_msg_received(conn, parts)
|
|
493
|
+
return unless @verbose_monitor
|
|
494
|
+
detail = { parts: parts }
|
|
495
|
+
detail[:wire_size] = conn.last_wire_size_in if conn.respond_to?(:last_wire_size_in)
|
|
496
|
+
emit_monitor_event(:message_received, detail: detail)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
|
|
464
500
|
# Looks up the transport module for an endpoint URI.
|
|
465
501
|
#
|
|
466
502
|
# @param endpoint [String] endpoint URI (e.g. "tcp://...", "inproc://...")
|
data/lib/omq/routing/fan_out.rb
CHANGED
|
@@ -179,7 +179,7 @@ module OMQ
|
|
|
179
179
|
Routing.drain_send_queue(q, batch)
|
|
180
180
|
if write_matching_batch(conn, batch, use_wire)
|
|
181
181
|
conn.flush
|
|
182
|
-
batch.each { |parts| @engine.
|
|
182
|
+
batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
|
|
183
183
|
end
|
|
184
184
|
end
|
|
185
185
|
end
|
|
@@ -227,7 +227,7 @@ module OMQ
|
|
|
227
227
|
|
|
228
228
|
conn.write_message(latest)
|
|
229
229
|
conn.flush
|
|
230
|
-
@engine.
|
|
230
|
+
@engine.emit_verbose_msg_sent(conn, latest)
|
|
231
231
|
end
|
|
232
232
|
end
|
|
233
233
|
end
|
data/lib/omq/routing/pair.rb
CHANGED
|
@@ -96,7 +96,7 @@ module OMQ
|
|
|
96
96
|
conn.write_messages(batch)
|
|
97
97
|
end
|
|
98
98
|
conn.flush
|
|
99
|
-
batch.each { |parts| @engine.
|
|
99
|
+
batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
@tasks << @send_pump
|
|
@@ -134,7 +134,7 @@ module OMQ
|
|
|
134
134
|
ensure
|
|
135
135
|
@in_flight -= batch.size
|
|
136
136
|
end
|
|
137
|
-
batch.each { |parts| @engine.
|
|
137
|
+
batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
|
|
138
138
|
Async::Task.current.yield
|
|
139
139
|
end
|
|
140
140
|
end
|
|
@@ -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/transport/ipc.rb
CHANGED
|
@@ -79,7 +79,7 @@ module OMQ
|
|
|
79
79
|
if abstract?(path)
|
|
80
80
|
"\0#{path[1..]}"
|
|
81
81
|
else
|
|
82
|
-
path
|
|
82
|
+
path
|
|
83
83
|
end
|
|
84
84
|
end
|
|
85
85
|
|
|
@@ -106,7 +106,7 @@ module OMQ
|
|
|
106
106
|
|
|
107
107
|
# @param endpoint [String] the IPC endpoint URI
|
|
108
108
|
# @param server [UNIXServer]
|
|
109
|
-
# @param path [String] filesystem or abstract namespace path
|
|
109
|
+
# @param path [String] filesystem or abstract namespace path
|
|
110
110
|
# @param engine [Engine]
|
|
111
111
|
#
|
|
112
112
|
def initialize(endpoint, server, path, engine)
|
|
@@ -124,15 +124,14 @@ module OMQ
|
|
|
124
124
|
# @param parent_task [Async::Task]
|
|
125
125
|
# @yieldparam io [IO::Stream::Buffered]
|
|
126
126
|
#
|
|
127
|
-
def start_accept_loops(parent_task
|
|
127
|
+
def start_accept_loops(parent_task)
|
|
128
128
|
annotation = "ipc accept #{@endpoint}"
|
|
129
129
|
@task = parent_task.async(transient: true, annotation:) do
|
|
130
130
|
loop do
|
|
131
131
|
client = @server.accept
|
|
132
132
|
IPC.apply_buffer_sizes(client, @engine.options)
|
|
133
133
|
Async::Task.current.defer_stop do
|
|
134
|
-
|
|
135
|
-
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
134
|
+
yield IO::Stream::Buffered.wrap(client)
|
|
136
135
|
end
|
|
137
136
|
end
|
|
138
137
|
rescue Async::Stop
|
|
@@ -155,7 +154,7 @@ module OMQ
|
|
|
155
154
|
@server.close rescue nil
|
|
156
155
|
|
|
157
156
|
# Clean up socket file for file-based paths
|
|
158
|
-
unless @path.start_with?("@")
|
|
157
|
+
unless @path.start_with?("@")
|
|
159
158
|
File.delete(@path) rescue nil
|
|
160
159
|
end
|
|
161
160
|
end
|
data/lib/omq/transport/tcp.rb
CHANGED
|
@@ -17,23 +17,19 @@ module OMQ
|
|
|
17
17
|
# @return [Listener]
|
|
18
18
|
#
|
|
19
19
|
def bind(endpoint, engine)
|
|
20
|
-
host, port
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
servers
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
host_part = host.include?(":") ? "[#{host}]" : host
|
|
36
|
-
resolved = "tcp://#{host_part}:#{actual_port}"
|
|
20
|
+
host, port = self.parse_endpoint(endpoint)
|
|
21
|
+
lookup_host = normalize_bind_host(host)
|
|
22
|
+
|
|
23
|
+
# Socket.tcp_server_sockets coordinates ephemeral ports across
|
|
24
|
+
# address families and sets IPV6_V6ONLY so IPv4 and IPv6
|
|
25
|
+
# wildcards don't collide on Linux.
|
|
26
|
+
servers = ::Socket.tcp_server_sockets(lookup_host, port)
|
|
27
|
+
raise ::Socket::ResolutionError, "no addresses for #{host.inspect}" if servers.empty?
|
|
28
|
+
|
|
29
|
+
actual_port = servers.first.local_address.ip_port
|
|
30
|
+
display_host = host == "*" ? "*" : (lookup_host || "*")
|
|
31
|
+
host_part = display_host.include?(":") ? "[#{display_host}]" : display_host
|
|
32
|
+
resolved = "tcp://#{host_part}:#{actual_port}"
|
|
37
33
|
Listener.new(resolved, servers, actual_port, engine)
|
|
38
34
|
end
|
|
39
35
|
|
|
@@ -45,7 +41,8 @@ module OMQ
|
|
|
45
41
|
#
|
|
46
42
|
def validate_endpoint!(endpoint)
|
|
47
43
|
host, _port = parse_endpoint(endpoint)
|
|
48
|
-
|
|
44
|
+
lookup_host = normalize_bind_host(host)
|
|
45
|
+
Addrinfo.getaddrinfo(lookup_host, nil, nil, :STREAM) if lookup_host
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
|
|
@@ -57,12 +54,54 @@ module OMQ
|
|
|
57
54
|
#
|
|
58
55
|
def connect(endpoint, engine)
|
|
59
56
|
host, port = self.parse_endpoint(endpoint)
|
|
60
|
-
|
|
57
|
+
host = normalize_connect_host(host)
|
|
58
|
+
sock = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
|
|
61
59
|
apply_buffer_sizes(sock, engine.options)
|
|
62
60
|
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
63
61
|
end
|
|
64
62
|
|
|
65
63
|
|
|
64
|
+
# Normalizes the bind host:
|
|
65
|
+
# "*" → nil (dual-stack wildcard via AI_PASSIVE)
|
|
66
|
+
# "" / nil / "localhost" → loopback_host (::1 on IPv6-capable hosts, else 127.0.0.1)
|
|
67
|
+
# else → unchanged
|
|
68
|
+
#
|
|
69
|
+
def normalize_bind_host(host)
|
|
70
|
+
case host
|
|
71
|
+
when "*" then nil
|
|
72
|
+
when nil, "", "localhost" then loopback_host
|
|
73
|
+
else host
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Normalizes the connect host: "", nil, "*", and "localhost" all
|
|
79
|
+
# map to the loopback host. Everything else is passed through so
|
|
80
|
+
# real hostnames still go through the resolver + Happy Eyeballs.
|
|
81
|
+
#
|
|
82
|
+
def normalize_connect_host(host)
|
|
83
|
+
case host
|
|
84
|
+
when nil, "", "*", "localhost" then loopback_host
|
|
85
|
+
else host
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Loopback address preference for bind/connect normalization.
|
|
91
|
+
# Returns "::1" when the host has at least one non-loopback,
|
|
92
|
+
# non-link-local IPv6 address, otherwise "127.0.0.1".
|
|
93
|
+
#
|
|
94
|
+
def loopback_host
|
|
95
|
+
@loopback_host ||= begin
|
|
96
|
+
has_ipv6 = ::Socket.getifaddrs.any? do |ifa|
|
|
97
|
+
addr = ifa.addr
|
|
98
|
+
addr&.ipv6? && !addr.ipv6_loopback? && !addr.ipv6_linklocal?
|
|
99
|
+
end
|
|
100
|
+
has_ipv6 ? "::1" : "127.0.0.1"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
|
|
66
105
|
# Connect timeout: cap each attempt at the reconnect interval so a
|
|
67
106
|
# hung connect(2) (e.g. macOS kqueue + IPv6 ECONNREFUSED not delivered)
|
|
68
107
|
# doesn't block the retry loop. Floor at 0.5s for real-network latency.
|
|
@@ -114,13 +153,13 @@ module OMQ
|
|
|
114
153
|
#
|
|
115
154
|
attr_reader :port
|
|
116
155
|
|
|
117
|
-
# @return [Array<
|
|
156
|
+
# @return [Array<Socket>] bound server sockets
|
|
118
157
|
#
|
|
119
158
|
attr_reader :servers
|
|
120
159
|
|
|
121
160
|
|
|
122
161
|
# @param endpoint [String] resolved endpoint URI
|
|
123
|
-
# @param servers [Array<
|
|
162
|
+
# @param servers [Array<Socket>]
|
|
124
163
|
# @param port [Integer] bound port number
|
|
125
164
|
# @param engine [Engine]
|
|
126
165
|
#
|
|
@@ -139,17 +178,16 @@ module OMQ
|
|
|
139
178
|
# @param parent_task [Async::Task]
|
|
140
179
|
# @yieldparam io [IO::Stream::Buffered]
|
|
141
180
|
#
|
|
142
|
-
def start_accept_loops(parent_task
|
|
181
|
+
def start_accept_loops(parent_task)
|
|
143
182
|
@tasks = @servers.map do |server|
|
|
144
183
|
# TODO: use this server's exact host:port (@endpoint might not be unique)
|
|
145
184
|
annotation = "tcp accept #{@endpoint}"
|
|
146
185
|
parent_task.async(transient: true, annotation:) do
|
|
147
186
|
loop do
|
|
148
|
-
client = server.accept
|
|
187
|
+
client, _addr = server.accept
|
|
149
188
|
TCP.apply_buffer_sizes(client, @engine.options)
|
|
150
189
|
Async::Task.current.defer_stop do
|
|
151
|
-
|
|
152
|
-
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
190
|
+
yield IO::Stream::Buffered.wrap(client)
|
|
153
191
|
end
|
|
154
192
|
end
|
|
155
193
|
rescue Async::Stop
|
data/lib/omq/version.rb
CHANGED