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