omq 0.22.1 → 0.23.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -0
  3. data/README.md +17 -21
  4. data/lib/omq/channel.rb +35 -0
  5. data/lib/omq/client_server.rb +72 -0
  6. data/lib/omq/constants.rb +68 -0
  7. data/lib/omq/engine/connection_lifecycle.rb +11 -3
  8. data/lib/omq/engine/heartbeat.rb +2 -3
  9. data/lib/omq/engine/maintenance.rb +4 -5
  10. data/lib/omq/engine/reconnect.rb +12 -11
  11. data/lib/omq/engine/recv_pump.rb +3 -4
  12. data/lib/omq/engine/socket_lifecycle.rb +26 -9
  13. data/lib/omq/engine.rb +196 -85
  14. data/lib/omq/peer.rb +49 -0
  15. data/lib/omq/pub_sub.rb +2 -2
  16. data/lib/omq/radio_dish.rb +122 -0
  17. data/lib/omq/reactor.rb +14 -5
  18. data/lib/omq/routing/channel.rb +110 -0
  19. data/lib/omq/routing/client.rb +70 -0
  20. data/lib/omq/routing/conn_send_pump.rb +4 -7
  21. data/lib/omq/routing/dealer.rb +1 -13
  22. data/lib/omq/routing/dish.rb +94 -0
  23. data/lib/omq/routing/fan_out.rb +5 -9
  24. data/lib/omq/routing/gather.rb +60 -0
  25. data/lib/omq/routing/pair.rb +3 -22
  26. data/lib/omq/routing/peer.rb +95 -0
  27. data/lib/omq/routing/pub.rb +0 -11
  28. data/lib/omq/routing/pull.rb +1 -13
  29. data/lib/omq/routing/push.rb +1 -10
  30. data/lib/omq/routing/radio.rb +187 -0
  31. data/lib/omq/routing/rep.rb +3 -17
  32. data/lib/omq/routing/req.rb +4 -16
  33. data/lib/omq/routing/round_robin.rb +11 -15
  34. data/lib/omq/routing/router.rb +3 -17
  35. data/lib/omq/routing/scatter.rb +77 -0
  36. data/lib/omq/routing/server.rb +90 -0
  37. data/lib/omq/routing/sub.rb +1 -13
  38. data/lib/omq/routing/xpub.rb +0 -11
  39. data/lib/omq/routing/xsub.rb +6 -23
  40. data/lib/omq/scatter_gather.rb +56 -0
  41. data/lib/omq/socket.rb +8 -23
  42. data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
  43. data/lib/omq/transport/inproc.rb +11 -3
  44. data/lib/omq/transport/ipc.rb +41 -13
  45. data/lib/omq/transport/tcp.rb +59 -23
  46. data/lib/omq/transport/udp.rb +281 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq.rb +9 -64
  49. metadata +16 -2
  50. data/lib/omq/monitor_event.rb +0 -16
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module Routing
7
+ # SERVER socket routing: identity-based routing with auto-generated
8
+ # 4-byte routing IDs.
9
+ #
10
+ # Prepends routing ID on receive. Strips routing ID on send and
11
+ # routes to the identified connection.
12
+ #
13
+ class Server
14
+ # @return [Async::LimitedQueue]
15
+ #
16
+ attr_reader :recv_queue
17
+
18
+
19
+ # @param engine [Engine]
20
+ #
21
+ def initialize(engine)
22
+ @engine = engine
23
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
24
+ @connections_by_routing_id = {}
25
+ @routing_id_by_connection = {}
26
+ @conn_queues = {}
27
+ end
28
+
29
+
30
+ # Dequeues the next received message. Blocks until one is available.
31
+ #
32
+ # @return [Array<String>, nil]
33
+ #
34
+ def dequeue_recv
35
+ @recv_queue.dequeue
36
+ end
37
+
38
+
39
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel.
40
+ #
41
+ # @return [void]
42
+ #
43
+ def unblock_recv
44
+ @recv_queue.enqueue(nil)
45
+ end
46
+
47
+
48
+ # @param connection [Connection]
49
+ #
50
+ def connection_added(connection)
51
+ routing_id = SecureRandom.bytes(4)
52
+ @connections_by_routing_id[routing_id] = connection
53
+ @routing_id_by_connection[connection] = routing_id
54
+
55
+ @engine.start_recv_pump(connection, @recv_queue) { |msg| [routing_id, *msg] }
56
+
57
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
58
+ @conn_queues[connection] = q
59
+ ConnSendPump.start(@engine, connection, q)
60
+ end
61
+
62
+
63
+ # @param connection [Connection]
64
+ #
65
+ def connection_removed(connection)
66
+ routing_id = @routing_id_by_connection.delete(connection)
67
+ @connections_by_routing_id.delete(routing_id) if routing_id
68
+ @conn_queues.delete(connection)
69
+ end
70
+
71
+
72
+ # @param parts [Array<String>]
73
+ #
74
+ def enqueue(parts)
75
+ routing_id = parts.first
76
+ conn = @connections_by_routing_id[routing_id]
77
+ return unless conn
78
+ @conn_queues[conn]&.enqueue(parts[1..])
79
+ end
80
+
81
+
82
+ # True when all per-connection send queues are empty.
83
+ #
84
+ def send_queues_drained?
85
+ @conn_queues.values.all?(&:empty?)
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -20,7 +20,6 @@ module OMQ
20
20
  @connections = Set.new
21
21
  @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
22
22
  @subscriptions = Set.new
23
- @tasks = []
24
23
  end
25
24
 
26
25
 
@@ -51,8 +50,7 @@ module OMQ
51
50
  send_subscribe(connection, prefix)
52
51
  end
53
52
 
54
- task = @engine.start_recv_pump(connection, @recv_queue)
55
- @tasks << task if task
53
+ @engine.start_recv_pump(connection, @recv_queue)
56
54
  end
57
55
 
58
56
 
@@ -90,16 +88,6 @@ module OMQ
90
88
  end
91
89
 
92
90
 
93
- # Stops all background tasks.
94
- #
95
- # @return [void]
96
- #
97
- def stop
98
- @tasks.each(&:stop)
99
- @tasks.clear
100
- end
101
-
102
-
103
91
  private
104
92
 
105
93
 
@@ -24,7 +24,6 @@ module OMQ
24
24
  def initialize(engine)
25
25
  @engine = engine
26
26
  @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
27
- @tasks = []
28
27
 
29
28
  init_fan_out(engine)
30
29
  end
@@ -68,16 +67,6 @@ module OMQ
68
67
  end
69
68
 
70
69
 
71
- # Stops all background tasks.
72
- #
73
- # @return [void]
74
- #
75
- def stop
76
- @tasks.each(&:stop)
77
- @tasks.clear
78
- end
79
-
80
-
81
70
  private
82
71
 
83
72
 
@@ -18,12 +18,10 @@ module OMQ
18
18
  # @param engine [Engine]
19
19
  #
20
20
  def initialize(engine)
21
- @engine = engine
22
- @connections = Set.new
23
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
24
- @conn_queues = {} # connection => per-connection send queue
25
- @conn_send_tasks = {} # connection => send pump task
26
- @tasks = []
21
+ @engine = engine
22
+ @connections = Set.new
23
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
24
+ @conn_queues = {}
27
25
  end
28
26
 
29
27
 
@@ -50,8 +48,7 @@ module OMQ
50
48
  def connection_added(connection)
51
49
  @connections << connection
52
50
 
53
- task = @engine.start_recv_pump(connection, @recv_queue)
54
- @tasks << task if task
51
+ @engine.start_recv_pump(connection, @recv_queue)
55
52
 
56
53
  q = Routing.build_queue(@engine.options.send_hwm, :block)
57
54
  @conn_queues[connection] = q
@@ -64,7 +61,6 @@ module OMQ
64
61
  def connection_removed(connection)
65
62
  @connections.delete(connection)
66
63
  @conn_queues.delete(connection)
67
- @conn_send_tasks.delete(connection)&.stop
68
64
  end
69
65
 
70
66
 
@@ -79,16 +75,6 @@ module OMQ
79
75
  end
80
76
 
81
77
 
82
- # Stops all background tasks.
83
- #
84
- # @return [void]
85
- #
86
- def stop
87
- @tasks.each(&:stop)
88
- @tasks.clear
89
- end
90
-
91
-
92
78
  # @return [Boolean] true when all per-connection send queues are empty
93
79
  #
94
80
  def send_queues_drained?
@@ -100,7 +86,7 @@ module OMQ
100
86
 
101
87
 
102
88
  def start_conn_send_pump(conn, q)
103
- task = @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
89
+ @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
104
90
  loop do
105
91
  parts = q.dequeue
106
92
  frame = parts.first&.b
@@ -128,9 +114,6 @@ module OMQ
128
114
  end
129
115
  end
130
116
  end
131
-
132
- @conn_send_tasks[conn] = task
133
- @tasks << task
134
117
  end
135
118
 
136
119
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OMQ SCATTER/GATHER socket types (ZeroMQ RFC 49).
4
+ #
5
+ # Not loaded by +require "omq"+; opt in with:
6
+ #
7
+ # require "omq/scatter_gather"
8
+
9
+ require "omq"
10
+ require_relative "routing/scatter"
11
+ require_relative "routing/gather"
12
+
13
+ module OMQ
14
+ # Pipeline sender socket that round-robins to GATHER peers (ZeroMQ RFC 49).
15
+ class SCATTER < Socket
16
+ include Writable
17
+ include SingleFrame
18
+
19
+ # Creates a new SCATTER socket.
20
+ #
21
+ # @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
22
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
23
+ # @param send_hwm [Integer, nil] send high-water mark
24
+ # @param send_timeout [Integer, nil] send timeout in seconds
25
+ # @param backend [Object, nil] optional transport backend
26
+ def initialize(endpoints = nil, linger: Float::INFINITY, send_hwm: nil, send_timeout: nil, backend: nil)
27
+ init_engine(:SCATTER, send_hwm: send_hwm, send_timeout: send_timeout, backend: backend)
28
+ @options.linger = linger
29
+ attach_endpoints(endpoints, default: :connect)
30
+ end
31
+ end
32
+
33
+
34
+ # Pipeline receiver socket that fair-queues from SCATTER peers (ZeroMQ RFC 49).
35
+ class GATHER < Socket
36
+ include Readable
37
+ include SingleFrame
38
+
39
+ # Creates a new GATHER socket.
40
+ #
41
+ # @param endpoints [String, Array<String>, nil] endpoint(s) to bind to
42
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
43
+ # @param recv_hwm [Integer, nil] receive high-water mark
44
+ # @param recv_timeout [Integer, nil] receive timeout in seconds
45
+ # @param backend [Object, nil] optional transport backend
46
+ def initialize(endpoints = nil, linger: Float::INFINITY, recv_hwm: nil, recv_timeout: nil, backend: nil)
47
+ init_engine(:GATHER, recv_hwm: recv_hwm, recv_timeout: recv_timeout, backend: backend)
48
+ @options.linger = linger
49
+ attach_endpoints(endpoints, default: :bind)
50
+ end
51
+ end
52
+
53
+
54
+ Routing.register(:SCATTER, Routing::Scatter)
55
+ Routing.register(:GATHER, Routing::Gather)
56
+ end
data/lib/omq/socket.rb CHANGED
@@ -35,11 +35,6 @@ module OMQ
35
35
  attr_reader :options
36
36
 
37
37
 
38
- # @return [Integer, nil] last auto-selected TCP port
39
- #
40
- attr_reader :last_tcp_port
41
-
42
-
43
38
  # @return [Engine] the socket's engine. Exposed for peer tooling
44
39
  # (omq-cli, omq-ffi, omq-ractor) that needs to reach into the
45
40
  # socket's internals — not part of the stable user API.
@@ -109,14 +104,11 @@ module OMQ
109
104
  # can coordinate teardown with their own Async tree. Only the
110
105
  # *first* bind/connect call captures the parent — subsequent
111
106
  # calls ignore the kwarg.
112
- # @return [void]
107
+ # @return [URI::Generic] resolved endpoint URI (with auto-selected port for "tcp://host:0")
113
108
  #
114
- def bind(endpoint, parent: nil)
109
+ def bind(endpoint, parent: nil, **opts)
115
110
  ensure_parent_task(parent: parent)
116
- Reactor.run do
117
- @engine.bind(endpoint) # TODO: use timeout?
118
- @last_tcp_port = @engine.last_tcp_port
119
- end
111
+ Reactor.run { @engine.bind(endpoint, **opts) } # TODO: use timeout?
120
112
  end
121
113
 
122
114
 
@@ -124,11 +116,11 @@ module OMQ
124
116
  #
125
117
  # @param endpoint [String]
126
118
  # @param parent [#async, nil] see {#bind}.
127
- # @return [void]
119
+ # @return [URI::Generic] parsed endpoint URI
128
120
  #
129
- def connect(endpoint, parent: nil)
121
+ def connect(endpoint, parent: nil, **opts)
130
122
  ensure_parent_task(parent: parent)
131
- Reactor.run { @engine.connect(endpoint) } # TODO: use timeout?
123
+ Reactor.run { @engine.connect(endpoint, **opts) } # TODO: use timeout?
132
124
  end
133
125
 
134
126
 
@@ -152,13 +144,6 @@ module OMQ
152
144
  end
153
145
 
154
146
 
155
- # @return [String, nil] last bound endpoint
156
- #
157
- def last_endpoint
158
- @engine.last_endpoint
159
- end
160
-
161
-
162
147
  # @return [Async::Promise] resolves when first peer completes handshake
163
148
  def peer_connected
164
149
  @engine.peer_connected
@@ -167,7 +152,7 @@ module OMQ
167
152
 
168
153
  # @return [Async::Promise] resolves when first subscriber joins (PUB/XPUB only)
169
154
  def subscriber_joined
170
- @engine.routing.subscriber_joined
155
+ @engine.subscriber_joined
171
156
  end
172
157
 
173
158
 
@@ -285,7 +270,7 @@ module OMQ
285
270
  # @return [String]
286
271
  #
287
272
  def inspect
288
- format("#<%s last_endpoint=%p>", self.class, last_endpoint)
273
+ format("#<%s bound=%p>", self.class, @engine.listeners.keys)
289
274
  end
290
275
 
291
276
 
@@ -50,12 +50,6 @@ module OMQ
50
50
  attr_reader :direct_recv_queue
51
51
 
52
52
 
53
- # @return [Proc, nil] optional transform applied before
54
- # enqueuing into {#direct_recv_queue}
55
- #
56
- attr_accessor :direct_recv_transform
57
-
58
-
59
53
  # @param send_queue [Async::Queue, nil] outgoing command queue
60
54
  # (nil for non-PUB/SUB types that don't exchange commands)
61
55
  # @param receive_queue [Async::Queue, nil] incoming command queue
@@ -75,18 +69,26 @@ module OMQ
75
69
  end
76
70
 
77
71
 
78
- # Sets the direct recv queue. Drains any messages that were
79
- # buffered before the queue was available.
72
+ # Wires up the direct recv fast-path. Called once by the recv
73
+ # pump when the receiving side of an inproc pipe pair is set up.
74
+ # After this, peer-side {#send_message} calls enqueue straight
75
+ # into +queue+ instead of hopping through the intermediate pipe
76
+ # queue and a recv pump fiber.
80
77
  #
81
- # @param queue [Async::LimitedQueue, nil]
78
+ # Drains any messages the peer buffered into +@pending_direct+
79
+ # before the queue was available.
80
+ #
81
+ # @param queue [Async::LimitedQueue]
82
+ # @param transform [Proc, nil] optional per-message transform
82
83
  # @return [void]
83
84
  #
84
- def direct_recv_queue=(queue)
85
- @direct_recv_queue = queue
86
- if queue && @pending_direct
87
- @pending_direct.each { |msg| queue.enqueue(msg) }
88
- @pending_direct = nil
89
- end
85
+ def wire_direct_recv(queue, transform)
86
+ @direct_recv_transform = transform
87
+ @direct_recv_queue = queue
88
+ return unless @pending_direct
89
+
90
+ @pending_direct.each { |msg| queue.enqueue(msg) }
91
+ @pending_direct = nil
90
92
  end
91
93
 
92
94
 
@@ -15,6 +15,9 @@ module OMQ
15
15
  # prevent shared mutable state without copying.
16
16
  #
17
17
  module Inproc
18
+ Engine.transports["inproc"] = self
19
+
20
+
18
21
  # Socket types that exchange commands (SUBSCRIBE/CANCEL) over inproc.
19
22
  #
20
23
  COMMAND_TYPES = %i[PUB SUB XPUB XSUB RADIO DISH].freeze
@@ -28,14 +31,19 @@ module OMQ
28
31
 
29
32
 
30
33
  class << self
31
- # Binds an engine to an inproc endpoint.
34
+ # @return [Hash{String => Engine}] bound inproc endpoints
35
+ #
36
+ attr_reader :registry
37
+
38
+
39
+ # Creates a bound inproc listener.
32
40
  #
33
41
  # @param endpoint [String] e.g. "inproc://my-endpoint"
34
42
  # @param engine [Engine] the owning engine
35
43
  # @return [Listener]
36
44
  # @raise [ArgumentError] if endpoint is already bound
37
45
  #
38
- def bind(endpoint, engine)
46
+ def listener(endpoint, engine, **)
39
47
  @mutex.synchronize do
40
48
  if @registry.key?(endpoint)
41
49
  raise ArgumentError, "endpoint already bound: #{endpoint}"
@@ -57,7 +65,7 @@ module OMQ
57
65
  # @param engine [Engine] the connecting engine
58
66
  # @return [void]
59
67
  #
60
- def connect(endpoint, engine)
68
+ def connect(endpoint, engine, **)
61
69
  bound_engine = @mutex.synchronize { @registry[endpoint] }
62
70
  bound_engine ||= await_bind(endpoint, engine) or return
63
71
  establish_link(engine, bound_engine, endpoint)
@@ -11,14 +11,17 @@ module OMQ
11
11
  # (paths starting with @).
12
12
  #
13
13
  module IPC
14
+ Engine.transports["ipc"] = self
15
+
16
+
14
17
  class << self
15
- # Binds an IPC server.
18
+ # Creates a bound IPC listener.
16
19
  #
17
20
  # @param endpoint [String] e.g. "ipc:///tmp/my.sock" or "ipc://@abstract"
18
21
  # @param engine [Engine]
19
22
  # @return [Listener]
20
23
  #
21
- def bind(endpoint, engine)
24
+ def listener(endpoint, engine, **)
22
25
  path = parse_path(endpoint)
23
26
  sock_path = to_socket_path(path)
24
27
 
@@ -31,18 +34,14 @@ module OMQ
31
34
  end
32
35
 
33
36
 
34
- # Connects to an IPC endpoint.
37
+ # Creates an IPC dialer for an endpoint.
35
38
  #
36
39
  # @param endpoint [String]
37
40
  # @param engine [Engine]
38
- # @return [void]
41
+ # @return [Dialer]
39
42
  #
40
- def connect(endpoint, engine)
41
- path = parse_path(endpoint)
42
- sock_path = to_socket_path(path)
43
- sock = UNIXSocket.new(sock_path)
44
- apply_buffer_sizes(sock, engine.options)
45
- engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
43
+ def dialer(endpoint, engine, **)
44
+ Dialer.new(endpoint, engine)
46
45
  end
47
46
 
48
47
 
@@ -63,9 +62,6 @@ module OMQ
63
62
  end
64
63
 
65
64
 
66
- private
67
-
68
-
69
65
  # Extracts path from "ipc://path".
70
66
  #
71
67
  def parse_path(endpoint)
@@ -92,6 +88,38 @@ module OMQ
92
88
  end
93
89
 
94
90
 
91
+ # An IPC dialer — stateful factory for outgoing connections.
92
+ #
93
+ class Dialer
94
+ # @return [String] the endpoint this dialer connects to
95
+ #
96
+ attr_reader :endpoint
97
+
98
+
99
+ # @param endpoint [String]
100
+ # @param engine [Engine]
101
+ #
102
+ def initialize(endpoint, engine)
103
+ @endpoint = endpoint
104
+ @engine = engine
105
+ end
106
+
107
+
108
+ # Establishes a Unix socket connection to the endpoint.
109
+ #
110
+ # @return [void]
111
+ #
112
+ def connect
113
+ path = IPC.parse_path(@endpoint)
114
+ sock_path = IPC.to_socket_path(path)
115
+ sock = UNIXSocket.new(sock_path)
116
+ IPC.apply_buffer_sizes(sock, @engine.options)
117
+ @engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: @endpoint)
118
+ end
119
+
120
+ end
121
+
122
+
95
123
  # A bound IPC listener.
96
124
  #
97
125
  class Listener
@@ -9,14 +9,17 @@ module OMQ
9
9
  # TCP transport using Ruby sockets with Async.
10
10
  #
11
11
  module TCP
12
+ Engine.transports["tcp"] = self
13
+
14
+
12
15
  class << self
13
- # Binds a TCP server.
16
+ # Creates a bound TCP listener.
14
17
  #
15
18
  # @param endpoint [String] e.g. "tcp://127.0.0.1:5555" or "tcp://*:0"
16
19
  # @param engine [Engine]
17
20
  # @return [Listener]
18
21
  #
19
- def bind(endpoint, engine)
22
+ def listener(endpoint, engine, **)
20
23
  host, port = self.parse_endpoint(endpoint)
21
24
  lookup_host = normalize_bind_host(host)
22
25
 
@@ -34,6 +37,17 @@ module OMQ
34
37
  end
35
38
 
36
39
 
40
+ # Creates a TCP dialer for an endpoint.
41
+ #
42
+ # @param endpoint [String] e.g. "tcp://127.0.0.1:5555"
43
+ # @param engine [Engine]
44
+ # @return [Dialer]
45
+ #
46
+ def dialer(endpoint, engine, **)
47
+ Dialer.new(endpoint, engine)
48
+ end
49
+
50
+
37
51
  # Validates that the endpoint's host can be resolved.
38
52
  #
39
53
  # @param endpoint [String]
@@ -46,21 +60,6 @@ module OMQ
46
60
  end
47
61
 
48
62
 
49
- # Connects to a TCP endpoint.
50
- #
51
- # @param endpoint [String] e.g. "tcp://127.0.0.1:5555"
52
- # @param engine [Engine]
53
- # @return [void]
54
- #
55
- def connect(endpoint, engine)
56
- host, port = self.parse_endpoint(endpoint)
57
- host = normalize_connect_host(host)
58
- sock = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
59
- apply_buffer_sizes(sock, engine.options)
60
- engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
61
- end
62
-
63
-
64
63
  # Normalizes the bind host:
65
64
  # "*" → nil (dual-stack wildcard via AI_PASSIVE)
66
65
  # "" / nil / "localhost" → loopback_host (::1 on IPv6-capable hosts, else 127.0.0.1)
@@ -142,6 +141,42 @@ module OMQ
142
141
  end
143
142
 
144
143
 
144
+ # A TCP dialer — stateful factory for outgoing connections.
145
+ #
146
+ # Created once per {Engine#connect}, stored in +@dialers[endpoint]+.
147
+ # Reconnect calls {#connect} directly — no transport lookup or opts
148
+ # replay needed.
149
+ #
150
+ class Dialer
151
+ # @return [String] the endpoint this dialer connects to
152
+ #
153
+ attr_reader :endpoint
154
+
155
+
156
+ # @param endpoint [String] e.g. "tcp://127.0.0.1:5555"
157
+ # @param engine [Engine]
158
+ #
159
+ def initialize(endpoint, engine)
160
+ @endpoint = endpoint
161
+ @engine = engine
162
+ end
163
+
164
+
165
+ # Establishes a TCP connection to the endpoint.
166
+ #
167
+ # @return [void]
168
+ #
169
+ def connect
170
+ host, port = TCP.parse_endpoint(@endpoint)
171
+ host = TCP.normalize_connect_host(host)
172
+ sock = ::Socket.tcp(host, port, connect_timeout: TCP.connect_timeout(@engine.options))
173
+ TCP.apply_buffer_sizes(sock, @engine.options)
174
+ @engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: @endpoint)
175
+ end
176
+
177
+ end
178
+
179
+
145
180
  # A bound TCP listener.
146
181
  #
147
182
  class Listener
@@ -168,7 +203,7 @@ module OMQ
168
203
  @servers = servers
169
204
  @port = port
170
205
  @engine = engine
171
- @tasks = []
206
+ @barrier = nil
172
207
  end
173
208
 
174
209
 
@@ -179,10 +214,11 @@ module OMQ
179
214
  # @yieldparam io [IO::Stream::Buffered]
180
215
  #
181
216
  def start_accept_loops(parent_task)
182
- @tasks = @servers.map do |server|
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
217
+ @barrier = Async::Barrier.new(parent: parent_task)
218
+
219
+ @servers.each do |server|
220
+ annotation = "tcp accept #{server.local_address.inspect_sockaddr}"
221
+ @barrier.async(transient: true, annotation:) do
186
222
  loop do
187
223
  client, _addr = server.accept
188
224
  TCP.apply_buffer_sizes(client, @engine.options)
@@ -207,7 +243,7 @@ module OMQ
207
243
  # @return [void]
208
244
  #
209
245
  def stop
210
- @tasks.each(&:stop)
246
+ @barrier&.stop
211
247
  @servers.each { |s| s.close rescue nil }
212
248
  end
213
249