omq 0.8.0 → 0.10.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 +87 -0
- data/README.md +9 -49
- data/lib/omq/channel.rb +3 -3
- data/lib/omq/client_server.rb +6 -6
- data/lib/omq/engine.rb +641 -0
- data/lib/omq/options.rb +46 -0
- data/lib/omq/pair.rb +2 -2
- data/lib/omq/peer.rb +3 -3
- data/lib/omq/pub_sub.rb +6 -6
- data/lib/omq/push_pull.rb +2 -2
- data/lib/omq/radio_dish.rb +2 -2
- data/lib/omq/reactor.rb +128 -0
- data/lib/omq/readable.rb +42 -0
- data/lib/omq/req_rep.rb +4 -4
- data/lib/omq/router_dealer.rb +4 -4
- 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 +131 -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 +140 -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 +4 -4
- data/lib/omq/single_frame.rb +18 -0
- data/lib/omq/socket.rb +24 -9
- 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/version.rb +1 -1
- data/lib/omq/writable.rb +65 -0
- data/lib/omq.rb +60 -4
- metadata +38 -58
- data/exe/omq +0 -6
- data/lib/omq/cli/base_runner.rb +0 -459
- data/lib/omq/cli/channel.rb +0 -8
- data/lib/omq/cli/client_server.rb +0 -111
- data/lib/omq/cli/config.rb +0 -54
- data/lib/omq/cli/formatter.rb +0 -75
- data/lib/omq/cli/pair.rb +0 -31
- data/lib/omq/cli/peer.rb +0 -8
- data/lib/omq/cli/pipe.rb +0 -265
- data/lib/omq/cli/pub_sub.rb +0 -14
- data/lib/omq/cli/push_pull.rb +0 -14
- data/lib/omq/cli/radio_dish.rb +0 -27
- data/lib/omq/cli/req_rep.rb +0 -83
- data/lib/omq/cli/router_dealer.rb +0 -76
- data/lib/omq/cli/scatter_gather.rb +0 -14
- data/lib/omq/cli.rb +0 -540
- 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,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# XSUB socket routing: like SUB but subscriptions sent as data messages.
|
|
6
|
+
#
|
|
7
|
+
# Subscriptions are sent as data frames: \x01 + prefix for subscribe,
|
|
8
|
+
# \x00 + prefix for unsubscribe.
|
|
9
|
+
#
|
|
10
|
+
class XSub
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@connections = []
|
|
17
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
18
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
19
|
+
@tasks = []
|
|
20
|
+
@send_pump_started = false
|
|
21
|
+
@send_pump_idle = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Async::LimitedQueue]
|
|
25
|
+
#
|
|
26
|
+
attr_reader :recv_queue, :send_queue
|
|
27
|
+
|
|
28
|
+
# @param connection [Connection]
|
|
29
|
+
#
|
|
30
|
+
def connection_added(connection)
|
|
31
|
+
@connections << connection
|
|
32
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
33
|
+
@tasks << task if task
|
|
34
|
+
start_send_pump unless @send_pump_started
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param connection [Connection]
|
|
38
|
+
#
|
|
39
|
+
def connection_removed(connection)
|
|
40
|
+
@connections.delete(connection)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param parts [Array<String>]
|
|
44
|
+
#
|
|
45
|
+
def enqueue(parts)
|
|
46
|
+
@send_queue.enqueue(parts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
def stop
|
|
51
|
+
@tasks.each(&:stop)
|
|
52
|
+
@tasks.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def send_pump_idle? = @send_pump_idle
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def start_send_pump
|
|
60
|
+
@send_pump_started = true
|
|
61
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
62
|
+
loop do
|
|
63
|
+
@send_pump_idle = true
|
|
64
|
+
parts = @send_queue.dequeue
|
|
65
|
+
@send_pump_idle = false
|
|
66
|
+
frame = parts.first&.b
|
|
67
|
+
next if frame.nil? || frame.empty?
|
|
68
|
+
|
|
69
|
+
flag = frame.getbyte(0)
|
|
70
|
+
prefix = frame.byteslice(1..) || "".b
|
|
71
|
+
|
|
72
|
+
case flag
|
|
73
|
+
when 0x01
|
|
74
|
+
@connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix)) }
|
|
75
|
+
when 0x00
|
|
76
|
+
@connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix)) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/omq/routing.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/queue"
|
|
5
|
+
require "async/limited_queue"
|
|
6
|
+
|
|
7
|
+
module OMQ
|
|
8
|
+
# Routing strategies for each ZMQ socket type.
|
|
9
|
+
#
|
|
10
|
+
# Each strategy manages how messages flow between connections and
|
|
11
|
+
# the socket's send/recv queues.
|
|
12
|
+
#
|
|
13
|
+
module Routing
|
|
14
|
+
# Shared frozen empty binary string to avoid repeated allocations.
|
|
15
|
+
EMPTY_BINARY = "".b.freeze
|
|
16
|
+
|
|
17
|
+
# Drains all available messages from +queue+ into +batch+ without
|
|
18
|
+
# blocking. Call after the initial blocking dequeue.
|
|
19
|
+
#
|
|
20
|
+
# No cap is needed: IO::Stream auto-flushes at 64 KB, so the
|
|
21
|
+
# write buffer hits the wire naturally under sustained load.
|
|
22
|
+
# The explicit flush after the batch pushes out the remainder.
|
|
23
|
+
#
|
|
24
|
+
# @param queue [Async::LimitedQueue]
|
|
25
|
+
# @param batch [Array]
|
|
26
|
+
# @return [void]
|
|
27
|
+
#
|
|
28
|
+
def self.drain_send_queue(queue, batch)
|
|
29
|
+
loop do
|
|
30
|
+
msg = queue.dequeue(timeout: 0)
|
|
31
|
+
break unless msg
|
|
32
|
+
batch << msg
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the routing strategy class for a socket type.
|
|
37
|
+
#
|
|
38
|
+
# @param socket_type [Symbol] e.g. :PAIR, :REQ
|
|
39
|
+
# @return [Class]
|
|
40
|
+
#
|
|
41
|
+
def self.for(socket_type)
|
|
42
|
+
case socket_type
|
|
43
|
+
when :PAIR then Pair
|
|
44
|
+
when :REQ then Req
|
|
45
|
+
when :REP then Rep
|
|
46
|
+
when :DEALER then Dealer
|
|
47
|
+
when :ROUTER then Router
|
|
48
|
+
when :PUB then Pub
|
|
49
|
+
when :SUB then Sub
|
|
50
|
+
when :XPUB then XPub
|
|
51
|
+
when :XSUB then XSub
|
|
52
|
+
when :PUSH then Push
|
|
53
|
+
when :PULL then Pull
|
|
54
|
+
when :CLIENT then Client
|
|
55
|
+
when :SERVER then Server
|
|
56
|
+
when :RADIO then Radio
|
|
57
|
+
when :DISH then Dish
|
|
58
|
+
when :SCATTER then Scatter
|
|
59
|
+
when :GATHER then Gather
|
|
60
|
+
when :PEER then Peer
|
|
61
|
+
when :CHANNEL then Channel
|
|
62
|
+
else raise ArgumentError, "unknown socket type: #{socket_type}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/omq/scatter_gather.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class SCATTER < Socket
|
|
5
|
-
include
|
|
6
|
-
include
|
|
5
|
+
include Writable
|
|
6
|
+
include SingleFrame
|
|
7
7
|
|
|
8
8
|
def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil)
|
|
9
9
|
_init_engine(:SCATTER, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout)
|
|
@@ -12,8 +12,8 @@ module OMQ
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
class GATHER < Socket
|
|
15
|
-
include
|
|
16
|
-
include
|
|
15
|
+
include Readable
|
|
16
|
+
include SingleFrame
|
|
17
17
|
|
|
18
18
|
def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil)
|
|
19
19
|
_init_engine(:GATHER, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
# Mixin that rejects multipart messages.
|
|
5
|
+
#
|
|
6
|
+
# All draft socket types (CLIENT, SERVER, RADIO, DISH, SCATTER,
|
|
7
|
+
# GATHER, PEER, CHANNEL) require single-frame messages for
|
|
8
|
+
# thread-safe atomic operations.
|
|
9
|
+
#
|
|
10
|
+
module SingleFrame
|
|
11
|
+
def send(message)
|
|
12
|
+
if message.is_a?(Array) && message.size > 1
|
|
13
|
+
raise ArgumentError, "#{self.class} does not support multipart messages"
|
|
14
|
+
end
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/omq/socket.rb
CHANGED
|
@@ -6,7 +6,7 @@ module OMQ
|
|
|
6
6
|
# Socket base class.
|
|
7
7
|
#
|
|
8
8
|
class Socket
|
|
9
|
-
# @return [
|
|
9
|
+
# @return [Options]
|
|
10
10
|
#
|
|
11
11
|
attr_reader :options
|
|
12
12
|
|
|
@@ -70,8 +70,11 @@ module OMQ
|
|
|
70
70
|
# @return [void]
|
|
71
71
|
#
|
|
72
72
|
def bind(endpoint)
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
ensure_parent_task
|
|
74
|
+
Reactor.run do
|
|
75
|
+
@engine.bind(endpoint)
|
|
76
|
+
@last_tcp_port = @engine.last_tcp_port
|
|
77
|
+
end
|
|
75
78
|
end
|
|
76
79
|
|
|
77
80
|
|
|
@@ -81,7 +84,8 @@ module OMQ
|
|
|
81
84
|
# @return [void]
|
|
82
85
|
#
|
|
83
86
|
def connect(endpoint)
|
|
84
|
-
|
|
87
|
+
ensure_parent_task
|
|
88
|
+
Reactor.run { @engine.connect(endpoint) }
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
|
|
@@ -91,7 +95,7 @@ module OMQ
|
|
|
91
95
|
# @return [void]
|
|
92
96
|
#
|
|
93
97
|
def disconnect(endpoint)
|
|
94
|
-
@engine.disconnect(endpoint)
|
|
98
|
+
Reactor.run { @engine.disconnect(endpoint) }
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
|
|
@@ -101,7 +105,7 @@ module OMQ
|
|
|
101
105
|
# @return [void]
|
|
102
106
|
#
|
|
103
107
|
def unbind(endpoint)
|
|
104
|
-
@engine.unbind(endpoint)
|
|
108
|
+
Reactor.run { @engine.unbind(endpoint) }
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
|
|
@@ -145,7 +149,7 @@ module OMQ
|
|
|
145
149
|
# Closes the socket.
|
|
146
150
|
#
|
|
147
151
|
def close
|
|
148
|
-
@engine.close
|
|
152
|
+
Reactor.run { @engine.close }
|
|
149
153
|
nil
|
|
150
154
|
end
|
|
151
155
|
|
|
@@ -187,6 +191,15 @@ module OMQ
|
|
|
187
191
|
end
|
|
188
192
|
|
|
189
193
|
|
|
194
|
+
# Sets the engine's parent task before the first bind or connect.
|
|
195
|
+
# Must be called OUTSIDE Reactor.run so that non-Async callers
|
|
196
|
+
# get the IO thread's root task, not an ephemeral work task.
|
|
197
|
+
#
|
|
198
|
+
def ensure_parent_task
|
|
199
|
+
@engine.capture_parent_task
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
|
|
190
203
|
# Connects or binds based on endpoint prefix convention.
|
|
191
204
|
#
|
|
192
205
|
# @param endpoints [String, nil]
|
|
@@ -212,13 +225,15 @@ module OMQ
|
|
|
212
225
|
#
|
|
213
226
|
def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
|
|
214
227
|
send_timeout: nil, recv_timeout: nil, conflate: false)
|
|
215
|
-
@options =
|
|
228
|
+
@options = Options.new(linger: linger)
|
|
216
229
|
@options.send_hwm = send_hwm if send_hwm
|
|
217
230
|
@options.recv_hwm = recv_hwm if recv_hwm
|
|
218
231
|
@options.send_timeout = send_timeout if send_timeout
|
|
219
232
|
@options.recv_timeout = recv_timeout if recv_timeout
|
|
220
233
|
@options.conflate = conflate
|
|
221
|
-
@
|
|
234
|
+
@recv_buffer = []
|
|
235
|
+
@recv_mutex = Mutex.new
|
|
236
|
+
@engine = Engine.new(socket_type, @options)
|
|
222
237
|
end
|
|
223
238
|
end
|
|
224
239
|
end
|
|
@@ -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
|