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,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# REQ socket routing: round-robin send with strict send/recv alternation.
|
|
6
|
+
#
|
|
7
|
+
# REQ prepends an empty delimiter frame on send and strips it on receive.
|
|
8
|
+
#
|
|
9
|
+
class Req
|
|
10
|
+
include RoundRobin
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
17
|
+
@tasks = []
|
|
18
|
+
init_round_robin(engine)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Async::LimitedQueue]
|
|
22
|
+
#
|
|
23
|
+
attr_reader :recv_queue, :send_queue
|
|
24
|
+
|
|
25
|
+
# @param connection [Connection]
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
@connections << connection
|
|
29
|
+
signal_connection_available
|
|
30
|
+
update_direct_pipe
|
|
31
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
32
|
+
msg.first&.empty? ? msg[1..] : msg
|
|
33
|
+
end
|
|
34
|
+
@tasks << task if task
|
|
35
|
+
start_send_pump unless @send_pump_started
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param connection [Connection]
|
|
39
|
+
#
|
|
40
|
+
def connection_removed(connection)
|
|
41
|
+
@connections.delete(connection)
|
|
42
|
+
update_direct_pipe
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param parts [Array<String>]
|
|
46
|
+
#
|
|
47
|
+
def enqueue(parts)
|
|
48
|
+
enqueue_round_robin(parts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
def stop
|
|
53
|
+
@tasks.each(&:stop)
|
|
54
|
+
@tasks.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# REQ prepends empty delimiter frame on the wire.
|
|
60
|
+
#
|
|
61
|
+
def transform_send(parts) = [EMPTY_BINARY, *parts]
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Mixin for routing strategies that send via round-robin.
|
|
6
|
+
#
|
|
7
|
+
# Provides reactive connection management: Async::Promise waits
|
|
8
|
+
# for the first connection, Array#cycle handles round-robin,
|
|
9
|
+
# and a new Promise is created when all connections drop.
|
|
10
|
+
#
|
|
11
|
+
# Including classes must call `init_round_robin(engine)` from
|
|
12
|
+
# their #initialize.
|
|
13
|
+
#
|
|
14
|
+
module RoundRobin
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Initializes round-robin state for the including class.
|
|
19
|
+
#
|
|
20
|
+
# @param engine [Engine]
|
|
21
|
+
#
|
|
22
|
+
def init_round_robin(engine)
|
|
23
|
+
@connections = []
|
|
24
|
+
@cycle = @connections.cycle
|
|
25
|
+
@connection_available = Async::Promise.new
|
|
26
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
27
|
+
@send_pump_started = false
|
|
28
|
+
@send_pump_idle = true
|
|
29
|
+
@direct_pipe = nil
|
|
30
|
+
@written = Set.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Resolves the connection-available promise so blocked
|
|
35
|
+
# senders can proceed.
|
|
36
|
+
#
|
|
37
|
+
def signal_connection_available
|
|
38
|
+
unless @connection_available.resolved?
|
|
39
|
+
@connection_available.resolve(true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Updates the direct-pipe shortcut for inproc single-peer bypass.
|
|
45
|
+
# Call from connection_added after @connections is updated.
|
|
46
|
+
#
|
|
47
|
+
def update_direct_pipe
|
|
48
|
+
if @connections.size == 1 && @connections.first.is_a?(Transport::Inproc::DirectPipe)
|
|
49
|
+
@direct_pipe = @connections.first
|
|
50
|
+
else
|
|
51
|
+
@direct_pipe = nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Enqueues directly to the inproc peer's recv queue if possible,
|
|
57
|
+
# otherwise falls back to the send queue for the send pump.
|
|
58
|
+
#
|
|
59
|
+
def enqueue_round_robin(parts)
|
|
60
|
+
pipe = @direct_pipe
|
|
61
|
+
if pipe&.direct_recv_queue
|
|
62
|
+
pipe.send_message(transform_send(parts))
|
|
63
|
+
else
|
|
64
|
+
@send_queue.enqueue(parts)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Blocks until a connection is available, then returns
|
|
70
|
+
# the next one in round-robin order.
|
|
71
|
+
#
|
|
72
|
+
# @return [Connection]
|
|
73
|
+
#
|
|
74
|
+
def next_connection
|
|
75
|
+
@cycle.next
|
|
76
|
+
rescue StopIteration
|
|
77
|
+
@connection_available = Async::Promise.new
|
|
78
|
+
@connection_available.wait
|
|
79
|
+
@cycle = @connections.cycle
|
|
80
|
+
retry
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Transforms parts before sending. Override in subclasses
|
|
85
|
+
# (e.g. REQ prepends an empty delimiter frame).
|
|
86
|
+
#
|
|
87
|
+
# @param parts [Array<String>]
|
|
88
|
+
# @return [Array<String>]
|
|
89
|
+
#
|
|
90
|
+
def transform_send(parts) = parts
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Starts the background send pump that dequeues messages
|
|
94
|
+
# and dispatches them round-robin across connections.
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean] true when the send pump is idle (not sending a batch)
|
|
97
|
+
def send_pump_idle? = @send_pump_idle
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def start_send_pump
|
|
101
|
+
@send_pump_started = true
|
|
102
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
103
|
+
loop do
|
|
104
|
+
@send_pump_idle = true
|
|
105
|
+
batch = [@send_queue.dequeue]
|
|
106
|
+
@send_pump_idle = false
|
|
107
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
108
|
+
|
|
109
|
+
if batch.size == 1
|
|
110
|
+
send_with_retry(batch[0])
|
|
111
|
+
else
|
|
112
|
+
send_batch(batch)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Sends a single message, retrying on a new connection if
|
|
120
|
+
# the current one is lost.
|
|
121
|
+
#
|
|
122
|
+
# @param parts [Array<String>]
|
|
123
|
+
#
|
|
124
|
+
def send_with_retry(parts)
|
|
125
|
+
conn = next_connection
|
|
126
|
+
conn.send_message(transform_send(parts))
|
|
127
|
+
rescue *CONNECTION_LOST
|
|
128
|
+
@engine.connection_lost(conn)
|
|
129
|
+
retry
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Sends a batch of messages, writing without flushing for
|
|
134
|
+
# throughput. Falls back to #send_with_retry on failure.
|
|
135
|
+
#
|
|
136
|
+
# @param batch [Array<Array<String>>]
|
|
137
|
+
#
|
|
138
|
+
def send_batch(batch)
|
|
139
|
+
@written.clear
|
|
140
|
+
batch.each_with_index do |parts, i|
|
|
141
|
+
conn = next_connection
|
|
142
|
+
begin
|
|
143
|
+
conn.write_message(transform_send(parts))
|
|
144
|
+
@written << conn
|
|
145
|
+
rescue *CONNECTION_LOST
|
|
146
|
+
@engine.connection_lost(conn)
|
|
147
|
+
# Flush what we've written so far
|
|
148
|
+
@written.each do |c|
|
|
149
|
+
c.flush
|
|
150
|
+
rescue *CONNECTION_LOST
|
|
151
|
+
# will be cleaned up
|
|
152
|
+
end
|
|
153
|
+
@written.clear
|
|
154
|
+
# Fall back to send_with_retry for this and remaining
|
|
155
|
+
send_with_retry(parts)
|
|
156
|
+
batch[(i + 1)..].each { |p| send_with_retry(p) }
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
@written.each do |conn|
|
|
161
|
+
conn.flush
|
|
162
|
+
rescue *CONNECTION_LOST
|
|
163
|
+
# will be cleaned up
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module OMQ
|
|
7
|
+
module Routing
|
|
8
|
+
# ROUTER socket routing: identity-based routing.
|
|
9
|
+
#
|
|
10
|
+
# Prepends peer identity frame on receive. Uses first frame as
|
|
11
|
+
# routing identity on send.
|
|
12
|
+
#
|
|
13
|
+
class Router
|
|
14
|
+
# @param engine [Engine]
|
|
15
|
+
#
|
|
16
|
+
def initialize(engine)
|
|
17
|
+
@engine = engine
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
20
|
+
@connections_by_identity = {}
|
|
21
|
+
@identity_by_connection = {}
|
|
22
|
+
@tasks = []
|
|
23
|
+
@send_pump_started = false
|
|
24
|
+
@send_pump_idle = true
|
|
25
|
+
@written = Set.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Async::LimitedQueue]
|
|
29
|
+
#
|
|
30
|
+
attr_reader :recv_queue, :send_queue
|
|
31
|
+
|
|
32
|
+
# @param connection [Connection]
|
|
33
|
+
#
|
|
34
|
+
def connection_added(connection)
|
|
35
|
+
identity = connection.peer_identity
|
|
36
|
+
identity = SecureRandom.bytes(5) if identity.nil? || identity.empty?
|
|
37
|
+
@connections_by_identity[identity] = connection
|
|
38
|
+
@identity_by_connection[connection] = identity
|
|
39
|
+
|
|
40
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
41
|
+
[identity, *msg]
|
|
42
|
+
end
|
|
43
|
+
@tasks << task if task
|
|
44
|
+
|
|
45
|
+
start_send_pump unless @send_pump_started
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param connection [Connection]
|
|
49
|
+
#
|
|
50
|
+
def connection_removed(connection)
|
|
51
|
+
identity = @identity_by_connection.delete(connection)
|
|
52
|
+
@connections_by_identity.delete(identity) if identity
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Enqueues a message for sending.
|
|
56
|
+
#
|
|
57
|
+
# @param parts [Array<String>]
|
|
58
|
+
#
|
|
59
|
+
def enqueue(parts)
|
|
60
|
+
if @engine.options.router_mandatory?
|
|
61
|
+
identity = parts.first
|
|
62
|
+
unless @connections_by_identity[identity]
|
|
63
|
+
raise SocketError, "no route to identity #{identity.inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
@send_queue.enqueue(parts)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stop
|
|
70
|
+
@tasks.each(&:stop)
|
|
71
|
+
@tasks.clear
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def send_pump_idle? = @send_pump_idle
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def start_send_pump
|
|
79
|
+
@send_pump_started = true
|
|
80
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
81
|
+
loop do
|
|
82
|
+
@send_pump_idle = true
|
|
83
|
+
batch = [@send_queue.dequeue]
|
|
84
|
+
@send_pump_idle = false
|
|
85
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
86
|
+
|
|
87
|
+
@written.clear
|
|
88
|
+
batch.each do |parts|
|
|
89
|
+
identity = parts.first
|
|
90
|
+
conn = @connections_by_identity[identity]
|
|
91
|
+
next unless conn # silently drop (peer may have disconnected)
|
|
92
|
+
begin
|
|
93
|
+
conn.write_message(parts[1..])
|
|
94
|
+
@written << conn
|
|
95
|
+
rescue *CONNECTION_LOST
|
|
96
|
+
# will be cleaned up
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@written.each do |conn|
|
|
101
|
+
conn.flush
|
|
102
|
+
rescue *CONNECTION_LOST
|
|
103
|
+
# will be cleaned up
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# SCATTER socket routing: round-robin send to GATHER peers.
|
|
6
|
+
#
|
|
7
|
+
class Scatter
|
|
8
|
+
include RoundRobin
|
|
9
|
+
|
|
10
|
+
# @param engine [Engine]
|
|
11
|
+
#
|
|
12
|
+
def initialize(engine)
|
|
13
|
+
@engine = engine
|
|
14
|
+
@tasks = []
|
|
15
|
+
init_round_robin(engine)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# @return [Async::LimitedQueue]
|
|
20
|
+
#
|
|
21
|
+
attr_reader :send_queue
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# SCATTER is write-only.
|
|
25
|
+
#
|
|
26
|
+
def recv_queue
|
|
27
|
+
raise "SCATTER sockets cannot receive"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# @param connection [Connection]
|
|
32
|
+
#
|
|
33
|
+
def connection_added(connection)
|
|
34
|
+
@connections << connection
|
|
35
|
+
signal_connection_available
|
|
36
|
+
update_direct_pipe
|
|
37
|
+
start_send_pump unless @send_pump_started
|
|
38
|
+
start_reaper(connection)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# @param connection [Connection]
|
|
43
|
+
#
|
|
44
|
+
def connection_removed(connection)
|
|
45
|
+
@connections.delete(connection)
|
|
46
|
+
update_direct_pipe
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# @param parts [Array<String>]
|
|
51
|
+
#
|
|
52
|
+
def enqueue(parts)
|
|
53
|
+
enqueue_round_robin(parts)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Stops all background tasks (send pump, reapers).
|
|
58
|
+
#
|
|
59
|
+
def stop
|
|
60
|
+
@tasks.each(&:stop)
|
|
61
|
+
@tasks.clear
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Detects peer disconnection on write-only sockets by
|
|
68
|
+
# blocking on a receive that only returns on disconnect.
|
|
69
|
+
#
|
|
70
|
+
# @param conn [Connection]
|
|
71
|
+
#
|
|
72
|
+
def start_reaper(conn)
|
|
73
|
+
return if conn.is_a?(Transport::Inproc::DirectPipe)
|
|
74
|
+
@tasks << @engine.spawn_pump_task(annotation: "reaper") do
|
|
75
|
+
conn.receive_message
|
|
76
|
+
rescue *CONNECTION_LOST
|
|
77
|
+
@engine.connection_lost(conn)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
# @param engine [Engine]
|
|
15
|
+
#
|
|
16
|
+
def initialize(engine)
|
|
17
|
+
@engine = engine
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
20
|
+
@connections_by_routing_id = {}
|
|
21
|
+
@routing_id_by_connection = {}
|
|
22
|
+
@tasks = []
|
|
23
|
+
@send_pump_started = false
|
|
24
|
+
@send_pump_idle = true
|
|
25
|
+
@written = Set.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Async::LimitedQueue]
|
|
29
|
+
#
|
|
30
|
+
attr_reader :recv_queue, :send_queue
|
|
31
|
+
|
|
32
|
+
# @param connection [Connection]
|
|
33
|
+
#
|
|
34
|
+
def connection_added(connection)
|
|
35
|
+
routing_id = SecureRandom.bytes(4)
|
|
36
|
+
@connections_by_routing_id[routing_id] = connection
|
|
37
|
+
@routing_id_by_connection[connection] = routing_id
|
|
38
|
+
|
|
39
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
40
|
+
[routing_id, *msg]
|
|
41
|
+
end
|
|
42
|
+
@tasks << task if task
|
|
43
|
+
|
|
44
|
+
start_send_pump unless @send_pump_started
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param connection [Connection]
|
|
48
|
+
#
|
|
49
|
+
def connection_removed(connection)
|
|
50
|
+
routing_id = @routing_id_by_connection.delete(connection)
|
|
51
|
+
@connections_by_routing_id.delete(routing_id) if routing_id
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param parts [Array<String>]
|
|
55
|
+
#
|
|
56
|
+
def enqueue(parts)
|
|
57
|
+
@send_queue.enqueue(parts)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stop
|
|
61
|
+
@tasks.each(&:stop)
|
|
62
|
+
@tasks.clear
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_pump_idle? = @send_pump_idle
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def start_send_pump
|
|
70
|
+
@send_pump_started = true
|
|
71
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
72
|
+
loop do
|
|
73
|
+
@send_pump_idle = true
|
|
74
|
+
batch = [@send_queue.dequeue]
|
|
75
|
+
@send_pump_idle = false
|
|
76
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
77
|
+
|
|
78
|
+
@written.clear
|
|
79
|
+
batch.each do |parts|
|
|
80
|
+
routing_id = parts.first
|
|
81
|
+
conn = @connections_by_routing_id[routing_id]
|
|
82
|
+
next unless conn # silently drop if peer gone
|
|
83
|
+
begin
|
|
84
|
+
conn.write_message(parts[1..])
|
|
85
|
+
@written << conn
|
|
86
|
+
rescue *CONNECTION_LOST
|
|
87
|
+
# will be cleaned up
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@written.each do |conn|
|
|
92
|
+
conn.flush
|
|
93
|
+
rescue *CONNECTION_LOST
|
|
94
|
+
# will be cleaned up
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# SUB socket routing: subscription-based receive from PUB peers.
|
|
6
|
+
#
|
|
7
|
+
# Sends SUBSCRIBE/CANCEL commands to connected PUB peers.
|
|
8
|
+
#
|
|
9
|
+
class Sub
|
|
10
|
+
|
|
11
|
+
# @param engine [Engine]
|
|
12
|
+
#
|
|
13
|
+
def initialize(engine)
|
|
14
|
+
@engine = engine
|
|
15
|
+
@connections = []
|
|
16
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
17
|
+
@subscriptions = Set.new
|
|
18
|
+
@tasks = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Async::LimitedQueue]
|
|
22
|
+
#
|
|
23
|
+
attr_reader :recv_queue
|
|
24
|
+
|
|
25
|
+
# @param connection [Connection]
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
@connections << connection
|
|
29
|
+
# Send existing subscriptions to new peer
|
|
30
|
+
@subscriptions.each do |prefix|
|
|
31
|
+
connection.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
|
|
32
|
+
end
|
|
33
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
34
|
+
@tasks << task if task
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param connection [Connection]
|
|
38
|
+
#
|
|
39
|
+
def connection_removed(connection)
|
|
40
|
+
@connections.delete(connection)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# SUB is read-only.
|
|
44
|
+
#
|
|
45
|
+
def enqueue(_parts)
|
|
46
|
+
raise "SUB sockets cannot send"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Subscribes to a topic prefix.
|
|
50
|
+
#
|
|
51
|
+
# @param prefix [String]
|
|
52
|
+
#
|
|
53
|
+
def subscribe(prefix)
|
|
54
|
+
@subscriptions << prefix
|
|
55
|
+
@connections.each do |conn|
|
|
56
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Unsubscribes from a topic prefix.
|
|
61
|
+
#
|
|
62
|
+
# @param prefix [String]
|
|
63
|
+
#
|
|
64
|
+
def unsubscribe(prefix)
|
|
65
|
+
@subscriptions.delete(prefix)
|
|
66
|
+
@connections.each do |conn|
|
|
67
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stop
|
|
72
|
+
@tasks.each(&:stop)
|
|
73
|
+
@tasks.clear
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# XPUB socket routing: like PUB but exposes subscription messages.
|
|
6
|
+
#
|
|
7
|
+
# Subscription/unsubscription messages from peers are delivered to
|
|
8
|
+
# the application as data frames: \x01 + prefix for subscribe,
|
|
9
|
+
# \x00 + prefix for unsubscribe.
|
|
10
|
+
#
|
|
11
|
+
class XPub
|
|
12
|
+
include FanOut
|
|
13
|
+
|
|
14
|
+
# @param engine [Engine]
|
|
15
|
+
#
|
|
16
|
+
def initialize(engine)
|
|
17
|
+
@engine = engine
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@tasks = []
|
|
20
|
+
init_fan_out(engine)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Async::LimitedQueue]
|
|
24
|
+
#
|
|
25
|
+
attr_reader :recv_queue, :send_queue
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
#
|
|
29
|
+
def connection_added(connection)
|
|
30
|
+
@connections << connection
|
|
31
|
+
@subscriptions[connection] = Set.new
|
|
32
|
+
start_subscription_listener(connection)
|
|
33
|
+
start_send_pump unless @send_pump_started
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param connection [Connection]
|
|
37
|
+
#
|
|
38
|
+
def connection_removed(connection)
|
|
39
|
+
@connections.delete(connection)
|
|
40
|
+
@subscriptions.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
|
+
# Expose subscription to application as data message.
|
|
58
|
+
#
|
|
59
|
+
def on_subscribe(conn, prefix)
|
|
60
|
+
super
|
|
61
|
+
@recv_queue.enqueue(["\x01#{prefix}".b])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Expose unsubscription to application as data message.
|
|
65
|
+
#
|
|
66
|
+
def on_cancel(conn, prefix)
|
|
67
|
+
super
|
|
68
|
+
@recv_queue.enqueue(["\x00#{prefix}".b])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|