omq 0.9.0 → 0.11.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 +129 -0
- data/README.md +28 -3
- data/lib/omq/channel.rb +5 -5
- data/lib/omq/client_server.rb +10 -10
- data/lib/omq/engine.rb +702 -0
- data/lib/omq/options.rb +48 -0
- data/lib/omq/pair.rb +4 -4
- data/lib/omq/peer.rb +5 -5
- data/lib/omq/pub_sub.rb +18 -18
- data/lib/omq/push_pull.rb +6 -6
- data/lib/omq/queue_interface.rb +73 -0
- data/lib/omq/radio_dish.rb +6 -6
- data/lib/omq/reactor.rb +128 -0
- data/lib/omq/readable.rb +44 -0
- data/lib/omq/req_rep.rb +8 -8
- data/lib/omq/router_dealer.rb +8 -8
- 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 +140 -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 +150 -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 +8 -8
- data/lib/omq/single_frame.rb +18 -0
- data/lib/omq/socket.rb +32 -11
- 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/transport/tls.rb +146 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +66 -0
- data/lib/omq.rb +64 -4
- metadata +34 -33
- 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,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# DEALER socket routing: round-robin send, fair-queue receive.
|
|
6
|
+
#
|
|
7
|
+
# No envelope manipulation — messages pass through unchanged.
|
|
8
|
+
#
|
|
9
|
+
class Dealer
|
|
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)
|
|
32
|
+
@tasks << task if task
|
|
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
|
+
update_direct_pipe
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param parts [Array<String>]
|
|
44
|
+
#
|
|
45
|
+
def enqueue(parts)
|
|
46
|
+
enqueue_round_robin(parts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
def stop
|
|
51
|
+
@tasks.each(&:stop)
|
|
52
|
+
@tasks.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# DISH socket routing: group-based receive from RADIO peers.
|
|
6
|
+
#
|
|
7
|
+
# Sends JOIN/LEAVE commands to connected RADIO peers.
|
|
8
|
+
# Receives two-frame messages (group + body) from RADIO.
|
|
9
|
+
#
|
|
10
|
+
class Dish
|
|
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
|
+
@groups = 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 group memberships to new peer
|
|
31
|
+
@groups.each do |group|
|
|
32
|
+
connection.send_command(Protocol::ZMTP::Codec::Command.join(group))
|
|
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
|
+
# DISH is read-only.
|
|
45
|
+
#
|
|
46
|
+
def enqueue(_parts)
|
|
47
|
+
raise "DISH sockets cannot send"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Joins a group.
|
|
51
|
+
#
|
|
52
|
+
# @param group [String]
|
|
53
|
+
#
|
|
54
|
+
def join(group)
|
|
55
|
+
@groups << group
|
|
56
|
+
@connections.each do |conn|
|
|
57
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.join(group))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Leaves a group.
|
|
62
|
+
#
|
|
63
|
+
# @param group [String]
|
|
64
|
+
#
|
|
65
|
+
def leave(group)
|
|
66
|
+
@groups.delete(group)
|
|
67
|
+
@connections.each do |conn|
|
|
68
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.leave(group))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stop
|
|
73
|
+
@tasks.each(&:stop)
|
|
74
|
+
@tasks.clear
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Mixin for routing strategies that fan-out to subscribers.
|
|
6
|
+
#
|
|
7
|
+
# Manages per-connection subscription sets, subscription command
|
|
8
|
+
# listeners, and a send pump that delivers to all matching peers.
|
|
9
|
+
#
|
|
10
|
+
# Including classes must call `init_fan_out(engine)` from
|
|
11
|
+
# their #initialize.
|
|
12
|
+
#
|
|
13
|
+
module FanOut
|
|
14
|
+
attr_reader :subscriber_joined
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def init_fan_out(engine)
|
|
19
|
+
@connections = []
|
|
20
|
+
@subscriptions = {} # connection => Set of prefixes
|
|
21
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
22
|
+
@send_pump_started = false
|
|
23
|
+
@send_pump_idle = true
|
|
24
|
+
@conflate = engine.options.conflate
|
|
25
|
+
@subscriber_joined = Async::Promise.new
|
|
26
|
+
@written = Set.new
|
|
27
|
+
@latest = {} if @conflate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] whether the connection is subscribed to the topic
|
|
31
|
+
#
|
|
32
|
+
def subscribed?(conn, topic)
|
|
33
|
+
subs = @subscriptions[conn]
|
|
34
|
+
return false unless subs
|
|
35
|
+
subs.any? { |prefix| topic.start_with?(prefix) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Called when a subscription command is received from a peer.
|
|
39
|
+
# Override in subclasses to expose subscriptions to the
|
|
40
|
+
# application (e.g. XPUB enqueues to recv_queue).
|
|
41
|
+
#
|
|
42
|
+
# @param conn [Connection]
|
|
43
|
+
# @param prefix [String]
|
|
44
|
+
#
|
|
45
|
+
def on_subscribe(conn, prefix)
|
|
46
|
+
@subscriptions[conn] << prefix.b.freeze
|
|
47
|
+
@subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Called when a cancel command is received from a peer.
|
|
51
|
+
# Override in subclasses (e.g. XPUB enqueues to recv_queue).
|
|
52
|
+
#
|
|
53
|
+
# @param conn [Connection]
|
|
54
|
+
# @param prefix [String]
|
|
55
|
+
#
|
|
56
|
+
def on_cancel(conn, prefix)
|
|
57
|
+
@subscriptions[conn]&.delete(prefix)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] true when the send pump is idle (not sending a batch)
|
|
61
|
+
def send_pump_idle? = @send_pump_idle
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def start_send_pump
|
|
65
|
+
@send_pump_started = true
|
|
66
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
67
|
+
loop do
|
|
68
|
+
@send_pump_idle = true
|
|
69
|
+
batch = [@send_queue.dequeue]
|
|
70
|
+
@send_pump_idle = false
|
|
71
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
72
|
+
|
|
73
|
+
@written.clear
|
|
74
|
+
|
|
75
|
+
if @conflate
|
|
76
|
+
# Keep only the last matching message per connection.
|
|
77
|
+
@latest.clear
|
|
78
|
+
batch.each do |parts|
|
|
79
|
+
topic = parts.first || EMPTY_BINARY
|
|
80
|
+
@connections.each do |conn|
|
|
81
|
+
next unless subscribed?(conn, topic)
|
|
82
|
+
@latest[conn] = parts
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
@latest.each do |conn, parts|
|
|
86
|
+
begin
|
|
87
|
+
conn.write_message(parts)
|
|
88
|
+
@written << conn
|
|
89
|
+
rescue *CONNECTION_LOST
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
batch.each do |parts|
|
|
94
|
+
topic = parts.first || EMPTY_BINARY
|
|
95
|
+
wire_bytes = nil
|
|
96
|
+
|
|
97
|
+
@connections.each do |conn|
|
|
98
|
+
next unless subscribed?(conn, topic)
|
|
99
|
+
begin
|
|
100
|
+
if conn.respond_to?(:curve?) && conn.curve?
|
|
101
|
+
conn.write_message(parts)
|
|
102
|
+
elsif conn.respond_to?(:write_wire)
|
|
103
|
+
wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(parts)
|
|
104
|
+
conn.write_wire(wire_bytes)
|
|
105
|
+
else
|
|
106
|
+
conn.write_message(parts)
|
|
107
|
+
end
|
|
108
|
+
@written << conn
|
|
109
|
+
rescue *CONNECTION_LOST
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
@written.each do |conn|
|
|
116
|
+
conn.flush
|
|
117
|
+
rescue *CONNECTION_LOST
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def start_subscription_listener(conn)
|
|
124
|
+
@tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
|
|
125
|
+
loop do
|
|
126
|
+
frame = conn.read_frame
|
|
127
|
+
next unless frame.command?
|
|
128
|
+
cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
|
|
129
|
+
case cmd.name
|
|
130
|
+
when "SUBSCRIBE" then on_subscribe(conn, cmd.data)
|
|
131
|
+
when "CANCEL" then on_cancel(conn, cmd.data)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
rescue *CONNECTION_LOST
|
|
135
|
+
@engine.connection_lost(conn)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# GATHER socket routing: fair-queue receive from SCATTER peers.
|
|
6
|
+
#
|
|
7
|
+
class Gather
|
|
8
|
+
# @param engine [Engine]
|
|
9
|
+
#
|
|
10
|
+
def initialize(engine)
|
|
11
|
+
@engine = engine
|
|
12
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
13
|
+
@tasks = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Async::LimitedQueue]
|
|
17
|
+
#
|
|
18
|
+
attr_reader :recv_queue
|
|
19
|
+
|
|
20
|
+
# @param connection [Connection]
|
|
21
|
+
#
|
|
22
|
+
def connection_added(connection)
|
|
23
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
24
|
+
@tasks << task if task
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
#
|
|
29
|
+
def connection_removed(connection)
|
|
30
|
+
# recv pump stops on CONNECTION_LOST
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# GATHER is read-only.
|
|
34
|
+
#
|
|
35
|
+
def enqueue(_parts)
|
|
36
|
+
raise "GATHER sockets cannot send"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
def stop
|
|
41
|
+
@tasks.each(&:stop)
|
|
42
|
+
@tasks.clear
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# PAIR socket routing: exclusive 1-to-1 bidirectional.
|
|
6
|
+
#
|
|
7
|
+
# Only one peer connection is allowed. Messages flow through
|
|
8
|
+
# internal send/recv queues backed by Async::LimitedQueue.
|
|
9
|
+
#
|
|
10
|
+
class Pair
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@connection = nil
|
|
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_idle = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Async::LimitedQueue]
|
|
24
|
+
#
|
|
25
|
+
attr_reader :recv_queue, :send_queue
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
# @raise [RuntimeError] if a connection already exists
|
|
29
|
+
#
|
|
30
|
+
def connection_added(connection)
|
|
31
|
+
raise "PAIR allows only one peer" if @connection
|
|
32
|
+
@connection = connection
|
|
33
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
34
|
+
@tasks << task if task
|
|
35
|
+
start_send_pump(connection) unless connection.is_a?(Transport::Inproc::DirectPipe)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param connection [Connection]
|
|
39
|
+
#
|
|
40
|
+
def connection_removed(connection)
|
|
41
|
+
if @connection == connection
|
|
42
|
+
@connection = nil
|
|
43
|
+
@send_pump&.stop
|
|
44
|
+
@send_pump = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param parts [Array<String>]
|
|
49
|
+
#
|
|
50
|
+
def enqueue(parts)
|
|
51
|
+
conn = @connection
|
|
52
|
+
if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
|
|
53
|
+
conn.send_message(parts)
|
|
54
|
+
else
|
|
55
|
+
@send_queue.enqueue(parts)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
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(conn)
|
|
70
|
+
@send_pump = @engine.spawn_pump_task(annotation: "send pump") do
|
|
71
|
+
loop do
|
|
72
|
+
@send_pump_idle = true
|
|
73
|
+
batch = [@send_queue.dequeue]
|
|
74
|
+
@send_pump_idle = false
|
|
75
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
76
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
77
|
+
conn.flush
|
|
78
|
+
end
|
|
79
|
+
rescue *CONNECTION_LOST
|
|
80
|
+
@engine.connection_lost(conn)
|
|
81
|
+
end
|
|
82
|
+
@tasks << @send_pump
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
module Routing
|
|
7
|
+
# PEER socket routing: bidirectional multi-peer 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 Peer
|
|
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,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# PUB socket routing: fan-out to all subscribers.
|
|
6
|
+
#
|
|
7
|
+
# Listens for SUBSCRIBE/CANCEL commands from peers.
|
|
8
|
+
# Drops messages if a subscriber's connection write fails.
|
|
9
|
+
#
|
|
10
|
+
class Pub
|
|
11
|
+
include FanOut
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@tasks = []
|
|
18
|
+
init_fan_out(engine)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Async::LimitedQueue]
|
|
22
|
+
#
|
|
23
|
+
attr_reader :send_queue
|
|
24
|
+
|
|
25
|
+
# PUB is write-only.
|
|
26
|
+
#
|
|
27
|
+
def recv_queue
|
|
28
|
+
raise "PUB sockets cannot receive"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param connection [Connection]
|
|
32
|
+
#
|
|
33
|
+
def connection_added(connection)
|
|
34
|
+
@connections << connection
|
|
35
|
+
@subscriptions[connection] = Set.new
|
|
36
|
+
start_subscription_listener(connection)
|
|
37
|
+
start_send_pump unless @send_pump_started
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param connection [Connection]
|
|
41
|
+
#
|
|
42
|
+
def connection_removed(connection)
|
|
43
|
+
@connections.delete(connection)
|
|
44
|
+
@subscriptions.delete(connection)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param parts [Array<String>]
|
|
48
|
+
#
|
|
49
|
+
def enqueue(parts)
|
|
50
|
+
@send_queue.enqueue(parts)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#
|
|
54
|
+
def stop
|
|
55
|
+
@tasks.each(&:stop)
|
|
56
|
+
@tasks.clear
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# PULL socket routing: fair-queue receive from PUSH peers.
|
|
6
|
+
#
|
|
7
|
+
class Pull
|
|
8
|
+
# @param engine [Engine]
|
|
9
|
+
#
|
|
10
|
+
def initialize(engine)
|
|
11
|
+
@engine = engine
|
|
12
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
13
|
+
@tasks = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Async::LimitedQueue]
|
|
17
|
+
#
|
|
18
|
+
attr_reader :recv_queue
|
|
19
|
+
|
|
20
|
+
# @param connection [Connection]
|
|
21
|
+
#
|
|
22
|
+
def connection_added(connection)
|
|
23
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
24
|
+
@tasks << task if task
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
#
|
|
29
|
+
def connection_removed(connection)
|
|
30
|
+
# recv pump stops on EOFError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# PULL is read-only.
|
|
34
|
+
#
|
|
35
|
+
def enqueue(_parts)
|
|
36
|
+
raise "PULL sockets cannot send"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
def stop
|
|
41
|
+
@tasks.each(&:stop)
|
|
42
|
+
@tasks.clear
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# PUSH socket routing: round-robin send to PULL peers.
|
|
6
|
+
#
|
|
7
|
+
class Push
|
|
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
|
+
# PUSH is write-only.
|
|
25
|
+
#
|
|
26
|
+
def recv_queue
|
|
27
|
+
raise "PUSH 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. Without
|
|
68
|
+
# this, a dead peer is only noticed on the next send — which
|
|
69
|
+
# may succeed if the kernel send buffer absorbs the data.
|
|
70
|
+
#
|
|
71
|
+
def start_reaper(conn)
|
|
72
|
+
return if conn.is_a?(Transport::Inproc::DirectPipe)
|
|
73
|
+
@tasks << @engine.spawn_pump_task(annotation: "reaper") do
|
|
74
|
+
conn.receive_message # blocks until peer disconnects
|
|
75
|
+
rescue *CONNECTION_LOST
|
|
76
|
+
@engine.connection_lost(conn)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|