omq 0.18.0 → 0.19.0
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 +45 -0
- data/lib/omq/engine/recv_pump.rb +6 -2
- data/lib/omq/engine.rb +22 -0
- 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 +1 -1
- 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: f01c18a7a887e0f2811abff2251f1b2c9b201091e800248fb852b92bedbcaf06
|
|
4
|
+
data.tar.gz: d32b3e07223d906a3bb25fc8542409bb5c05af36b509f7fe70c91419d0836cf9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0b26ad39e26fe586cec53371bc4b1beeaf357b51f32e90ad8d889a8413f62c08c13be2d8f787e3528d4fca6bccc1ceae21454474465aff25f9418c37460d957
|
|
7
|
+
data.tar.gz: b8d3acedee187c9c757268af173b55bd28daf502f8e054f92ef670c59a1781e2b2f73267425c239d27acc7cb036b0030ab562a12918c7db205dc34f4bcf8e38d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0 — 2026-04-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Verbose-monitor helpers `Engine#emit_verbose_msg_sent` and
|
|
8
|
+
`#emit_verbose_msg_received`.** Used by `RecvPump` and every
|
|
9
|
+
send-pump routing strategy (`conn_send_pump`, `round_robin`,
|
|
10
|
+
`pair`, `fan_out`) to emit `:message_sent` / `:message_received`
|
|
11
|
+
monitor events with a connection reference. When the connection
|
|
12
|
+
exposes `#last_wire_size_out` / `#last_wire_size_in` (as the
|
|
13
|
+
`omq-rfc-zstd` `CompressionConnection` wrapper does), the event
|
|
14
|
+
detail includes `wire_size:` so verbose traces can annotate
|
|
15
|
+
compressed message previews with the post-compression byte count.
|
|
16
|
+
`RecvPump` now emits the trace *before* enqueueing the message
|
|
17
|
+
so the monitor fiber runs before the application fiber, which
|
|
18
|
+
preserves log-before-body ordering at `-vvv`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **`OMQ::Transport::TCP` normalizes host shorthands.** `tcp://*:PORT`
|
|
23
|
+
now binds *dual-stack* (both `0.0.0.0` and `::` on the same port,
|
|
24
|
+
with `IPV6_V6ONLY` set) rather than IPv4-only `0.0.0.0`, matching
|
|
25
|
+
[Puma v8.0.0's behavior](https://github.com/puma/puma/releases/tag/v8.0.0).
|
|
26
|
+
`tcp://:PORT`, `tcp://localhost:PORT`, and `tcp://*:PORT` on the
|
|
27
|
+
connect side all normalize to the loopback host — `::1` on
|
|
28
|
+
IPv6-capable machines (at least one non-loopback, non-link-local
|
|
29
|
+
IPv6 address), otherwise `127.0.0.1`. Explicit addresses
|
|
30
|
+
(`0.0.0.0`, `::`, `127.0.0.1`, `::1`) pass through unchanged.
|
|
31
|
+
Documented in `GETTING_STARTED.md` under "TCP host shorthands".
|
|
32
|
+
This normalization previously lived in `omq-cli` and is now
|
|
33
|
+
shared by all callers.
|
|
34
|
+
|
|
35
|
+
- **TCP accept loop uses `Socket.tcp_server_sockets`** instead of
|
|
36
|
+
manually iterating `Addrinfo.getaddrinfo` + `TCPServer.new`.
|
|
37
|
+
`tcp_server_sockets` handles dual-stack port coordination and
|
|
38
|
+
`IPV6_V6ONLY` automatically. `Listener#servers` now holds
|
|
39
|
+
`Socket` instances rather than `TCPServer`; `#accept` returns
|
|
40
|
+
`[client, addrinfo]` pairs, which the accept loop destructures.
|
|
41
|
+
|
|
42
|
+
- **`Listener#start_accept_loops` uses `yield`** instead of capturing
|
|
43
|
+
the block as an explicit `&on_accepted` proc. The block is bound
|
|
44
|
+
to the enclosing method even when invoked from inside a spawned
|
|
45
|
+
`Async::Task`, so the explicit capture was unnecessary. Applies
|
|
46
|
+
to both TCP and IPC transports.
|
|
47
|
+
|
|
3
48
|
## 0.18.0 — 2026-04-12
|
|
4
49
|
|
|
5
50
|
### Changed
|
data/lib/omq/engine/recv_pump.rb
CHANGED
|
@@ -94,8 +94,12 @@ 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
|
|
@@ -124,8 +128,8 @@ module OMQ
|
|
|
124
128
|
bytes = 0
|
|
125
129
|
while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
|
|
126
130
|
msg = conn.receive_message
|
|
131
|
+
engine.emit_verbose_msg_received(conn, msg)
|
|
127
132
|
recv_queue.enqueue(msg)
|
|
128
|
-
engine.emit_verbose_monitor_event(:message_received, parts: msg)
|
|
129
133
|
count += 1
|
|
130
134
|
bytes += msg.sum(&:bytesize) if count_bytes
|
|
131
135
|
end
|
data/lib/omq/engine.rb
CHANGED
|
@@ -461,6 +461,28 @@ module OMQ
|
|
|
461
461
|
end
|
|
462
462
|
|
|
463
463
|
|
|
464
|
+
# Emits a :message_sent verbose event and enriches it with the
|
|
465
|
+
# on-wire (post-compression) byte size if +conn+ exposes
|
|
466
|
+
# +last_wire_size_out+ (installed by ZMTP-Zstd etc.).
|
|
467
|
+
def emit_verbose_msg_sent(conn, parts)
|
|
468
|
+
return unless @verbose_monitor
|
|
469
|
+
detail = { parts: parts }
|
|
470
|
+
detail[:wire_size] = conn.last_wire_size_out if conn.respond_to?(:last_wire_size_out)
|
|
471
|
+
emit_monitor_event(:message_sent, detail: detail)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# Emits a :message_received verbose event and enriches it with the
|
|
476
|
+
# on-wire (pre-decompression) byte size if +conn+ exposes
|
|
477
|
+
# +last_wire_size_in+.
|
|
478
|
+
def emit_verbose_msg_received(conn, parts)
|
|
479
|
+
return unless @verbose_monitor
|
|
480
|
+
detail = { parts: parts }
|
|
481
|
+
detail[:wire_size] = conn.last_wire_size_in if conn.respond_to?(:last_wire_size_in)
|
|
482
|
+
emit_monitor_event(:message_received, detail: detail)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
|
|
464
486
|
# Looks up the transport module for an endpoint URI.
|
|
465
487
|
#
|
|
466
488
|
# @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
|
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