omq 0.17.9 → 0.18.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 +54 -0
- data/README.md +7 -5
- data/lib/omq/engine/connection_lifecycle.rb +32 -8
- data/lib/omq/engine/reconnect.rb +4 -1
- data/lib/omq/engine/recv_pump.rb +23 -0
- data/lib/omq/engine/socket_lifecycle.rb +19 -1
- data/lib/omq/engine.rb +14 -1
- data/lib/omq/pair.rb +2 -2
- data/lib/omq/pub_sub.rb +8 -8
- data/lib/omq/push_pull.rb +4 -4
- data/lib/omq/queue_interface.rb +3 -1
- data/lib/omq/reactor.rb +41 -20
- data/lib/omq/readable.rb +3 -1
- data/lib/omq/req_rep.rb +4 -4
- data/lib/omq/router_dealer.rb +4 -4
- data/lib/omq/routing/conn_send_pump.rb +9 -3
- data/lib/omq/routing/dealer.rb +2 -0
- data/lib/omq/routing/fair_queue.rb +14 -3
- data/lib/omq/routing/fan_out.rb +39 -2
- data/lib/omq/routing/req.rb +10 -1
- data/lib/omq/routing.rb +5 -4
- data/lib/omq/socket.rb +44 -58
- data/lib/omq/transport/inproc/direct_pipe.rb +16 -2
- data/lib/omq/transport/inproc.rb +41 -7
- data/lib/omq/transport/ipc.rb +29 -9
- data/lib/omq/transport/tcp.rb +21 -5
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +5 -1
- data/lib/omq.rb +2 -1
- metadata +3 -3
data/lib/omq/router_dealer.rb
CHANGED
|
@@ -12,8 +12,8 @@ module OMQ
|
|
|
12
12
|
# @param backend [Symbol, nil] :ruby (default) or :ffi
|
|
13
13
|
#
|
|
14
14
|
def initialize(endpoints = nil, linger: 0, backend: nil)
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
init_engine(:DEALER, linger: linger, backend: backend)
|
|
16
|
+
attach_endpoints(endpoints, default: :connect)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -29,8 +29,8 @@ module OMQ
|
|
|
29
29
|
# @param backend [Symbol, nil] :ruby (default) or :ffi
|
|
30
30
|
#
|
|
31
31
|
def initialize(endpoints = nil, linger: 0, backend: nil)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
init_engine(:ROUTER, linger: linger, backend: backend)
|
|
33
|
+
attach_endpoints(endpoints, default: :bind)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
|
|
@@ -21,15 +21,21 @@ module OMQ
|
|
|
21
21
|
loop do
|
|
22
22
|
batch = [q.dequeue]
|
|
23
23
|
Routing.drain_send_queue(q, batch)
|
|
24
|
+
|
|
24
25
|
if batch.size == 1
|
|
25
|
-
conn.write_message
|
|
26
|
+
conn.write_message batch.first
|
|
26
27
|
else
|
|
27
|
-
conn.write_messages
|
|
28
|
+
conn.write_messages batch
|
|
28
29
|
end
|
|
30
|
+
|
|
29
31
|
conn.flush
|
|
30
|
-
|
|
32
|
+
|
|
33
|
+
batch.each do |parts|
|
|
34
|
+
engine.emit_verbose_monitor_event :message_sent, parts: parts
|
|
35
|
+
end
|
|
31
36
|
end
|
|
32
37
|
end
|
|
38
|
+
|
|
33
39
|
tasks << task
|
|
34
40
|
task
|
|
35
41
|
end
|
data/lib/omq/routing/dealer.rb
CHANGED
|
@@ -10,6 +10,7 @@ module OMQ
|
|
|
10
10
|
include RoundRobin
|
|
11
11
|
include FairRecv
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
# @param engine [Engine]
|
|
14
15
|
#
|
|
15
16
|
def initialize(engine)
|
|
@@ -24,6 +25,7 @@ module OMQ
|
|
|
24
25
|
#
|
|
25
26
|
attr_reader :recv_queue
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
# @param connection [Connection]
|
|
28
30
|
#
|
|
29
31
|
def connection_added(connection)
|
|
@@ -104,8 +104,10 @@ module OMQ
|
|
|
104
104
|
@drain.empty? && @queues.all?(&:empty?)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
|
|
107
108
|
private
|
|
108
109
|
|
|
110
|
+
|
|
109
111
|
# Drains orphaned queues first (preserves FIFO for disconnected
|
|
110
112
|
# peers), then tries each active queue once in round-robin order.
|
|
111
113
|
#
|
|
@@ -162,16 +164,25 @@ module OMQ
|
|
|
162
164
|
# @param timeout [Numeric, nil] dequeue timeout
|
|
163
165
|
# @return [Array<String>, nil]
|
|
164
166
|
#
|
|
165
|
-
def dequeue(timeout: nil)
|
|
167
|
+
def dequeue(timeout: nil)
|
|
168
|
+
@inner.dequeue(timeout: timeout)
|
|
169
|
+
end
|
|
170
|
+
|
|
166
171
|
|
|
167
172
|
# @return [Boolean]
|
|
168
173
|
#
|
|
169
|
-
def empty?
|
|
174
|
+
def empty?
|
|
175
|
+
@inner.empty?
|
|
176
|
+
end
|
|
177
|
+
|
|
170
178
|
|
|
171
179
|
# @param item [Object, nil]
|
|
172
180
|
# @return [void]
|
|
173
181
|
#
|
|
174
|
-
def push(item)
|
|
182
|
+
def push(item)
|
|
183
|
+
@inner.push(item)
|
|
184
|
+
end
|
|
185
|
+
|
|
175
186
|
end
|
|
176
187
|
end
|
|
177
188
|
end
|
data/lib/omq/routing/fan_out.rb
CHANGED
|
@@ -17,18 +17,25 @@ module OMQ
|
|
|
17
17
|
# their #initialize.
|
|
18
18
|
#
|
|
19
19
|
module FanOut
|
|
20
|
+
# Shared frozen empty binary string to avoid repeated allocations.
|
|
21
|
+
EMPTY_BINARY = ::Protocol::ZMTP::Codec::EMPTY_BINARY
|
|
22
|
+
|
|
23
|
+
|
|
20
24
|
# @return [Async::Promise] resolves when the first subscriber joins
|
|
21
25
|
#
|
|
22
26
|
attr_reader :subscriber_joined
|
|
23
27
|
|
|
28
|
+
|
|
24
29
|
# @return [Boolean] true when all per-connection send queues are empty
|
|
25
30
|
#
|
|
26
31
|
def send_queues_drained?
|
|
27
32
|
@conn_queues.values.all?(&:empty?)
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
|
|
30
36
|
private
|
|
31
37
|
|
|
38
|
+
|
|
32
39
|
def init_fan_out(engine)
|
|
33
40
|
@connections = Set.new
|
|
34
41
|
@subscriptions = {} # connection => Set of prefixes
|
|
@@ -156,6 +163,15 @@ module OMQ
|
|
|
156
163
|
end
|
|
157
164
|
|
|
158
165
|
|
|
166
|
+
# Send pump variant for non-conflate fan-out: dequeues, batch-drains,
|
|
167
|
+
# writes each subscribed message, then flushes once.
|
|
168
|
+
#
|
|
169
|
+
# @param conn [Connection]
|
|
170
|
+
# @param q [Async::LimitedQueue, DropQueue]
|
|
171
|
+
# @param use_wire [Boolean] true iff the encoded wire bytes can
|
|
172
|
+
# be shared across peers (unencrypted ZMTP)
|
|
173
|
+
# @return [Async::Task]
|
|
174
|
+
#
|
|
159
175
|
def start_conn_send_pump_normal(conn, q, use_wire)
|
|
160
176
|
@engine.spawn_conn_pump_task(conn, annotation: "send pump") do
|
|
161
177
|
loop do
|
|
@@ -170,31 +186,52 @@ module OMQ
|
|
|
170
186
|
end
|
|
171
187
|
|
|
172
188
|
|
|
189
|
+
# Writes every batch entry whose topic matches a subscription on
|
|
190
|
+
# +conn+ to +conn+'s write buffer. Does not flush.
|
|
191
|
+
#
|
|
192
|
+
# @return [Boolean] true iff at least one message was written
|
|
193
|
+
#
|
|
173
194
|
def write_matching_batch(conn, batch, use_wire)
|
|
174
195
|
sent = false
|
|
175
196
|
batch.each do |parts|
|
|
176
197
|
next unless subscribed?(conn, parts.first || EMPTY_BINARY)
|
|
177
|
-
use_wire
|
|
198
|
+
if use_wire
|
|
199
|
+
conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts))
|
|
200
|
+
else
|
|
201
|
+
conn.write_message(parts)
|
|
202
|
+
end
|
|
178
203
|
sent = true
|
|
179
204
|
end
|
|
180
205
|
sent
|
|
181
206
|
end
|
|
182
207
|
|
|
183
208
|
|
|
209
|
+
# Send pump variant for conflate mode: keeps only the latest
|
|
210
|
+
# subscribed message per batch. Stale duplicates are dropped.
|
|
211
|
+
#
|
|
212
|
+
# @param conn [Connection]
|
|
213
|
+
# @param q [Async::LimitedQueue, DropQueue]
|
|
214
|
+
# @return [Async::Task]
|
|
215
|
+
#
|
|
184
216
|
def start_conn_send_pump_conflate(conn, q)
|
|
185
217
|
@engine.spawn_conn_pump_task(conn, annotation: "send pump") do
|
|
186
218
|
loop do
|
|
187
219
|
batch = [q.dequeue]
|
|
188
220
|
Routing.drain_send_queue(q, batch)
|
|
221
|
+
|
|
189
222
|
# Keep only the latest message that matches the subscription.
|
|
190
|
-
latest = batch.reverse.find
|
|
223
|
+
latest = batch.reverse.find do |parts|
|
|
224
|
+
subscribed?(conn, parts.first || EMPTY_BINARY)
|
|
225
|
+
end
|
|
191
226
|
next unless latest
|
|
227
|
+
|
|
192
228
|
conn.write_message(latest)
|
|
193
229
|
conn.flush
|
|
194
230
|
@engine.emit_verbose_monitor_event(:message_sent, parts: latest)
|
|
195
231
|
end
|
|
196
232
|
end
|
|
197
233
|
end
|
|
234
|
+
|
|
198
235
|
end
|
|
199
236
|
end
|
|
200
237
|
end
|
data/lib/omq/routing/req.rb
CHANGED
|
@@ -10,6 +10,10 @@ module OMQ
|
|
|
10
10
|
include RoundRobin
|
|
11
11
|
include FairRecv
|
|
12
12
|
|
|
13
|
+
# Shared frozen empty binary string to avoid repeated allocations.
|
|
14
|
+
EMPTY_BINARY = ::Protocol::ZMTP::Codec::EMPTY_BINARY
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
# @param engine [Engine]
|
|
14
18
|
#
|
|
15
19
|
def initialize(engine)
|
|
@@ -64,11 +68,16 @@ module OMQ
|
|
|
64
68
|
@tasks.clear
|
|
65
69
|
end
|
|
66
70
|
|
|
71
|
+
|
|
67
72
|
private
|
|
68
73
|
|
|
74
|
+
|
|
69
75
|
# REQ prepends empty delimiter frame on the wire.
|
|
70
76
|
#
|
|
71
|
-
def transform_send(parts)
|
|
77
|
+
def transform_send(parts)
|
|
78
|
+
[EMPTY_BINARY, *parts]
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
end
|
|
73
82
|
end
|
|
74
83
|
end
|
data/lib/omq/routing.rb
CHANGED
|
@@ -15,16 +15,17 @@ module OMQ
|
|
|
15
15
|
# the socket's send/recv queues.
|
|
16
16
|
#
|
|
17
17
|
module Routing
|
|
18
|
-
# Shared frozen empty binary string to avoid repeated allocations.
|
|
19
|
-
EMPTY_BINARY = "".b.freeze
|
|
20
|
-
|
|
21
|
-
|
|
22
18
|
# Plugin registry for socket types not built into omq.
|
|
23
19
|
# Populated by sister gems via +Routing.register+.
|
|
24
20
|
#
|
|
25
21
|
@registry = {}
|
|
26
22
|
|
|
23
|
+
|
|
27
24
|
class << self
|
|
25
|
+
# @return [Hash{Symbol => Class}] plugin registry
|
|
26
|
+
attr_reader :registry
|
|
27
|
+
|
|
28
|
+
|
|
28
29
|
# Registers a routing strategy class for a socket type.
|
|
29
30
|
# Called by omq-draft (and other plugins) at require time.
|
|
30
31
|
#
|
data/lib/omq/socket.rb
CHANGED
|
@@ -88,7 +88,7 @@ module OMQ
|
|
|
88
88
|
def bind(endpoint, parent: nil)
|
|
89
89
|
ensure_parent_task(parent: parent)
|
|
90
90
|
Reactor.run do
|
|
91
|
-
@engine.bind(endpoint)
|
|
91
|
+
@engine.bind(endpoint) # TODO: use timeout?
|
|
92
92
|
@last_tcp_port = @engine.last_tcp_port
|
|
93
93
|
end
|
|
94
94
|
end
|
|
@@ -102,7 +102,7 @@ module OMQ
|
|
|
102
102
|
#
|
|
103
103
|
def connect(endpoint, parent: nil)
|
|
104
104
|
ensure_parent_task(parent: parent)
|
|
105
|
-
Reactor.run { @engine.connect(endpoint) }
|
|
105
|
+
Reactor.run { @engine.connect(endpoint) } # TODO: use timeout?
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
|
|
@@ -112,7 +112,7 @@ module OMQ
|
|
|
112
112
|
# @return [void]
|
|
113
113
|
#
|
|
114
114
|
def disconnect(endpoint)
|
|
115
|
-
Reactor.run { @engine.disconnect(endpoint) }
|
|
115
|
+
Reactor.run { @engine.disconnect(endpoint) } # TODO: use timeout?
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
|
|
@@ -122,7 +122,7 @@ module OMQ
|
|
|
122
122
|
# @return [void]
|
|
123
123
|
#
|
|
124
124
|
def unbind(endpoint)
|
|
125
|
-
Reactor.run { @engine.unbind(endpoint) }
|
|
125
|
+
Reactor.run { @engine.unbind(endpoint) } # TODO: use timeout?
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
|
|
@@ -183,9 +183,11 @@ module OMQ
|
|
|
183
183
|
#
|
|
184
184
|
def monitor(verbose: false, &block)
|
|
185
185
|
ensure_parent_task
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
|
|
187
|
+
queue = Async::LimitedQueue.new(64)
|
|
188
|
+
@engine.monitor_queue = queue
|
|
188
189
|
@engine.verbose_monitor = verbose
|
|
190
|
+
|
|
189
191
|
Reactor.run do
|
|
190
192
|
@engine.parent_task.async(transient: true, annotation: "monitor") do
|
|
191
193
|
while (event = queue.dequeue)
|
|
@@ -218,7 +220,7 @@ module OMQ
|
|
|
218
220
|
# @return [nil]
|
|
219
221
|
#
|
|
220
222
|
def close
|
|
221
|
-
Reactor.run { @engine.close }
|
|
223
|
+
Reactor.run { @engine.close } # TODO: use timeout?
|
|
222
224
|
nil
|
|
223
225
|
end
|
|
224
226
|
|
|
@@ -230,7 +232,7 @@ module OMQ
|
|
|
230
232
|
# @return [nil]
|
|
231
233
|
#
|
|
232
234
|
def stop
|
|
233
|
-
Reactor.run { @engine.stop }
|
|
235
|
+
Reactor.run { @engine.stop } # TODO: use timeout?
|
|
234
236
|
nil
|
|
235
237
|
end
|
|
236
238
|
|
|
@@ -253,42 +255,13 @@ module OMQ
|
|
|
253
255
|
end
|
|
254
256
|
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# Runs a block with a timeout. Uses Async's with_timeout if inside
|
|
260
|
-
# a reactor, otherwise falls back to Timeout.timeout.
|
|
261
|
-
#
|
|
262
|
-
# @param seconds [Numeric]
|
|
263
|
-
# @raise [IO::TimeoutError]
|
|
264
|
-
#
|
|
265
|
-
def with_timeout(seconds, &block)
|
|
266
|
-
return yield if seconds.nil?
|
|
267
|
-
if Async::Task.current?
|
|
268
|
-
Async::Task.current.with_timeout(seconds, &block)
|
|
269
|
-
else
|
|
270
|
-
Timeout.timeout(seconds, &block)
|
|
271
|
-
end
|
|
272
|
-
rescue Async::TimeoutError, Timeout::Error
|
|
273
|
-
raise IO::TimeoutError, "timed out"
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
# Sets the engine's parent task before the first bind or connect.
|
|
278
|
-
# Must be called OUTSIDE Reactor.run so that non-Async callers
|
|
279
|
-
# get the IO thread's root task, not an ephemeral work task.
|
|
280
|
-
#
|
|
281
|
-
def ensure_parent_task(parent: nil)
|
|
282
|
-
@engine.capture_parent_task(parent: parent)
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# Connects or binds based on endpoint prefix convention.
|
|
258
|
+
# Connects or binds based on endpoint prefix convention. Called
|
|
259
|
+
# from subclass initializers (including out-of-tree socket types).
|
|
287
260
|
#
|
|
288
261
|
# @param endpoints [String, nil]
|
|
289
262
|
# @param default [Symbol] :connect or :bind
|
|
290
263
|
#
|
|
291
|
-
def
|
|
264
|
+
def attach_endpoints(endpoints, default:)
|
|
292
265
|
return unless endpoints
|
|
293
266
|
case endpoints
|
|
294
267
|
when /\A@(.+)\z/
|
|
@@ -301,29 +274,42 @@ module OMQ
|
|
|
301
274
|
end
|
|
302
275
|
|
|
303
276
|
|
|
304
|
-
# Initializes engine and options for a socket type.
|
|
277
|
+
# Initializes engine and options for a socket type. Called from
|
|
278
|
+
# subclass initializers (including out-of-tree socket types).
|
|
305
279
|
#
|
|
306
280
|
# @param socket_type [Symbol]
|
|
307
281
|
# @param linger [Integer]
|
|
308
282
|
#
|
|
309
|
-
def
|
|
310
|
-
|
|
311
|
-
|
|
283
|
+
def init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
|
|
284
|
+
send_timeout: nil, recv_timeout: nil, conflate: false,
|
|
285
|
+
on_mute: nil, backend: nil)
|
|
312
286
|
@options = Options.new(linger: linger)
|
|
313
|
-
@options.send_hwm
|
|
314
|
-
@options.recv_hwm
|
|
315
|
-
@options.send_timeout
|
|
316
|
-
@options.recv_timeout
|
|
317
|
-
@options.conflate
|
|
318
|
-
@options.on_mute
|
|
319
|
-
@engine
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
287
|
+
@options.send_hwm = send_hwm if send_hwm
|
|
288
|
+
@options.recv_hwm = recv_hwm if recv_hwm
|
|
289
|
+
@options.send_timeout = send_timeout if send_timeout
|
|
290
|
+
@options.recv_timeout = recv_timeout if recv_timeout
|
|
291
|
+
@options.conflate = conflate
|
|
292
|
+
@options.on_mute = on_mute if on_mute
|
|
293
|
+
@engine = case backend
|
|
294
|
+
when nil, :ruby
|
|
295
|
+
Engine.new(socket_type, @options)
|
|
296
|
+
when :ffi
|
|
297
|
+
FFI::Engine.new(socket_type, @options)
|
|
298
|
+
else
|
|
299
|
+
raise ArgumentError, "unknown backend: #{backend}"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
private
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# Sets the engine's parent task before the first bind or connect.
|
|
308
|
+
# Must be called OUTSIDE Reactor.run so that non-Async callers
|
|
309
|
+
# get the IO thread's root task, not an ephemeral work task.
|
|
310
|
+
#
|
|
311
|
+
def ensure_parent_task(parent: nil)
|
|
312
|
+
@engine.capture_parent_task(parent: parent)
|
|
327
313
|
end
|
|
328
314
|
end
|
|
329
315
|
end
|
|
@@ -112,6 +112,7 @@ module OMQ
|
|
|
112
112
|
#
|
|
113
113
|
def encrypted? = false
|
|
114
114
|
|
|
115
|
+
|
|
115
116
|
# No-op — inproc has no IO buffer to flush.
|
|
116
117
|
#
|
|
117
118
|
# @return [nil]
|
|
@@ -127,11 +128,16 @@ module OMQ
|
|
|
127
128
|
def receive_message
|
|
128
129
|
loop do
|
|
129
130
|
item = @receive_queue.dequeue
|
|
131
|
+
|
|
130
132
|
raise EOFError, "connection closed" if item.nil?
|
|
133
|
+
|
|
131
134
|
if item.is_a?(Array) && item.first == :command
|
|
132
|
-
|
|
135
|
+
if block_given?
|
|
136
|
+
yield Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
|
|
137
|
+
end
|
|
133
138
|
next
|
|
134
139
|
end
|
|
140
|
+
|
|
135
141
|
return item
|
|
136
142
|
end
|
|
137
143
|
end
|
|
@@ -157,6 +163,7 @@ module OMQ
|
|
|
157
163
|
loop do
|
|
158
164
|
item = @receive_queue.dequeue
|
|
159
165
|
raise EOFError, "connection closed" if item.nil?
|
|
166
|
+
|
|
160
167
|
if item.is_a?(Array) && item.first == :command
|
|
161
168
|
return Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
|
|
162
169
|
end
|
|
@@ -174,11 +181,18 @@ module OMQ
|
|
|
174
181
|
@send_queue&.enqueue(nil) # close sentinel
|
|
175
182
|
end
|
|
176
183
|
|
|
184
|
+
|
|
177
185
|
private
|
|
178
186
|
|
|
187
|
+
|
|
179
188
|
def apply_transform(parts)
|
|
180
|
-
|
|
189
|
+
if @direct_recv_transform
|
|
190
|
+
@direct_recv_transform.call(parts).freeze
|
|
191
|
+
else
|
|
192
|
+
parts
|
|
193
|
+
end
|
|
181
194
|
end
|
|
195
|
+
|
|
182
196
|
end
|
|
183
197
|
end
|
|
184
198
|
end
|
data/lib/omq/transport/inproc.rb
CHANGED
|
@@ -37,7 +37,10 @@ module OMQ
|
|
|
37
37
|
#
|
|
38
38
|
def bind(endpoint, engine)
|
|
39
39
|
@mutex.synchronize do
|
|
40
|
-
|
|
40
|
+
if @registry.key?(endpoint)
|
|
41
|
+
raise ArgumentError, "endpoint already bound: #{endpoint}"
|
|
42
|
+
end
|
|
43
|
+
|
|
41
44
|
@registry[endpoint] = engine
|
|
42
45
|
|
|
43
46
|
# Wake any pending connects
|
|
@@ -96,34 +99,55 @@ module OMQ
|
|
|
96
99
|
def establish_link(client_engine, server_engine, endpoint)
|
|
97
100
|
client_type = client_engine.socket_type
|
|
98
101
|
server_type = server_engine.socket_type
|
|
102
|
+
|
|
99
103
|
unless Protocol::ZMTP::VALID_PEERS[client_type]&.include?(server_type)
|
|
100
104
|
raise Protocol::ZMTP::Error,
|
|
101
105
|
"incompatible socket types: #{client_type} cannot connect to #{server_type}"
|
|
102
106
|
end
|
|
107
|
+
|
|
103
108
|
needs_cmds = needs_commands?(client_engine, server_engine, client_type, server_type)
|
|
104
|
-
client_pipe, server_pipe = make_pipe_pair
|
|
109
|
+
client_pipe, server_pipe = make_pipe_pair client_engine, server_engine,
|
|
110
|
+
client_type, server_type, needs_cmds
|
|
111
|
+
|
|
105
112
|
client_engine.connection_ready(client_pipe, endpoint: endpoint)
|
|
106
113
|
server_engine.connection_ready(server_pipe, endpoint: endpoint)
|
|
107
114
|
end
|
|
108
115
|
|
|
109
116
|
|
|
117
|
+
# Decides whether a DirectPipe pair needs command queues.
|
|
118
|
+
# DirectPipe's fast path skips queues entirely; command queues
|
|
119
|
+
# are only needed for socket types that exchange ZMTP commands
|
|
120
|
+
# (e.g. ROUTER/DEALER identity, PUB/SUB subscriptions) or when
|
|
121
|
+
# either side enables QoS ≥ 1.
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean]
|
|
124
|
+
#
|
|
110
125
|
def needs_commands?(ce, se, ct, st)
|
|
111
|
-
COMMAND_TYPES.include?(ct) || COMMAND_TYPES.include?(st)
|
|
112
|
-
|
|
126
|
+
return true if COMMAND_TYPES.include?(ct) || COMMAND_TYPES.include?(st)
|
|
127
|
+
return true if ce.options.qos >= 1 || se.options.qos >= 1
|
|
128
|
+
false
|
|
113
129
|
end
|
|
114
130
|
|
|
115
131
|
|
|
132
|
+
# Builds a bidirectional {DirectPipe} pair for client + server.
|
|
133
|
+
# When +needs_cmds+ is false the pipes have no command queues
|
|
134
|
+
# (fast path — all traffic bypasses Async::Queue entirely).
|
|
135
|
+
#
|
|
136
|
+
# @return [Array(DirectPipe, DirectPipe)] client, server
|
|
137
|
+
#
|
|
116
138
|
def make_pipe_pair(ce, se, ct, st, needs_cmds)
|
|
117
139
|
if needs_cmds
|
|
118
140
|
a_to_b = Async::Queue.new
|
|
119
141
|
b_to_a = Async::Queue.new
|
|
120
142
|
end
|
|
143
|
+
|
|
121
144
|
client = DirectPipe.new(send_queue: needs_cmds ? a_to_b : nil,
|
|
122
145
|
receive_queue: needs_cmds ? b_to_a : nil,
|
|
123
146
|
peer_identity: se.options.identity, peer_type: st.to_s)
|
|
124
147
|
server = DirectPipe.new(send_queue: needs_cmds ? b_to_a : nil,
|
|
125
148
|
receive_queue: needs_cmds ? a_to_b : nil,
|
|
126
149
|
peer_identity: ce.options.identity, peer_type: ct.to_s)
|
|
150
|
+
|
|
127
151
|
client.peer = server
|
|
128
152
|
server.peer = client
|
|
129
153
|
[client, server]
|
|
@@ -136,11 +160,20 @@ module OMQ
|
|
|
136
160
|
ri = engine.options.reconnect_interval
|
|
137
161
|
timeout = ri.is_a?(Range) ? ri.begin : ri
|
|
138
162
|
promise = Async::Promise.new
|
|
139
|
-
|
|
163
|
+
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
@waiters[endpoint] << promise
|
|
166
|
+
end
|
|
167
|
+
|
|
140
168
|
if promise.wait?(timeout: timeout)
|
|
141
|
-
@mutex.synchronize
|
|
169
|
+
@mutex.synchronize do
|
|
170
|
+
@registry[endpoint]
|
|
171
|
+
end
|
|
142
172
|
else
|
|
143
|
-
@mutex.synchronize
|
|
173
|
+
@mutex.synchronize do
|
|
174
|
+
@waiters[endpoint].delete(promise)
|
|
175
|
+
end
|
|
176
|
+
|
|
144
177
|
start_connect_retry(endpoint, engine)
|
|
145
178
|
nil
|
|
146
179
|
end
|
|
@@ -158,6 +191,7 @@ module OMQ
|
|
|
158
191
|
loop do
|
|
159
192
|
sleep ivl
|
|
160
193
|
bound_engine = @mutex.synchronize { @registry[endpoint] }
|
|
194
|
+
|
|
161
195
|
if bound_engine
|
|
162
196
|
establish_link(engine, bound_engine, endpoint)
|
|
163
197
|
break
|
data/lib/omq/transport/ipc.rb
CHANGED
|
@@ -19,7 +19,7 @@ module OMQ
|
|
|
19
19
|
# @return [Listener]
|
|
20
20
|
#
|
|
21
21
|
def bind(endpoint, engine)
|
|
22
|
-
path
|
|
22
|
+
path = parse_path(endpoint)
|
|
23
23
|
sock_path = to_socket_path(path)
|
|
24
24
|
|
|
25
25
|
# Remove stale socket file for file-based paths
|
|
@@ -45,17 +45,31 @@ module OMQ
|
|
|
45
45
|
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
|
|
49
|
+
# Applies SO_SNDBUF / SO_RCVBUF to +sock+ from the socket's
|
|
50
|
+
# {Options}. No-op when both are nil (OS default).
|
|
51
|
+
#
|
|
52
|
+
# @param sock [UNIXSocket, UNIXServer]
|
|
53
|
+
# @param options [Options]
|
|
54
|
+
#
|
|
48
55
|
def apply_buffer_sizes(sock, options)
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
if options.sndbuf
|
|
57
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, options.sndbuf)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if options.rcvbuf
|
|
61
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, options.rcvbuf)
|
|
62
|
+
end
|
|
51
63
|
end
|
|
52
64
|
|
|
65
|
+
|
|
53
66
|
private
|
|
54
67
|
|
|
68
|
+
|
|
55
69
|
# Extracts path from "ipc://path".
|
|
56
70
|
#
|
|
57
71
|
def parse_path(endpoint)
|
|
58
|
-
endpoint.
|
|
72
|
+
endpoint.delete_prefix("ipc://")
|
|
59
73
|
end
|
|
60
74
|
|
|
61
75
|
|
|
@@ -65,7 +79,7 @@ module OMQ
|
|
|
65
79
|
if abstract?(path)
|
|
66
80
|
"\0#{path[1..]}"
|
|
67
81
|
else
|
|
68
|
-
path
|
|
82
|
+
path # TODO: return Pathname
|
|
69
83
|
end
|
|
70
84
|
end
|
|
71
85
|
|
|
@@ -92,7 +106,7 @@ module OMQ
|
|
|
92
106
|
|
|
93
107
|
# @param endpoint [String] the IPC endpoint URI
|
|
94
108
|
# @param server [UNIXServer]
|
|
95
|
-
# @param path [String] filesystem or abstract namespace path
|
|
109
|
+
# @param path [String] filesystem or abstract namespace path # TODO: Pathname
|
|
96
110
|
# @param engine [Engine]
|
|
97
111
|
#
|
|
98
112
|
def initialize(endpoint, server, path, engine)
|
|
@@ -111,11 +125,15 @@ module OMQ
|
|
|
111
125
|
# @yieldparam io [IO::Stream::Buffered]
|
|
112
126
|
#
|
|
113
127
|
def start_accept_loops(parent_task, &on_accepted)
|
|
114
|
-
|
|
128
|
+
annotation = "ipc accept #{@endpoint}"
|
|
129
|
+
@task = parent_task.async(transient: true, annotation:) do
|
|
115
130
|
loop do
|
|
116
131
|
client = @server.accept
|
|
117
132
|
IPC.apply_buffer_sizes(client, @engine.options)
|
|
118
|
-
Async::Task.current.defer_stop
|
|
133
|
+
Async::Task.current.defer_stop do
|
|
134
|
+
# TODO use yield
|
|
135
|
+
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
136
|
+
end
|
|
119
137
|
end
|
|
120
138
|
rescue Async::Stop
|
|
121
139
|
rescue IOError
|
|
@@ -135,11 +153,13 @@ module OMQ
|
|
|
135
153
|
def stop
|
|
136
154
|
@task&.stop
|
|
137
155
|
@server.close rescue nil
|
|
156
|
+
|
|
138
157
|
# Clean up socket file for file-based paths
|
|
139
|
-
unless @path.start_with?("@")
|
|
158
|
+
unless @path.start_with?("@") # TODO: check if it's a Pathname instead
|
|
140
159
|
File.delete(@path) rescue nil
|
|
141
160
|
end
|
|
142
161
|
end
|
|
162
|
+
|
|
143
163
|
end
|
|
144
164
|
end
|
|
145
165
|
end
|