omq 0.17.9 → 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 +99 -0
- data/README.md +7 -5
- data/lib/omq/engine/connection_lifecycle.rb +32 -8
- data/lib/omq/engine/reconnect.rb +4 -1
- data/lib/omq/engine/recv_pump.rb +29 -2
- data/lib/omq/engine/socket_lifecycle.rb +19 -1
- data/lib/omq/engine.rb +36 -1
- data/lib/omq/pair.rb +2 -2
- data/lib/omq/pub_sub.rb +8 -8
- data/lib/omq/push_pull.rb +4 -4
- data/lib/omq/queue_interface.rb +3 -1
- data/lib/omq/reactor.rb +41 -20
- data/lib/omq/readable.rb +3 -1
- data/lib/omq/req_rep.rb +4 -4
- data/lib/omq/router_dealer.rb +4 -4
- data/lib/omq/routing/conn_send_pump.rb +7 -3
- data/lib/omq/routing/dealer.rb +2 -0
- data/lib/omq/routing/fair_queue.rb +14 -3
- data/lib/omq/routing/fan_out.rb +41 -4
- data/lib/omq/routing/pair.rb +1 -1
- data/lib/omq/routing/req.rb +10 -1
- data/lib/omq/routing/round_robin.rb +1 -1
- data/lib/omq/routing.rb +5 -4
- data/lib/omq/socket.rb +44 -58
- data/lib/omq/transport/inproc/direct_pipe.rb +16 -2
- data/lib/omq/transport/inproc.rb +41 -7
- data/lib/omq/transport/ipc.rb +26 -7
- data/lib/omq/transport/tcp.rb +81 -27
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +5 -1
- data/lib/omq.rb +2 -1
- metadata +3 -3
data/lib/omq/transport/inproc.rb
CHANGED
|
@@ -37,7 +37,10 @@ module OMQ
|
|
|
37
37
|
#
|
|
38
38
|
def bind(endpoint, engine)
|
|
39
39
|
@mutex.synchronize do
|
|
40
|
-
|
|
40
|
+
if @registry.key?(endpoint)
|
|
41
|
+
raise ArgumentError, "endpoint already bound: #{endpoint}"
|
|
42
|
+
end
|
|
43
|
+
|
|
41
44
|
@registry[endpoint] = engine
|
|
42
45
|
|
|
43
46
|
# Wake any pending connects
|
|
@@ -96,34 +99,55 @@ module OMQ
|
|
|
96
99
|
def establish_link(client_engine, server_engine, endpoint)
|
|
97
100
|
client_type = client_engine.socket_type
|
|
98
101
|
server_type = server_engine.socket_type
|
|
102
|
+
|
|
99
103
|
unless Protocol::ZMTP::VALID_PEERS[client_type]&.include?(server_type)
|
|
100
104
|
raise Protocol::ZMTP::Error,
|
|
101
105
|
"incompatible socket types: #{client_type} cannot connect to #{server_type}"
|
|
102
106
|
end
|
|
107
|
+
|
|
103
108
|
needs_cmds = needs_commands?(client_engine, server_engine, client_type, server_type)
|
|
104
|
-
client_pipe, server_pipe = make_pipe_pair
|
|
109
|
+
client_pipe, server_pipe = make_pipe_pair client_engine, server_engine,
|
|
110
|
+
client_type, server_type, needs_cmds
|
|
111
|
+
|
|
105
112
|
client_engine.connection_ready(client_pipe, endpoint: endpoint)
|
|
106
113
|
server_engine.connection_ready(server_pipe, endpoint: endpoint)
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
|
|
117
|
+
# Decides whether a DirectPipe pair needs command queues.
|
|
118
|
+
# DirectPipe's fast path skips queues entirely; command queues
|
|
119
|
+
# are only needed for socket types that exchange ZMTP commands
|
|
120
|
+
# (e.g. ROUTER/DEALER identity, PUB/SUB subscriptions) or when
|
|
121
|
+
# either side enables QoS ≥ 1.
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
#
|
|
110
125
|
def needs_commands?(ce, se, ct, st)
|
|
111
|
-
COMMAND_TYPES.include?(ct) || COMMAND_TYPES.include?(st)
|
|
112
|
-
|
|
126
|
+
return true if COMMAND_TYPES.include?(ct) || COMMAND_TYPES.include?(st)
|
|
127
|
+
return true if ce.options.qos >= 1 || se.options.qos >= 1
|
|
128
|
+
false
|
|
113
129
|
end
|
|
114
130
|
|
|
115
131
|
|
|
132
|
+
# Builds a bidirectional {DirectPipe} pair for client + server.
|
|
133
|
+
# When +needs_cmds+ is false the pipes have no command queues
|
|
134
|
+
# (fast path — all traffic bypasses Async::Queue entirely).
|
|
135
|
+
#
|
|
136
|
+
# @return [Array(DirectPipe, DirectPipe)] client, server
|
|
137
|
+
#
|
|
116
138
|
def make_pipe_pair(ce, se, ct, st, needs_cmds)
|
|
117
139
|
if needs_cmds
|
|
118
140
|
a_to_b = Async::Queue.new
|
|
119
141
|
b_to_a = Async::Queue.new
|
|
120
142
|
end
|
|
143
|
+
|
|
121
144
|
client = DirectPipe.new(send_queue: needs_cmds ? a_to_b : nil,
|
|
122
145
|
receive_queue: needs_cmds ? b_to_a : nil,
|
|
123
146
|
peer_identity: se.options.identity, peer_type: st.to_s)
|
|
124
147
|
server = DirectPipe.new(send_queue: needs_cmds ? b_to_a : nil,
|
|
125
148
|
receive_queue: needs_cmds ? a_to_b : nil,
|
|
126
149
|
peer_identity: ce.options.identity, peer_type: ct.to_s)
|
|
150
|
+
|
|
127
151
|
client.peer = server
|
|
128
152
|
server.peer = client
|
|
129
153
|
[client, server]
|
|
@@ -136,11 +160,20 @@ module OMQ
|
|
|
136
160
|
ri = engine.options.reconnect_interval
|
|
137
161
|
timeout = ri.is_a?(Range) ? ri.begin : ri
|
|
138
162
|
promise = Async::Promise.new
|
|
139
|
-
|
|
163
|
+
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
@waiters[endpoint] << promise
|
|
166
|
+
end
|
|
167
|
+
|
|
140
168
|
if promise.wait?(timeout: timeout)
|
|
141
|
-
@mutex.synchronize
|
|
169
|
+
@mutex.synchronize do
|
|
170
|
+
@registry[endpoint]
|
|
171
|
+
end
|
|
142
172
|
else
|
|
143
|
-
@mutex.synchronize
|
|
173
|
+
@mutex.synchronize do
|
|
174
|
+
@waiters[endpoint].delete(promise)
|
|
175
|
+
end
|
|
176
|
+
|
|
144
177
|
start_connect_retry(endpoint, engine)
|
|
145
178
|
nil
|
|
146
179
|
end
|
|
@@ -158,6 +191,7 @@ module OMQ
|
|
|
158
191
|
loop do
|
|
159
192
|
sleep ivl
|
|
160
193
|
bound_engine = @mutex.synchronize { @registry[endpoint] }
|
|
194
|
+
|
|
161
195
|
if bound_engine
|
|
162
196
|
establish_link(engine, bound_engine, endpoint)
|
|
163
197
|
break
|
data/lib/omq/transport/ipc.rb
CHANGED
|
@@ -19,7 +19,7 @@ module OMQ
|
|
|
19
19
|
# @return [Listener]
|
|
20
20
|
#
|
|
21
21
|
def bind(endpoint, engine)
|
|
22
|
-
path
|
|
22
|
+
path = parse_path(endpoint)
|
|
23
23
|
sock_path = to_socket_path(path)
|
|
24
24
|
|
|
25
25
|
# Remove stale socket file for file-based paths
|
|
@@ -45,17 +45,31 @@ module OMQ
|
|
|
45
45
|
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
|
|
49
|
+
# Applies SO_SNDBUF / SO_RCVBUF to +sock+ from the socket's
|
|
50
|
+
# {Options}. No-op when both are nil (OS default).
|
|
51
|
+
#
|
|
52
|
+
# @param sock [UNIXSocket, UNIXServer]
|
|
53
|
+
# @param options [Options]
|
|
54
|
+
#
|
|
48
55
|
def apply_buffer_sizes(sock, options)
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
if options.sndbuf
|
|
57
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, options.sndbuf)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if options.rcvbuf
|
|
61
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, options.rcvbuf)
|
|
62
|
+
end
|
|
51
63
|
end
|
|
52
64
|
|
|
65
|
+
|
|
53
66
|
private
|
|
54
67
|
|
|
68
|
+
|
|
55
69
|
# Extracts path from "ipc://path".
|
|
56
70
|
#
|
|
57
71
|
def parse_path(endpoint)
|
|
58
|
-
endpoint.
|
|
72
|
+
endpoint.delete_prefix("ipc://")
|
|
59
73
|
end
|
|
60
74
|
|
|
61
75
|
|
|
@@ -110,12 +124,15 @@ module OMQ
|
|
|
110
124
|
# @param parent_task [Async::Task]
|
|
111
125
|
# @yieldparam io [IO::Stream::Buffered]
|
|
112
126
|
#
|
|
113
|
-
def start_accept_loops(parent_task
|
|
114
|
-
|
|
127
|
+
def start_accept_loops(parent_task)
|
|
128
|
+
annotation = "ipc accept #{@endpoint}"
|
|
129
|
+
@task = parent_task.async(transient: true, annotation:) do
|
|
115
130
|
loop do
|
|
116
131
|
client = @server.accept
|
|
117
132
|
IPC.apply_buffer_sizes(client, @engine.options)
|
|
118
|
-
Async::Task.current.defer_stop
|
|
133
|
+
Async::Task.current.defer_stop do
|
|
134
|
+
yield IO::Stream::Buffered.wrap(client)
|
|
135
|
+
end
|
|
119
136
|
end
|
|
120
137
|
rescue Async::Stop
|
|
121
138
|
rescue IOError
|
|
@@ -135,11 +152,13 @@ module OMQ
|
|
|
135
152
|
def stop
|
|
136
153
|
@task&.stop
|
|
137
154
|
@server.close rescue nil
|
|
155
|
+
|
|
138
156
|
# Clean up socket file for file-based paths
|
|
139
157
|
unless @path.start_with?("@")
|
|
140
158
|
File.delete(@path) rescue nil
|
|
141
159
|
end
|
|
142
160
|
end
|
|
161
|
+
|
|
143
162
|
end
|
|
144
163
|
end
|
|
145
164
|
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.
|
|
@@ -85,9 +124,20 @@ module OMQ
|
|
|
85
124
|
end
|
|
86
125
|
|
|
87
126
|
|
|
127
|
+
# Applies SO_SNDBUF / SO_RCVBUF to +sock+ from the socket's
|
|
128
|
+
# {Options}. No-op when both are nil (OS default).
|
|
129
|
+
#
|
|
130
|
+
# @param sock [Socket, TCPSocket]
|
|
131
|
+
# @param options [Options]
|
|
132
|
+
#
|
|
88
133
|
def apply_buffer_sizes(sock, options)
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
if options.sndbuf
|
|
135
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, options.sndbuf)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if options.rcvbuf
|
|
139
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, options.rcvbuf)
|
|
140
|
+
end
|
|
91
141
|
end
|
|
92
142
|
end
|
|
93
143
|
|
|
@@ -103,13 +153,13 @@ module OMQ
|
|
|
103
153
|
#
|
|
104
154
|
attr_reader :port
|
|
105
155
|
|
|
106
|
-
# @return [Array<
|
|
156
|
+
# @return [Array<Socket>] bound server sockets
|
|
107
157
|
#
|
|
108
158
|
attr_reader :servers
|
|
109
159
|
|
|
110
160
|
|
|
111
161
|
# @param endpoint [String] resolved endpoint URI
|
|
112
|
-
# @param servers [Array<
|
|
162
|
+
# @param servers [Array<Socket>]
|
|
113
163
|
# @param port [Integer] bound port number
|
|
114
164
|
# @param engine [Engine]
|
|
115
165
|
#
|
|
@@ -128,13 +178,17 @@ module OMQ
|
|
|
128
178
|
# @param parent_task [Async::Task]
|
|
129
179
|
# @yieldparam io [IO::Stream::Buffered]
|
|
130
180
|
#
|
|
131
|
-
def start_accept_loops(parent_task
|
|
181
|
+
def start_accept_loops(parent_task)
|
|
132
182
|
@tasks = @servers.map do |server|
|
|
133
|
-
|
|
183
|
+
# TODO: use this server's exact host:port (@endpoint might not be unique)
|
|
184
|
+
annotation = "tcp accept #{@endpoint}"
|
|
185
|
+
parent_task.async(transient: true, annotation:) do
|
|
134
186
|
loop do
|
|
135
|
-
client = server.accept
|
|
187
|
+
client, _addr = server.accept
|
|
136
188
|
TCP.apply_buffer_sizes(client, @engine.options)
|
|
137
|
-
Async::Task.current.defer_stop
|
|
189
|
+
Async::Task.current.defer_stop do
|
|
190
|
+
yield IO::Stream::Buffered.wrap(client)
|
|
191
|
+
end
|
|
138
192
|
end
|
|
139
193
|
rescue Async::Stop
|
|
140
194
|
rescue IOError
|
data/lib/omq/version.rb
CHANGED
data/lib/omq/writable.rb
CHANGED
|
@@ -15,7 +15,11 @@ module OMQ
|
|
|
15
15
|
#
|
|
16
16
|
def send(message)
|
|
17
17
|
parts = freeze_message(message)
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
Reactor.run timeout: @options.write_timeout do |task|
|
|
20
|
+
@engine.enqueue_send(parts)
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
self
|
|
20
24
|
end
|
|
21
25
|
|
data/lib/omq.rb
CHANGED
|
@@ -38,6 +38,7 @@ module OMQ
|
|
|
38
38
|
IO::Stream::ConnectionResetError,
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
+
|
|
41
42
|
# Errors raised when a peer cannot be reached.
|
|
42
43
|
CONNECTION_FAILED = [
|
|
43
44
|
Errno::ECONNREFUSED,
|
|
@@ -57,7 +58,7 @@ module OMQ
|
|
|
57
58
|
CONNECTION_LOST.freeze
|
|
58
59
|
CONNECTION_FAILED.freeze
|
|
59
60
|
Engine.transports.freeze
|
|
60
|
-
Routing.
|
|
61
|
+
Routing.registry.freeze
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
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.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.6'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0.
|
|
25
|
+
version: '0.6'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: async
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|