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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -0
  3. data/README.md +28 -3
  4. data/lib/omq/channel.rb +5 -5
  5. data/lib/omq/client_server.rb +10 -10
  6. data/lib/omq/engine.rb +702 -0
  7. data/lib/omq/options.rb +48 -0
  8. data/lib/omq/pair.rb +4 -4
  9. data/lib/omq/peer.rb +5 -5
  10. data/lib/omq/pub_sub.rb +18 -18
  11. data/lib/omq/push_pull.rb +6 -6
  12. data/lib/omq/queue_interface.rb +73 -0
  13. data/lib/omq/radio_dish.rb +6 -6
  14. data/lib/omq/reactor.rb +128 -0
  15. data/lib/omq/readable.rb +44 -0
  16. data/lib/omq/req_rep.rb +8 -8
  17. data/lib/omq/router_dealer.rb +8 -8
  18. data/lib/omq/routing/channel.rb +83 -0
  19. data/lib/omq/routing/client.rb +56 -0
  20. data/lib/omq/routing/dealer.rb +57 -0
  21. data/lib/omq/routing/dish.rb +78 -0
  22. data/lib/omq/routing/fan_out.rb +140 -0
  23. data/lib/omq/routing/gather.rb +46 -0
  24. data/lib/omq/routing/pair.rb +86 -0
  25. data/lib/omq/routing/peer.rb +101 -0
  26. data/lib/omq/routing/pub.rb +60 -0
  27. data/lib/omq/routing/pull.rb +46 -0
  28. data/lib/omq/routing/push.rb +81 -0
  29. data/lib/omq/routing/radio.rb +150 -0
  30. data/lib/omq/routing/rep.rb +101 -0
  31. data/lib/omq/routing/req.rb +65 -0
  32. data/lib/omq/routing/round_robin.rb +168 -0
  33. data/lib/omq/routing/router.rb +110 -0
  34. data/lib/omq/routing/scatter.rb +82 -0
  35. data/lib/omq/routing/server.rb +101 -0
  36. data/lib/omq/routing/sub.rb +78 -0
  37. data/lib/omq/routing/xpub.rb +72 -0
  38. data/lib/omq/routing/xsub.rb +83 -0
  39. data/lib/omq/routing.rb +66 -0
  40. data/lib/omq/scatter_gather.rb +8 -8
  41. data/lib/omq/single_frame.rb +18 -0
  42. data/lib/omq/socket.rb +32 -11
  43. data/lib/omq/transport/inproc.rb +355 -0
  44. data/lib/omq/transport/ipc.rb +117 -0
  45. data/lib/omq/transport/tcp.rb +111 -0
  46. data/lib/omq/transport/tls.rb +146 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq/writable.rb +66 -0
  49. data/lib/omq.rb +64 -4
  50. metadata +34 -33
  51. data/lib/omq/zmtp/engine.rb +0 -551
  52. data/lib/omq/zmtp/options.rb +0 -48
  53. data/lib/omq/zmtp/reactor.rb +0 -131
  54. data/lib/omq/zmtp/readable.rb +0 -29
  55. data/lib/omq/zmtp/routing/channel.rb +0 -81
  56. data/lib/omq/zmtp/routing/client.rb +0 -56
  57. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  58. data/lib/omq/zmtp/routing/dish.rb +0 -80
  59. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  60. data/lib/omq/zmtp/routing/gather.rb +0 -48
  61. data/lib/omq/zmtp/routing/pair.rb +0 -84
  62. data/lib/omq/zmtp/routing/peer.rb +0 -100
  63. data/lib/omq/zmtp/routing/pub.rb +0 -62
  64. data/lib/omq/zmtp/routing/pull.rb +0 -48
  65. data/lib/omq/zmtp/routing/push.rb +0 -80
  66. data/lib/omq/zmtp/routing/radio.rb +0 -139
  67. data/lib/omq/zmtp/routing/rep.rb +0 -101
  68. data/lib/omq/zmtp/routing/req.rb +0 -65
  69. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  70. data/lib/omq/zmtp/routing/router.rb +0 -109
  71. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  72. data/lib/omq/zmtp/routing/server.rb +0 -100
  73. data/lib/omq/zmtp/routing/sub.rb +0 -80
  74. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  75. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  76. data/lib/omq/zmtp/routing.rb +0 -65
  77. data/lib/omq/zmtp/single_frame.rb +0 -20
  78. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  79. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  80. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  81. data/lib/omq/zmtp/writable.rb +0 -61
  82. data/lib/omq/zmtp.rb +0 -81
@@ -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