omq 0.9.0 → 0.11.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 +129 -0
- data/README.md +28 -3
- data/lib/omq/channel.rb +5 -5
- data/lib/omq/client_server.rb +10 -10
- data/lib/omq/engine.rb +702 -0
- data/lib/omq/options.rb +48 -0
- data/lib/omq/pair.rb +4 -4
- data/lib/omq/peer.rb +5 -5
- data/lib/omq/pub_sub.rb +18 -18
- data/lib/omq/push_pull.rb +6 -6
- data/lib/omq/queue_interface.rb +73 -0
- data/lib/omq/radio_dish.rb +6 -6
- data/lib/omq/reactor.rb +128 -0
- data/lib/omq/readable.rb +44 -0
- data/lib/omq/req_rep.rb +8 -8
- data/lib/omq/router_dealer.rb +8 -8
- data/lib/omq/routing/channel.rb +83 -0
- data/lib/omq/routing/client.rb +56 -0
- data/lib/omq/routing/dealer.rb +57 -0
- data/lib/omq/routing/dish.rb +78 -0
- data/lib/omq/routing/fan_out.rb +140 -0
- data/lib/omq/routing/gather.rb +46 -0
- data/lib/omq/routing/pair.rb +86 -0
- data/lib/omq/routing/peer.rb +101 -0
- data/lib/omq/routing/pub.rb +60 -0
- data/lib/omq/routing/pull.rb +46 -0
- data/lib/omq/routing/push.rb +81 -0
- data/lib/omq/routing/radio.rb +150 -0
- data/lib/omq/routing/rep.rb +101 -0
- data/lib/omq/routing/req.rb +65 -0
- data/lib/omq/routing/round_robin.rb +168 -0
- data/lib/omq/routing/router.rb +110 -0
- data/lib/omq/routing/scatter.rb +82 -0
- data/lib/omq/routing/server.rb +101 -0
- data/lib/omq/routing/sub.rb +78 -0
- data/lib/omq/routing/xpub.rb +72 -0
- data/lib/omq/routing/xsub.rb +83 -0
- data/lib/omq/routing.rb +66 -0
- data/lib/omq/scatter_gather.rb +8 -8
- data/lib/omq/single_frame.rb +18 -0
- data/lib/omq/socket.rb +32 -11
- data/lib/omq/transport/inproc.rb +355 -0
- data/lib/omq/transport/ipc.rb +117 -0
- data/lib/omq/transport/tcp.rb +111 -0
- data/lib/omq/transport/tls.rb +146 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +66 -0
- data/lib/omq.rb +64 -4
- metadata +34 -33
- data/lib/omq/zmtp/engine.rb +0 -551
- data/lib/omq/zmtp/options.rb +0 -48
- data/lib/omq/zmtp/reactor.rb +0 -131
- data/lib/omq/zmtp/readable.rb +0 -29
- data/lib/omq/zmtp/routing/channel.rb +0 -81
- data/lib/omq/zmtp/routing/client.rb +0 -56
- data/lib/omq/zmtp/routing/dealer.rb +0 -57
- data/lib/omq/zmtp/routing/dish.rb +0 -80
- data/lib/omq/zmtp/routing/fan_out.rb +0 -131
- data/lib/omq/zmtp/routing/gather.rb +0 -48
- data/lib/omq/zmtp/routing/pair.rb +0 -84
- data/lib/omq/zmtp/routing/peer.rb +0 -100
- data/lib/omq/zmtp/routing/pub.rb +0 -62
- data/lib/omq/zmtp/routing/pull.rb +0 -48
- data/lib/omq/zmtp/routing/push.rb +0 -80
- data/lib/omq/zmtp/routing/radio.rb +0 -139
- data/lib/omq/zmtp/routing/rep.rb +0 -101
- data/lib/omq/zmtp/routing/req.rb +0 -65
- data/lib/omq/zmtp/routing/round_robin.rb +0 -143
- data/lib/omq/zmtp/routing/router.rb +0 -109
- data/lib/omq/zmtp/routing/scatter.rb +0 -81
- data/lib/omq/zmtp/routing/server.rb +0 -100
- data/lib/omq/zmtp/routing/sub.rb +0 -80
- data/lib/omq/zmtp/routing/xpub.rb +0 -74
- data/lib/omq/zmtp/routing/xsub.rb +0 -86
- data/lib/omq/zmtp/routing.rb +0 -65
- data/lib/omq/zmtp/single_frame.rb +0 -20
- data/lib/omq/zmtp/transport/inproc.rb +0 -359
- data/lib/omq/zmtp/transport/ipc.rb +0 -118
- data/lib/omq/zmtp/transport/tcp.rb +0 -117
- data/lib/omq/zmtp/writable.rb +0 -61
- data/lib/omq/zmtp.rb +0 -81
data/lib/omq/zmtp/routing.rb
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async"
|
|
4
|
-
require "async/queue"
|
|
5
|
-
require "async/limited_queue"
|
|
6
|
-
|
|
7
|
-
module OMQ
|
|
8
|
-
module ZMTP
|
|
9
|
-
# Routing strategies for each ZMQ socket type.
|
|
10
|
-
#
|
|
11
|
-
# Each strategy manages how messages flow between connections and
|
|
12
|
-
# the socket's send/recv queues.
|
|
13
|
-
#
|
|
14
|
-
module Routing
|
|
15
|
-
# Maximum messages to drain from the send queue per flush cycle.
|
|
16
|
-
MAX_SEND_BATCH = 64
|
|
17
|
-
|
|
18
|
-
# Drains up to +max+ additional messages from +queue+ into +batch+
|
|
19
|
-
# without blocking. Call after the initial blocking dequeue.
|
|
20
|
-
#
|
|
21
|
-
# @param queue [Async::LimitedQueue]
|
|
22
|
-
# @param batch [Array]
|
|
23
|
-
# @param max [Integer]
|
|
24
|
-
# @return [void]
|
|
25
|
-
#
|
|
26
|
-
def self.drain_send_queue(queue, batch, max = MAX_SEND_BATCH)
|
|
27
|
-
while batch.size < max
|
|
28
|
-
msg = queue.dequeue(timeout: 0)
|
|
29
|
-
break unless msg
|
|
30
|
-
batch << msg
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Returns the routing strategy class for a socket type.
|
|
35
|
-
#
|
|
36
|
-
# @param socket_type [Symbol] e.g. :PAIR, :REQ
|
|
37
|
-
# @return [Class]
|
|
38
|
-
#
|
|
39
|
-
def self.for(socket_type)
|
|
40
|
-
case socket_type
|
|
41
|
-
when :PAIR then Pair
|
|
42
|
-
when :REQ then Req
|
|
43
|
-
when :REP then Rep
|
|
44
|
-
when :DEALER then Dealer
|
|
45
|
-
when :ROUTER then Router
|
|
46
|
-
when :PUB then Pub
|
|
47
|
-
when :SUB then Sub
|
|
48
|
-
when :XPUB then XPub
|
|
49
|
-
when :XSUB then XSub
|
|
50
|
-
when :PUSH then Push
|
|
51
|
-
when :PULL then Pull
|
|
52
|
-
when :CLIENT then Client
|
|
53
|
-
when :SERVER then Server
|
|
54
|
-
when :RADIO then Radio
|
|
55
|
-
when :DISH then Dish
|
|
56
|
-
when :SCATTER then Scatter
|
|
57
|
-
when :GATHER then Gather
|
|
58
|
-
when :PEER then Peer
|
|
59
|
-
when :CHANNEL then Channel
|
|
60
|
-
else raise ArgumentError, "unknown socket type: #{socket_type}"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
# Mixin that rejects multipart messages.
|
|
6
|
-
#
|
|
7
|
-
# All draft socket types (CLIENT, SERVER, RADIO, DISH, SCATTER,
|
|
8
|
-
# GATHER, PEER, CHANNEL) require single-frame messages for
|
|
9
|
-
# thread-safe atomic operations.
|
|
10
|
-
#
|
|
11
|
-
module SingleFrame
|
|
12
|
-
def send(message)
|
|
13
|
-
if message.is_a?(Array) && message.size > 1
|
|
14
|
-
raise ArgumentError, "#{self.class} does not support multipart messages"
|
|
15
|
-
end
|
|
16
|
-
super
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async"
|
|
4
|
-
require "async/queue"
|
|
5
|
-
|
|
6
|
-
module OMQ
|
|
7
|
-
module ZMTP
|
|
8
|
-
module Transport
|
|
9
|
-
# In-process transport.
|
|
10
|
-
#
|
|
11
|
-
# Both peers are Ruby backend sockets in the same process (native
|
|
12
|
-
# ZMQ's inproc registry is separate and unreachable). Messages are
|
|
13
|
-
# transferred as Ruby arrays — no ZMTP framing, no byte
|
|
14
|
-
# serialization. String parts are frozen by Writable#send to
|
|
15
|
-
# prevent shared mutable state without copying.
|
|
16
|
-
#
|
|
17
|
-
module Inproc
|
|
18
|
-
# Socket types that exchange commands (SUBSCRIBE/CANCEL) over inproc.
|
|
19
|
-
#
|
|
20
|
-
COMMAND_TYPES = %i[PUB SUB XPUB XSUB RADIO DISH].freeze
|
|
21
|
-
|
|
22
|
-
# Global registry of bound inproc endpoints.
|
|
23
|
-
#
|
|
24
|
-
@registry = {}
|
|
25
|
-
@mutex = Mutex.new
|
|
26
|
-
@waiters = Hash.new { |h, k| h[k] = [] }
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class << self
|
|
30
|
-
# Binds an engine to an inproc endpoint.
|
|
31
|
-
#
|
|
32
|
-
# @param endpoint [String] e.g. "inproc://my-endpoint"
|
|
33
|
-
# @param engine [Engine] the owning engine
|
|
34
|
-
# @return [Listener]
|
|
35
|
-
# @raise [ArgumentError] if endpoint is already bound
|
|
36
|
-
#
|
|
37
|
-
def bind(endpoint, engine)
|
|
38
|
-
@mutex.synchronize do
|
|
39
|
-
raise ArgumentError, "endpoint already bound: #{endpoint}" if @registry.key?(endpoint)
|
|
40
|
-
@registry[endpoint] = engine
|
|
41
|
-
|
|
42
|
-
# Wake any pending connects
|
|
43
|
-
@waiters[endpoint].each { |p| p.resolve(true) }
|
|
44
|
-
@waiters.delete(endpoint)
|
|
45
|
-
end
|
|
46
|
-
Listener.new(endpoint)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# Connects to a bound inproc endpoint.
|
|
51
|
-
#
|
|
52
|
-
# @param endpoint [String] e.g. "inproc://my-endpoint"
|
|
53
|
-
# @param engine [Engine] the connecting engine
|
|
54
|
-
# @return [void]
|
|
55
|
-
#
|
|
56
|
-
def connect(endpoint, engine)
|
|
57
|
-
bound_engine = @mutex.synchronize { @registry[endpoint] }
|
|
58
|
-
|
|
59
|
-
unless bound_engine
|
|
60
|
-
# Endpoint not bound yet. Wait with timeout derived from
|
|
61
|
-
# reconnect_interval. If it doesn't appear, silently return —
|
|
62
|
-
# matching ZMQ 4.x behavior where inproc connect to an
|
|
63
|
-
# unbound endpoint succeeds but messages go nowhere.
|
|
64
|
-
# A background task retries periodically.
|
|
65
|
-
ri = engine.options.reconnect_interval
|
|
66
|
-
timeout = ri.is_a?(Range) ? ri.begin : ri
|
|
67
|
-
promise = Async::Promise.new
|
|
68
|
-
@mutex.synchronize { @waiters[endpoint] << promise }
|
|
69
|
-
unless promise.wait?(timeout: timeout)
|
|
70
|
-
@mutex.synchronize { @waiters[endpoint].delete(promise) }
|
|
71
|
-
start_connect_retry(endpoint, engine)
|
|
72
|
-
return
|
|
73
|
-
end
|
|
74
|
-
bound_engine = @mutex.synchronize { @registry[endpoint] }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
establish_link(engine, bound_engine, endpoint)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Removes a bound endpoint from the registry.
|
|
82
|
-
#
|
|
83
|
-
# @param endpoint [String]
|
|
84
|
-
# @return [void]
|
|
85
|
-
#
|
|
86
|
-
def unbind(endpoint)
|
|
87
|
-
@mutex.synchronize { @registry.delete(endpoint) }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# Resets the registry. Used in tests.
|
|
92
|
-
#
|
|
93
|
-
# @return [void]
|
|
94
|
-
#
|
|
95
|
-
def reset!
|
|
96
|
-
@mutex.synchronize do
|
|
97
|
-
@registry.clear
|
|
98
|
-
@waiters.clear
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# Wires up a client-server inproc pipe pair after validating
|
|
107
|
-
# that the two socket types are compatible.
|
|
108
|
-
#
|
|
109
|
-
# @param client_engine [Engine] the connecting engine
|
|
110
|
-
# @param server_engine [Engine] the bound engine
|
|
111
|
-
# @param endpoint [String] the inproc endpoint name
|
|
112
|
-
#
|
|
113
|
-
def establish_link(client_engine, server_engine, endpoint)
|
|
114
|
-
client_type = client_engine.socket_type
|
|
115
|
-
server_type = server_engine.socket_type
|
|
116
|
-
|
|
117
|
-
unless ZMTP::VALID_PEERS[client_type]&.include?(server_type)
|
|
118
|
-
raise ProtocolError,
|
|
119
|
-
"incompatible socket types: #{client_type} cannot connect to #{server_type}"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Only PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
|
|
123
|
-
# over inproc. All other types use only the direct recv queue
|
|
124
|
-
# bypass for data, so no internal queues are needed.
|
|
125
|
-
needs_commands = COMMAND_TYPES.include?(client_type) ||
|
|
126
|
-
COMMAND_TYPES.include?(server_type)
|
|
127
|
-
|
|
128
|
-
if needs_commands
|
|
129
|
-
a_to_b = Async::Queue.new
|
|
130
|
-
b_to_a = Async::Queue.new
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
client_pipe = DirectPipe.new(
|
|
134
|
-
send_queue: needs_commands ? a_to_b : nil,
|
|
135
|
-
receive_queue: needs_commands ? b_to_a : nil,
|
|
136
|
-
peer_identity: server_engine.options.identity,
|
|
137
|
-
peer_type: server_type.to_s,
|
|
138
|
-
)
|
|
139
|
-
server_pipe = DirectPipe.new(
|
|
140
|
-
send_queue: needs_commands ? b_to_a : nil,
|
|
141
|
-
receive_queue: needs_commands ? a_to_b : nil,
|
|
142
|
-
peer_identity: client_engine.options.identity,
|
|
143
|
-
peer_type: client_type.to_s,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
client_pipe.peer = server_pipe
|
|
147
|
-
server_pipe.peer = client_pipe
|
|
148
|
-
|
|
149
|
-
client_engine.connection_ready(client_pipe, endpoint: endpoint)
|
|
150
|
-
server_engine.connection_ready(server_pipe, endpoint: endpoint)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
# Spawns a background task that periodically retries
|
|
155
|
-
# #establish_link until the endpoint appears in the registry.
|
|
156
|
-
#
|
|
157
|
-
# @param endpoint [String] the inproc endpoint name
|
|
158
|
-
# @param engine [Engine] the connecting engine
|
|
159
|
-
#
|
|
160
|
-
def start_connect_retry(endpoint, engine)
|
|
161
|
-
Reactor.spawn_pump(annotation: "reconnect") do
|
|
162
|
-
ri = engine.options.reconnect_interval
|
|
163
|
-
ivl = ri.is_a?(Range) ? ri.begin : ri
|
|
164
|
-
loop do
|
|
165
|
-
sleep ivl
|
|
166
|
-
bound_engine = @mutex.synchronize { @registry[endpoint] }
|
|
167
|
-
if bound_engine
|
|
168
|
-
establish_link(engine, bound_engine, endpoint)
|
|
169
|
-
break
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# A bound inproc endpoint handle.
|
|
177
|
-
#
|
|
178
|
-
class Listener
|
|
179
|
-
# @return [String] the bound endpoint
|
|
180
|
-
#
|
|
181
|
-
attr_reader :endpoint
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# @param endpoint [String]
|
|
185
|
-
#
|
|
186
|
-
def initialize(endpoint)
|
|
187
|
-
@endpoint = endpoint
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Stops the listener by removing it from the registry.
|
|
192
|
-
#
|
|
193
|
-
# @return [void]
|
|
194
|
-
#
|
|
195
|
-
def stop
|
|
196
|
-
Inproc.unbind(@endpoint)
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# A direct in-process pipe that transfers Ruby arrays through queues.
|
|
201
|
-
#
|
|
202
|
-
# Implements the same interface as Connection so routing strategies
|
|
203
|
-
# can use it transparently.
|
|
204
|
-
#
|
|
205
|
-
# When a routing strategy sets {#direct_recv_queue} on a pipe,
|
|
206
|
-
# {#send_message} enqueues directly into the peer's recv queue,
|
|
207
|
-
# bypassing the intermediate pipe queues and the recv pump task.
|
|
208
|
-
# This reduces inproc from 3 queue hops to 2 (send_queue →
|
|
209
|
-
# recv_queue), eliminating the internal pipe queue in between.
|
|
210
|
-
#
|
|
211
|
-
class DirectPipe
|
|
212
|
-
# @return [String] peer's socket type
|
|
213
|
-
#
|
|
214
|
-
attr_reader :peer_socket_type
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# @return [String] peer's identity
|
|
218
|
-
#
|
|
219
|
-
attr_reader :peer_identity
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
# @return [DirectPipe, nil] the other end of this pipe pair
|
|
223
|
-
#
|
|
224
|
-
attr_accessor :peer
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# @return [Async::LimitedQueue, nil] when set, {#send_message}
|
|
228
|
-
# enqueues directly here instead of using the internal queue
|
|
229
|
-
#
|
|
230
|
-
attr_reader :direct_recv_queue
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
# @return [Proc, nil] optional transform applied before
|
|
234
|
-
# enqueuing into {#direct_recv_queue}
|
|
235
|
-
#
|
|
236
|
-
attr_accessor :direct_recv_transform
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
# @param send_queue [Async::Queue, nil] outgoing command queue
|
|
240
|
-
# (nil for non-PUB/SUB types that don't exchange commands)
|
|
241
|
-
# @param receive_queue [Async::Queue, nil] incoming command queue
|
|
242
|
-
# @param peer_identity [String]
|
|
243
|
-
# @param peer_type [String]
|
|
244
|
-
#
|
|
245
|
-
def initialize(send_queue: nil, receive_queue: nil, peer_identity:, peer_type:)
|
|
246
|
-
@send_queue = send_queue
|
|
247
|
-
@receive_queue = receive_queue
|
|
248
|
-
@peer_identity = peer_identity || "".b
|
|
249
|
-
@peer_socket_type = peer_type
|
|
250
|
-
@closed = false
|
|
251
|
-
@peer = nil
|
|
252
|
-
@direct_recv_queue = nil
|
|
253
|
-
@direct_recv_transform = nil
|
|
254
|
-
@pending_direct = nil
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# Sets the direct recv queue. Drains any messages that were
|
|
259
|
-
# buffered before the queue was available.
|
|
260
|
-
#
|
|
261
|
-
def direct_recv_queue=(queue)
|
|
262
|
-
@direct_recv_queue = queue
|
|
263
|
-
if queue && @pending_direct
|
|
264
|
-
@pending_direct.each do |msg|
|
|
265
|
-
queue.enqueue(msg)
|
|
266
|
-
end
|
|
267
|
-
@pending_direct = nil
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# Sends a multi-frame message.
|
|
273
|
-
#
|
|
274
|
-
# When {#direct_recv_queue} is set (inproc fast path), the
|
|
275
|
-
# message is delivered directly to the peer's recv queue,
|
|
276
|
-
# skipping the internal pipe queues and the recv pump.
|
|
277
|
-
#
|
|
278
|
-
# @param parts [Array<String>]
|
|
279
|
-
# @return [void]
|
|
280
|
-
#
|
|
281
|
-
def send_message(parts)
|
|
282
|
-
raise IOError, "closed" if @closed
|
|
283
|
-
if @direct_recv_queue
|
|
284
|
-
msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
|
|
285
|
-
@direct_recv_queue.enqueue(msg)
|
|
286
|
-
elsif @send_queue
|
|
287
|
-
@send_queue.enqueue(parts)
|
|
288
|
-
else
|
|
289
|
-
msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
|
|
290
|
-
(@pending_direct ||= []) << msg
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
alias write_message send_message
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# No-op — inproc has no IO buffer to flush.
|
|
299
|
-
#
|
|
300
|
-
# @return [void]
|
|
301
|
-
#
|
|
302
|
-
def flush = nil
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
# Receives a multi-frame message.
|
|
306
|
-
#
|
|
307
|
-
# @return [Array<String>]
|
|
308
|
-
# @raise [EOFError] if closed
|
|
309
|
-
#
|
|
310
|
-
def receive_message
|
|
311
|
-
msg = @receive_queue.dequeue
|
|
312
|
-
raise EOFError, "connection closed" if msg.nil?
|
|
313
|
-
msg
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
# Sends a command via the internal command queue.
|
|
318
|
-
# Only available for PUB/SUB-family pipes.
|
|
319
|
-
#
|
|
320
|
-
# @param command [Codec::Command]
|
|
321
|
-
# @return [void]
|
|
322
|
-
#
|
|
323
|
-
def send_command(command)
|
|
324
|
-
raise IOError, "closed" if @closed
|
|
325
|
-
@send_queue.enqueue([:command, command])
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
# Reads one command frame from the internal command queue.
|
|
330
|
-
# Used by PUB/XPUB subscription listeners.
|
|
331
|
-
#
|
|
332
|
-
# @return [Codec::Frame]
|
|
333
|
-
#
|
|
334
|
-
def read_frame
|
|
335
|
-
loop do
|
|
336
|
-
item = @receive_queue.dequeue
|
|
337
|
-
raise EOFError, "connection closed" if item.nil?
|
|
338
|
-
if item.is_a?(Array) && item.first == :command
|
|
339
|
-
cmd = item[1]
|
|
340
|
-
return Codec::Frame.new(cmd.to_body, command: true)
|
|
341
|
-
end
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
# Closes this pipe end.
|
|
347
|
-
#
|
|
348
|
-
# @return [void]
|
|
349
|
-
#
|
|
350
|
-
def close
|
|
351
|
-
return if @closed
|
|
352
|
-
@closed = true
|
|
353
|
-
@send_queue&.enqueue(nil) # close sentinel
|
|
354
|
-
end
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "io/stream"
|
|
5
|
-
|
|
6
|
-
module OMQ
|
|
7
|
-
module ZMTP
|
|
8
|
-
module Transport
|
|
9
|
-
# IPC transport using Unix domain sockets.
|
|
10
|
-
#
|
|
11
|
-
# Supports both file-based paths and Linux abstract namespace
|
|
12
|
-
# (paths starting with @).
|
|
13
|
-
#
|
|
14
|
-
module IPC
|
|
15
|
-
class << self
|
|
16
|
-
# Binds an IPC server.
|
|
17
|
-
#
|
|
18
|
-
# @param endpoint [String] e.g. "ipc:///tmp/my.sock" or "ipc://@abstract"
|
|
19
|
-
# @param engine [Engine]
|
|
20
|
-
# @return [Listener]
|
|
21
|
-
#
|
|
22
|
-
def bind(endpoint, engine)
|
|
23
|
-
path = parse_path(endpoint)
|
|
24
|
-
sock_path = to_socket_path(path)
|
|
25
|
-
|
|
26
|
-
# Remove stale socket file for file-based paths
|
|
27
|
-
File.delete(sock_path) if !abstract?(path) && File.exist?(sock_path)
|
|
28
|
-
|
|
29
|
-
server = UNIXServer.new(sock_path)
|
|
30
|
-
|
|
31
|
-
accept_task = Reactor.spawn_pump(annotation: "ipc accept #{endpoint}") do
|
|
32
|
-
loop do
|
|
33
|
-
client = server.accept
|
|
34
|
-
Async::Task.current.defer_stop do
|
|
35
|
-
engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: endpoint)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
rescue IOError
|
|
39
|
-
# server closed
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
Listener.new(endpoint, server, accept_task, path)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Connects to an IPC endpoint.
|
|
46
|
-
#
|
|
47
|
-
# @param endpoint [String]
|
|
48
|
-
# @param engine [Engine]
|
|
49
|
-
# @return [void]
|
|
50
|
-
#
|
|
51
|
-
def connect(endpoint, engine)
|
|
52
|
-
path = parse_path(endpoint)
|
|
53
|
-
sock_path = to_socket_path(path)
|
|
54
|
-
sock = UNIXSocket.new(sock_path)
|
|
55
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# Extracts path from "ipc://path".
|
|
61
|
-
#
|
|
62
|
-
def parse_path(endpoint)
|
|
63
|
-
endpoint.sub(%r{\Aipc://}, "")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Converts @ prefix to \0 for abstract namespace.
|
|
67
|
-
#
|
|
68
|
-
def to_socket_path(path)
|
|
69
|
-
if abstract?(path)
|
|
70
|
-
"\0#{path[1..]}"
|
|
71
|
-
else
|
|
72
|
-
path
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# @return [Boolean] true if abstract namespace path
|
|
77
|
-
#
|
|
78
|
-
def abstract?(path)
|
|
79
|
-
path.start_with?("@")
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# A bound IPC listener.
|
|
84
|
-
#
|
|
85
|
-
class Listener
|
|
86
|
-
# @return [String] the endpoint
|
|
87
|
-
#
|
|
88
|
-
attr_reader :endpoint
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# @param endpoint [String] the IPC endpoint URI
|
|
92
|
-
# @param server [UNIXServer]
|
|
93
|
-
# @param accept_task [#stop] the accept loop handle
|
|
94
|
-
# @param path [String] filesystem or abstract namespace path
|
|
95
|
-
#
|
|
96
|
-
def initialize(endpoint, server, accept_task, path)
|
|
97
|
-
@endpoint = endpoint
|
|
98
|
-
@server = server
|
|
99
|
-
@accept_task = accept_task
|
|
100
|
-
@path = path
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Stops the listener.
|
|
105
|
-
#
|
|
106
|
-
def stop
|
|
107
|
-
@accept_task.stop
|
|
108
|
-
@server.close rescue nil
|
|
109
|
-
# Clean up socket file for file-based paths
|
|
110
|
-
unless @path.start_with?("@")
|
|
111
|
-
File.delete(@path) rescue nil
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "uri"
|
|
5
|
-
require "io/stream"
|
|
6
|
-
|
|
7
|
-
module OMQ
|
|
8
|
-
module ZMTP
|
|
9
|
-
module Transport
|
|
10
|
-
# TCP transport using Ruby sockets with Async.
|
|
11
|
-
#
|
|
12
|
-
module TCP
|
|
13
|
-
class << self
|
|
14
|
-
# Binds a TCP server.
|
|
15
|
-
#
|
|
16
|
-
# @param endpoint [String] e.g. "tcp://127.0.0.1:5555" or "tcp://*:0"
|
|
17
|
-
# @param engine [Engine]
|
|
18
|
-
# @return [Listener]
|
|
19
|
-
#
|
|
20
|
-
def bind(endpoint, engine)
|
|
21
|
-
host, port = parse_endpoint(endpoint)
|
|
22
|
-
host = "0.0.0.0" if host == "*"
|
|
23
|
-
|
|
24
|
-
addrs = Addrinfo.getaddrinfo(host, port, nil, :STREAM, nil, ::Socket::AI_PASSIVE)
|
|
25
|
-
raise ::Socket::ResolutionError, "no addresses for #{host}" if addrs.empty?
|
|
26
|
-
|
|
27
|
-
servers = []
|
|
28
|
-
accept_tasks = []
|
|
29
|
-
actual_port = nil
|
|
30
|
-
|
|
31
|
-
addrs.each do |addr|
|
|
32
|
-
server = TCPServer.new(addr.ip_address, actual_port || port)
|
|
33
|
-
actual_port ||= server.local_address.ip_port
|
|
34
|
-
servers << server
|
|
35
|
-
|
|
36
|
-
ip = addr.ip_address
|
|
37
|
-
host_part = ip.include?(":") ? "[#{ip}]" : ip
|
|
38
|
-
resolved = "tcp://#{host_part}:#{actual_port}"
|
|
39
|
-
|
|
40
|
-
accept_tasks << Reactor.spawn_pump(annotation: "tcp accept #{resolved}") do
|
|
41
|
-
loop do
|
|
42
|
-
client = server.accept
|
|
43
|
-
Async::Task.current.defer_stop do
|
|
44
|
-
engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: resolved)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
rescue IOError
|
|
48
|
-
# server closed
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
host_part = host.include?(":") ? "[#{host}]" : host
|
|
53
|
-
resolved = "tcp://#{host_part}:#{actual_port}"
|
|
54
|
-
Listener.new(resolved, servers, accept_tasks, actual_port)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Connects to a TCP endpoint.
|
|
58
|
-
#
|
|
59
|
-
# @param endpoint [String] e.g. "tcp://127.0.0.1:5555"
|
|
60
|
-
# @param engine [Engine]
|
|
61
|
-
# @return [void]
|
|
62
|
-
#
|
|
63
|
-
def connect(endpoint, engine)
|
|
64
|
-
host, port = parse_endpoint(endpoint)
|
|
65
|
-
sock = TCPSocket.new(host, port)
|
|
66
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
# Parses a TCP endpoint URI into host and port.
|
|
72
|
-
#
|
|
73
|
-
# @param endpoint [String]
|
|
74
|
-
# @return [Array(String, Integer)]
|
|
75
|
-
#
|
|
76
|
-
def parse_endpoint(endpoint)
|
|
77
|
-
uri = URI.parse(endpoint)
|
|
78
|
-
[uri.hostname, uri.port]
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# A bound TCP listener.
|
|
83
|
-
#
|
|
84
|
-
class Listener
|
|
85
|
-
# @return [String] resolved endpoint with actual port
|
|
86
|
-
#
|
|
87
|
-
attr_reader :endpoint
|
|
88
|
-
|
|
89
|
-
# @return [Integer] bound port
|
|
90
|
-
#
|
|
91
|
-
attr_reader :port
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# @param endpoint [String] resolved endpoint URI
|
|
95
|
-
# @param server [TCPServer]
|
|
96
|
-
# @param accept_task [#stop] the accept loop handle
|
|
97
|
-
# @param port [Integer] bound port number
|
|
98
|
-
#
|
|
99
|
-
def initialize(endpoint, servers, accept_tasks, port)
|
|
100
|
-
@endpoint = endpoint
|
|
101
|
-
@servers = servers
|
|
102
|
-
@accept_tasks = accept_tasks
|
|
103
|
-
@port = port
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Stops the listener.
|
|
108
|
-
#
|
|
109
|
-
def stop
|
|
110
|
-
@accept_tasks.each(&:stop)
|
|
111
|
-
@servers.each { |s| s.close rescue nil }
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|