omq 0.1.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 +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +15 -0
- data/README.md +145 -0
- data/lib/omq/pair.rb +13 -0
- data/lib/omq/pub_sub.rb +77 -0
- data/lib/omq/push_pull.rb +21 -0
- data/lib/omq/req_rep.rb +23 -0
- data/lib/omq/router_dealer.rb +36 -0
- data/lib/omq/socket.rb +178 -0
- data/lib/omq/version.rb +5 -0
- data/lib/omq/zmtp/codec/command.rb +207 -0
- data/lib/omq/zmtp/codec/frame.rb +104 -0
- data/lib/omq/zmtp/codec/greeting.rb +96 -0
- data/lib/omq/zmtp/codec.rb +18 -0
- data/lib/omq/zmtp/connection.rb +233 -0
- data/lib/omq/zmtp/engine.rb +339 -0
- data/lib/omq/zmtp/mechanism/null.rb +70 -0
- data/lib/omq/zmtp/options.rb +57 -0
- data/lib/omq/zmtp/reactor.rb +142 -0
- data/lib/omq/zmtp/readable.rb +29 -0
- data/lib/omq/zmtp/routing/dealer.rb +57 -0
- data/lib/omq/zmtp/routing/fan_out.rb +89 -0
- data/lib/omq/zmtp/routing/pair.rb +68 -0
- data/lib/omq/zmtp/routing/pub.rb +62 -0
- data/lib/omq/zmtp/routing/pull.rb +48 -0
- data/lib/omq/zmtp/routing/push.rb +57 -0
- data/lib/omq/zmtp/routing/rep.rb +83 -0
- data/lib/omq/zmtp/routing/req.rb +70 -0
- data/lib/omq/zmtp/routing/round_robin.rb +69 -0
- data/lib/omq/zmtp/routing/router.rb +88 -0
- data/lib/omq/zmtp/routing/sub.rb +80 -0
- data/lib/omq/zmtp/routing/xpub.rb +74 -0
- data/lib/omq/zmtp/routing/xsub.rb +80 -0
- data/lib/omq/zmtp/routing.rb +38 -0
- data/lib/omq/zmtp/transport/inproc.rb +299 -0
- data/lib/omq/zmtp/transport/ipc.rb +114 -0
- data/lib/omq/zmtp/transport/tcp.rb +98 -0
- data/lib/omq/zmtp/valid_peers.rb +21 -0
- data/lib/omq/zmtp/writable.rb +44 -0
- data/lib/omq/zmtp.rb +47 -0
- data/lib/omq.rb +19 -0
- metadata +110 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# SUB socket routing: subscription-based receive from PUB peers.
|
|
7
|
+
#
|
|
8
|
+
# Sends SUBSCRIBE/CANCEL commands to connected PUB peers.
|
|
9
|
+
#
|
|
10
|
+
class Sub
|
|
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
|
+
@subscriptions = Set.new
|
|
19
|
+
@tasks = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Async::LimitedQueue]
|
|
23
|
+
#
|
|
24
|
+
attr_reader :recv_queue
|
|
25
|
+
|
|
26
|
+
# @param connection [Connection]
|
|
27
|
+
#
|
|
28
|
+
def connection_added(connection)
|
|
29
|
+
@connections << connection
|
|
30
|
+
# Send existing subscriptions to new peer
|
|
31
|
+
@subscriptions.each do |prefix|
|
|
32
|
+
connection.send_command(Codec::Command.subscribe(prefix))
|
|
33
|
+
end
|
|
34
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
35
|
+
@tasks << task if task
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param connection [Connection]
|
|
39
|
+
#
|
|
40
|
+
def connection_removed(connection)
|
|
41
|
+
@connections.delete(connection)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# SUB is read-only.
|
|
45
|
+
#
|
|
46
|
+
def enqueue(_parts)
|
|
47
|
+
raise "SUB sockets cannot send"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Subscribes to a topic prefix.
|
|
51
|
+
#
|
|
52
|
+
# @param prefix [String]
|
|
53
|
+
#
|
|
54
|
+
def subscribe(prefix)
|
|
55
|
+
@subscriptions << prefix
|
|
56
|
+
@connections.each do |conn|
|
|
57
|
+
conn.send_command(Codec::Command.subscribe(prefix))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Unsubscribes from a topic prefix.
|
|
62
|
+
#
|
|
63
|
+
# @param prefix [String]
|
|
64
|
+
#
|
|
65
|
+
def unsubscribe(prefix)
|
|
66
|
+
@subscriptions.delete(prefix)
|
|
67
|
+
@connections.each do |conn|
|
|
68
|
+
conn.send_command(Codec::Command.cancel(prefix))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stop
|
|
73
|
+
@tasks.each(&:stop)
|
|
74
|
+
@tasks.clear
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# XPUB socket routing: like PUB but exposes subscription messages.
|
|
7
|
+
#
|
|
8
|
+
# Subscription/unsubscription messages from peers are delivered to
|
|
9
|
+
# the application as data frames: \x01 + prefix for subscribe,
|
|
10
|
+
# \x00 + prefix for unsubscribe.
|
|
11
|
+
#
|
|
12
|
+
class XPub
|
|
13
|
+
include FanOut
|
|
14
|
+
|
|
15
|
+
# @param engine [Engine]
|
|
16
|
+
#
|
|
17
|
+
def initialize(engine)
|
|
18
|
+
@engine = engine
|
|
19
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
20
|
+
@tasks = []
|
|
21
|
+
init_fan_out(engine)
|
|
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
|
+
@subscriptions[connection] = Set.new
|
|
33
|
+
start_subscription_listener(connection)
|
|
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
|
+
@subscriptions.delete(connection)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param parts [Array<String>]
|
|
45
|
+
#
|
|
46
|
+
def enqueue(parts)
|
|
47
|
+
@send_queue.enqueue(parts)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
def stop
|
|
52
|
+
@tasks.each(&:stop)
|
|
53
|
+
@tasks.clear
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Expose subscription to application as data message.
|
|
59
|
+
#
|
|
60
|
+
def on_subscribe(conn, prefix)
|
|
61
|
+
super
|
|
62
|
+
@recv_queue.enqueue(["\x01#{prefix}".b])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Expose unsubscription to application as data message.
|
|
66
|
+
#
|
|
67
|
+
def on_cancel(conn, prefix)
|
|
68
|
+
super
|
|
69
|
+
@recv_queue.enqueue(["\x00#{prefix}".b])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# XSUB socket routing: like SUB but subscriptions sent as data messages.
|
|
7
|
+
#
|
|
8
|
+
# Subscriptions are sent as data frames: \x01 + prefix for subscribe,
|
|
9
|
+
# \x00 + prefix for unsubscribe.
|
|
10
|
+
#
|
|
11
|
+
class XSub
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@connections = []
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
20
|
+
@tasks = []
|
|
21
|
+
@send_pump_started = false
|
|
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
|
+
private
|
|
56
|
+
|
|
57
|
+
def start_send_pump
|
|
58
|
+
@send_pump_started = true
|
|
59
|
+
@tasks << Reactor.spawn_pump do
|
|
60
|
+
loop do
|
|
61
|
+
parts = @send_queue.dequeue
|
|
62
|
+
frame = parts.first&.b
|
|
63
|
+
next if frame.nil? || frame.empty?
|
|
64
|
+
|
|
65
|
+
flag = frame.getbyte(0)
|
|
66
|
+
prefix = frame.byteslice(1..) || "".b
|
|
67
|
+
|
|
68
|
+
case flag
|
|
69
|
+
when 0x01
|
|
70
|
+
@connections.each { |c| c.send_command(Codec::Command.subscribe(prefix)) }
|
|
71
|
+
when 0x00
|
|
72
|
+
@connections.each { |c| c.send_command(Codec::Command.cancel(prefix)) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
# Returns the routing strategy class for a socket type.
|
|
16
|
+
#
|
|
17
|
+
# @param socket_type [Symbol] e.g. :PAIR, :REQ
|
|
18
|
+
# @return [Class]
|
|
19
|
+
#
|
|
20
|
+
def self.for(socket_type)
|
|
21
|
+
case socket_type
|
|
22
|
+
when :PAIR then Pair
|
|
23
|
+
when :REQ then Req
|
|
24
|
+
when :REP then Rep
|
|
25
|
+
when :DEALER then Dealer
|
|
26
|
+
when :ROUTER then Router
|
|
27
|
+
when :PUB then Pub
|
|
28
|
+
when :SUB then Sub
|
|
29
|
+
when :XPUB then XPub
|
|
30
|
+
when :XSUB then XSub
|
|
31
|
+
when :PUSH then Push
|
|
32
|
+
when :PULL then Pull
|
|
33
|
+
else raise ArgumentError, "unknown socket type: #{socket_type}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
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].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
|
+
# 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
|
+
# Removes a bound endpoint from the registry.
|
|
80
|
+
#
|
|
81
|
+
# @param endpoint [String]
|
|
82
|
+
# @return [void]
|
|
83
|
+
#
|
|
84
|
+
def unbind(endpoint)
|
|
85
|
+
@mutex.synchronize { @registry.delete(endpoint) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resets the registry. Used in tests.
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
#
|
|
92
|
+
def reset!
|
|
93
|
+
@mutex.synchronize do
|
|
94
|
+
@registry.clear
|
|
95
|
+
@waiters.clear
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def establish_link(client_engine, server_engine, endpoint)
|
|
102
|
+
client_type = client_engine.socket_type
|
|
103
|
+
server_type = server_engine.socket_type
|
|
104
|
+
|
|
105
|
+
unless ZMTP::VALID_PEERS[client_type]&.include?(server_type)
|
|
106
|
+
raise ProtocolError,
|
|
107
|
+
"incompatible socket types: #{client_type} cannot connect to #{server_type}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Only PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
|
|
111
|
+
# over inproc. All other types use only the direct recv queue
|
|
112
|
+
# bypass for data, so no internal queues are needed.
|
|
113
|
+
needs_commands = COMMAND_TYPES.include?(client_type) ||
|
|
114
|
+
COMMAND_TYPES.include?(server_type)
|
|
115
|
+
|
|
116
|
+
if needs_commands
|
|
117
|
+
a_to_b = Async::Queue.new
|
|
118
|
+
b_to_a = Async::Queue.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
client_pipe = DirectPipe.new(
|
|
122
|
+
send_queue: needs_commands ? a_to_b : nil,
|
|
123
|
+
receive_queue: needs_commands ? b_to_a : nil,
|
|
124
|
+
peer_identity: server_engine.options.identity,
|
|
125
|
+
peer_type: server_type.to_s,
|
|
126
|
+
)
|
|
127
|
+
server_pipe = DirectPipe.new(
|
|
128
|
+
send_queue: needs_commands ? b_to_a : nil,
|
|
129
|
+
receive_queue: needs_commands ? a_to_b : nil,
|
|
130
|
+
peer_identity: client_engine.options.identity,
|
|
131
|
+
peer_type: client_type.to_s,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
client_pipe.peer = server_pipe
|
|
135
|
+
server_pipe.peer = client_pipe
|
|
136
|
+
|
|
137
|
+
client_engine.connection_ready(client_pipe, endpoint: endpoint)
|
|
138
|
+
server_engine.connection_ready(server_pipe, endpoint: endpoint)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def start_connect_retry(endpoint, engine)
|
|
142
|
+
Reactor.spawn_pump do
|
|
143
|
+
ri = engine.options.reconnect_interval
|
|
144
|
+
ivl = ri.is_a?(Range) ? ri.begin : ri
|
|
145
|
+
loop do
|
|
146
|
+
sleep ivl
|
|
147
|
+
bound_engine = @mutex.synchronize { @registry[endpoint] }
|
|
148
|
+
if bound_engine
|
|
149
|
+
establish_link(engine, bound_engine, endpoint)
|
|
150
|
+
break
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A bound inproc endpoint handle.
|
|
158
|
+
#
|
|
159
|
+
class Listener
|
|
160
|
+
# @return [String] the bound endpoint
|
|
161
|
+
#
|
|
162
|
+
attr_reader :endpoint
|
|
163
|
+
|
|
164
|
+
# @param endpoint [String]
|
|
165
|
+
#
|
|
166
|
+
def initialize(endpoint)
|
|
167
|
+
@endpoint = endpoint
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Stops the listener by removing it from the registry.
|
|
171
|
+
#
|
|
172
|
+
# @return [void]
|
|
173
|
+
#
|
|
174
|
+
def stop
|
|
175
|
+
Inproc.unbind(@endpoint)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A direct in-process pipe that transfers Ruby arrays through queues.
|
|
180
|
+
#
|
|
181
|
+
# Implements the same interface as Connection so routing strategies
|
|
182
|
+
# can use it transparently.
|
|
183
|
+
#
|
|
184
|
+
# When a routing strategy sets {#direct_recv_queue} on a pipe,
|
|
185
|
+
# {#send_message} enqueues directly into the peer's recv queue,
|
|
186
|
+
# bypassing the intermediate internal queues and the recv pump
|
|
187
|
+
# task entirely. This reduces inproc from 3 queue hops to 1.
|
|
188
|
+
#
|
|
189
|
+
class DirectPipe
|
|
190
|
+
# @return [String] peer's socket type
|
|
191
|
+
#
|
|
192
|
+
attr_reader :peer_socket_type
|
|
193
|
+
|
|
194
|
+
# @return [String] peer's identity
|
|
195
|
+
#
|
|
196
|
+
attr_reader :peer_identity
|
|
197
|
+
|
|
198
|
+
# @return [DirectPipe, nil] the other end of this pipe pair
|
|
199
|
+
#
|
|
200
|
+
attr_accessor :peer
|
|
201
|
+
|
|
202
|
+
# @return [Async::LimitedQueue, nil] when set, {#send_message}
|
|
203
|
+
# enqueues directly here instead of using the internal queue
|
|
204
|
+
#
|
|
205
|
+
attr_accessor :direct_recv_queue
|
|
206
|
+
|
|
207
|
+
# @return [Proc, nil] optional transform applied before
|
|
208
|
+
# enqueuing into {#direct_recv_queue}
|
|
209
|
+
#
|
|
210
|
+
attr_accessor :direct_recv_transform
|
|
211
|
+
|
|
212
|
+
# @param send_queue [Async::Queue, nil] outgoing command queue
|
|
213
|
+
# (nil for non-PUB/SUB types that don't exchange commands)
|
|
214
|
+
# @param receive_queue [Async::Queue, nil] incoming command queue
|
|
215
|
+
# @param peer_identity [String]
|
|
216
|
+
# @param peer_type [String]
|
|
217
|
+
#
|
|
218
|
+
def initialize(send_queue: nil, receive_queue: nil, peer_identity:, peer_type:)
|
|
219
|
+
@send_queue = send_queue
|
|
220
|
+
@receive_queue = receive_queue
|
|
221
|
+
@peer_identity = peer_identity || "".b
|
|
222
|
+
@peer_socket_type = peer_type
|
|
223
|
+
@closed = false
|
|
224
|
+
@peer = nil
|
|
225
|
+
@direct_recv_queue = nil
|
|
226
|
+
@direct_recv_transform = nil
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Sends a multi-frame message.
|
|
230
|
+
#
|
|
231
|
+
# When {#direct_recv_queue} is set (inproc fast path), the
|
|
232
|
+
# message is delivered directly to the peer's recv queue,
|
|
233
|
+
# skipping the internal pipe queues and the recv pump.
|
|
234
|
+
#
|
|
235
|
+
# @param parts [Array<String>]
|
|
236
|
+
# @return [void]
|
|
237
|
+
#
|
|
238
|
+
def send_message(parts)
|
|
239
|
+
raise IOError, "closed" if @closed
|
|
240
|
+
if @direct_recv_queue
|
|
241
|
+
msg = @direct_recv_transform ? @direct_recv_transform.call(parts) : parts
|
|
242
|
+
@direct_recv_queue.enqueue(msg)
|
|
243
|
+
else
|
|
244
|
+
@send_queue.enqueue(parts)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Receives a multi-frame message.
|
|
249
|
+
#
|
|
250
|
+
# @return [Array<String>]
|
|
251
|
+
# @raise [EOFError] if closed
|
|
252
|
+
#
|
|
253
|
+
def receive_message
|
|
254
|
+
msg = @receive_queue.dequeue
|
|
255
|
+
raise EOFError, "connection closed" if msg.nil?
|
|
256
|
+
msg
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Sends a command via the internal command queue.
|
|
260
|
+
# Only available for PUB/SUB-family pipes.
|
|
261
|
+
#
|
|
262
|
+
# @param command [Codec::Command]
|
|
263
|
+
# @return [void]
|
|
264
|
+
#
|
|
265
|
+
def send_command(command)
|
|
266
|
+
raise IOError, "closed" if @closed
|
|
267
|
+
@send_queue.enqueue([:command, command])
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Reads one command frame from the internal command queue.
|
|
271
|
+
# Used by PUB/XPUB subscription listeners.
|
|
272
|
+
#
|
|
273
|
+
# @return [Codec::Frame]
|
|
274
|
+
#
|
|
275
|
+
def read_frame
|
|
276
|
+
loop do
|
|
277
|
+
item = @receive_queue.dequeue
|
|
278
|
+
raise EOFError, "connection closed" if item.nil?
|
|
279
|
+
if item.is_a?(Array) && item.first == :command
|
|
280
|
+
cmd = item[1]
|
|
281
|
+
return Codec::Frame.new(cmd.to_body, command: true)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Closes this pipe end.
|
|
287
|
+
#
|
|
288
|
+
# @return [void]
|
|
289
|
+
#
|
|
290
|
+
def close
|
|
291
|
+
return if @closed
|
|
292
|
+
@closed = true
|
|
293
|
+
@send_queue&.enqueue(nil) # close sentinel
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
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 do
|
|
32
|
+
loop do
|
|
33
|
+
client = server.accept
|
|
34
|
+
Reactor.run do
|
|
35
|
+
engine.handle_accepted(IO::Stream::Buffered.wrap(client, minimum_write_size: 0), endpoint: endpoint)
|
|
36
|
+
rescue => e
|
|
37
|
+
client.close rescue nil
|
|
38
|
+
raise if !e.is_a?(ProtocolError) && !e.is_a?(EOFError)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
rescue IOError
|
|
42
|
+
# server closed
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Listener.new(endpoint, server, accept_task, path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Connects to an IPC endpoint.
|
|
49
|
+
#
|
|
50
|
+
# @param endpoint [String]
|
|
51
|
+
# @param engine [Engine]
|
|
52
|
+
# @return [void]
|
|
53
|
+
#
|
|
54
|
+
def connect(endpoint, engine)
|
|
55
|
+
path = parse_path(endpoint)
|
|
56
|
+
sock_path = to_socket_path(path)
|
|
57
|
+
sock = UNIXSocket.new(sock_path)
|
|
58
|
+
engine.handle_connected(IO::Stream::Buffered.wrap(sock, minimum_write_size: 0), endpoint: endpoint)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# Extracts path from "ipc://path".
|
|
64
|
+
#
|
|
65
|
+
def parse_path(endpoint)
|
|
66
|
+
endpoint.sub(%r{\Aipc://}, "")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Converts @ prefix to \0 for abstract namespace.
|
|
70
|
+
#
|
|
71
|
+
def to_socket_path(path)
|
|
72
|
+
if abstract?(path)
|
|
73
|
+
"\0#{path[1..]}"
|
|
74
|
+
else
|
|
75
|
+
path
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Boolean] true if abstract namespace path
|
|
80
|
+
#
|
|
81
|
+
def abstract?(path)
|
|
82
|
+
path.start_with?("@")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A bound IPC listener.
|
|
87
|
+
#
|
|
88
|
+
class Listener
|
|
89
|
+
# @return [String] the endpoint
|
|
90
|
+
#
|
|
91
|
+
attr_reader :endpoint
|
|
92
|
+
|
|
93
|
+
def initialize(endpoint, server, accept_task, path)
|
|
94
|
+
@endpoint = endpoint
|
|
95
|
+
@server = server
|
|
96
|
+
@accept_task = accept_task
|
|
97
|
+
@path = path
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Stops the listener.
|
|
101
|
+
#
|
|
102
|
+
def stop
|
|
103
|
+
@accept_task.stop
|
|
104
|
+
@server.close rescue nil
|
|
105
|
+
# Clean up socket file for file-based paths
|
|
106
|
+
unless @path.start_with?("@")
|
|
107
|
+
File.delete(@path) rescue nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|