omq 0.12.0 → 0.14.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 +84 -1
- data/README.md +27 -0
- data/lib/omq/drop_queue.rb +3 -0
- data/lib/omq/engine/connection_setup.rb +70 -0
- data/lib/omq/engine/heartbeat.rb +40 -0
- data/lib/omq/engine/maintenance.rb +35 -0
- data/lib/omq/engine/reconnect.rb +82 -0
- data/lib/omq/engine/recv_pump.rb +119 -0
- data/lib/omq/engine.rb +139 -304
- data/lib/omq/options.rb +44 -0
- data/lib/omq/pair.rb +6 -0
- data/lib/omq/pub_sub.rb +25 -0
- data/lib/omq/push_pull.rb +17 -0
- data/lib/omq/queue_interface.rb +1 -0
- data/lib/omq/readable.rb +2 -0
- data/lib/omq/req_rep.rb +13 -0
- data/lib/omq/router_dealer.rb +12 -0
- data/lib/omq/routing/conn_send_pump.rb +36 -0
- data/lib/omq/routing/dealer.rb +15 -10
- data/lib/omq/routing/fair_queue.rb +172 -0
- data/lib/omq/routing/fair_recv.rb +27 -0
- data/lib/omq/routing/fan_out.rb +127 -74
- data/lib/omq/routing/pair.rb +47 -20
- data/lib/omq/routing/pub.rb +12 -6
- data/lib/omq/routing/pull.rb +12 -4
- data/lib/omq/routing/push.rb +3 -12
- data/lib/omq/routing/rep.rb +41 -51
- data/lib/omq/routing/req.rb +15 -10
- data/lib/omq/routing/round_robin.rb +82 -63
- data/lib/omq/routing/router.rb +32 -48
- data/lib/omq/routing/sub.rb +18 -5
- data/lib/omq/routing/xpub.rb +15 -3
- data/lib/omq/routing/xsub.rb +53 -27
- data/lib/omq/routing.rb +29 -11
- data/lib/omq/socket.rb +25 -7
- data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
- data/lib/omq/transport/inproc.rb +41 -217
- data/lib/omq/transport/ipc.rb +7 -1
- data/lib/omq/transport/tcp.rb +12 -7
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +2 -0
- data/lib/omq.rb +4 -1
- metadata +14 -5
data/lib/omq/routing/fan_out.rb
CHANGED
|
@@ -5,40 +5,52 @@ module OMQ
|
|
|
5
5
|
# Mixin for routing strategies that fan-out to subscribers.
|
|
6
6
|
#
|
|
7
7
|
# Manages per-connection subscription sets, subscription command
|
|
8
|
-
# listeners, and
|
|
8
|
+
# listeners, and per-connection send queues/pumps that deliver
|
|
9
|
+
# to each matching peer independently.
|
|
10
|
+
#
|
|
11
|
+
# HWM is enforced per subscriber: each connection gets its own
|
|
12
|
+
# bounded send queue. DropQueues (for :drop_newest/:drop_oldest)
|
|
13
|
+
# silently drop messages for a slow subscriber without affecting
|
|
14
|
+
# others. LimitedQueues (for :block) block the publisher.
|
|
9
15
|
#
|
|
10
16
|
# Including classes must call `init_fan_out(engine)` from
|
|
11
17
|
# their #initialize.
|
|
12
18
|
#
|
|
13
19
|
module FanOut
|
|
20
|
+
# @return [Async::Promise] resolves when the first subscriber joins
|
|
21
|
+
#
|
|
14
22
|
attr_reader :subscriber_joined
|
|
15
23
|
|
|
16
|
-
# @return [Boolean] true when
|
|
17
|
-
|
|
24
|
+
# @return [Boolean] true when all per-connection send queues are empty
|
|
25
|
+
#
|
|
26
|
+
def send_queues_drained?
|
|
27
|
+
@conn_queues.values.all?(&:empty?)
|
|
28
|
+
end
|
|
18
29
|
|
|
19
30
|
private
|
|
20
31
|
|
|
21
32
|
def init_fan_out(engine)
|
|
22
|
-
@connections =
|
|
33
|
+
@connections = Set.new
|
|
23
34
|
@subscriptions = {} # connection => Set of prefixes
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
27
|
-
@send_pump_idle = true
|
|
35
|
+
@subscribe_all = Set.new # connections subscribed to "" (match-all fast path)
|
|
36
|
+
@conn_queues = {} # connection => per-connection send queue
|
|
37
|
+
@conn_send_tasks = {} # connection => send pump task
|
|
28
38
|
@conflate = engine.options.conflate
|
|
29
39
|
@subscriber_joined = Async::Promise.new
|
|
30
|
-
@written = Set.new
|
|
31
40
|
@latest = {} if @conflate
|
|
32
41
|
end
|
|
33
42
|
|
|
43
|
+
|
|
34
44
|
# @return [Boolean] whether the connection is subscribed to the topic
|
|
35
45
|
#
|
|
36
46
|
def subscribed?(conn, topic)
|
|
47
|
+
return true if @subscribe_all.include?(conn)
|
|
37
48
|
subs = @subscriptions[conn]
|
|
38
49
|
return false unless subs
|
|
39
50
|
subs.any? { |prefix| topic.start_with?(prefix) }
|
|
40
51
|
end
|
|
41
52
|
|
|
53
|
+
|
|
42
54
|
# Called when a subscription command is received from a peer.
|
|
43
55
|
# Override in subclasses to expose subscriptions to the
|
|
44
56
|
# application (e.g. XPUB enqueues to recv_queue).
|
|
@@ -48,9 +60,11 @@ module OMQ
|
|
|
48
60
|
#
|
|
49
61
|
def on_subscribe(conn, prefix)
|
|
50
62
|
@subscriptions[conn] << prefix.b.freeze
|
|
63
|
+
@subscribe_all.add(conn) if prefix.empty?
|
|
51
64
|
@subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
|
|
52
65
|
end
|
|
53
66
|
|
|
67
|
+
|
|
54
68
|
# Called when a cancel command is received from a peer.
|
|
55
69
|
# Override in subclasses (e.g. XPUB enqueues to recv_queue).
|
|
56
70
|
#
|
|
@@ -59,79 +73,54 @@ module OMQ
|
|
|
59
73
|
#
|
|
60
74
|
def on_cancel(conn, prefix)
|
|
61
75
|
@subscriptions[conn]&.delete(prefix)
|
|
76
|
+
@subscribe_all.delete(conn) if prefix.empty?
|
|
62
77
|
end
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
#
|
|
79
|
+
|
|
80
|
+
# Creates a per-connection send queue and starts its send pump.
|
|
81
|
+
# Call from #connection_added.
|
|
82
|
+
#
|
|
83
|
+
# @param conn [Connection]
|
|
66
84
|
#
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
def add_fan_out_send_connection(conn)
|
|
86
|
+
q = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
87
|
+
@conn_queues[conn] = q
|
|
88
|
+
start_conn_send_pump(conn, q)
|
|
71
89
|
end
|
|
72
90
|
|
|
73
91
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if @conflate
|
|
86
|
-
# Keep only the last matching message per connection.
|
|
87
|
-
@latest.clear
|
|
88
|
-
batch.each do |parts|
|
|
89
|
-
topic = parts.first || EMPTY_BINARY
|
|
90
|
-
@connections.each do |conn|
|
|
91
|
-
next unless subscribed?(conn, topic)
|
|
92
|
-
@latest[conn] = parts
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
@latest.each do |conn, parts|
|
|
96
|
-
next if muted?(conn)
|
|
97
|
-
begin
|
|
98
|
-
conn.write_message(parts)
|
|
99
|
-
@written << conn
|
|
100
|
-
rescue *CONNECTION_LOST
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
else
|
|
104
|
-
batch.each do |parts|
|
|
105
|
-
topic = parts.first || EMPTY_BINARY
|
|
106
|
-
wire_bytes = nil
|
|
107
|
-
|
|
108
|
-
@connections.each do |conn|
|
|
109
|
-
next unless subscribed?(conn, topic)
|
|
110
|
-
next if muted?(conn)
|
|
111
|
-
begin
|
|
112
|
-
if conn.respond_to?(:curve?) && conn.curve?
|
|
113
|
-
conn.write_message(parts)
|
|
114
|
-
elsif conn.respond_to?(:write_wire)
|
|
115
|
-
wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(parts)
|
|
116
|
-
conn.write_wire(wire_bytes)
|
|
117
|
-
else
|
|
118
|
-
conn.write_message(parts)
|
|
119
|
-
end
|
|
120
|
-
@written << conn
|
|
121
|
-
rescue *CONNECTION_LOST
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
92
|
+
# Stops the per-connection send pump and removes the queue.
|
|
93
|
+
# Call from #connection_removed.
|
|
94
|
+
#
|
|
95
|
+
# @param conn [Connection]
|
|
96
|
+
#
|
|
97
|
+
def remove_fan_out_send_connection(conn)
|
|
98
|
+
@subscribe_all.delete(conn)
|
|
99
|
+
@conn_queues.delete(conn)
|
|
100
|
+
@conn_send_tasks.delete(conn)&.stop
|
|
101
|
+
end
|
|
126
102
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
103
|
+
|
|
104
|
+
# Fans a message out to every connected peer's send queue.
|
|
105
|
+
# Subscription filtering happens in the per-connection send pump so
|
|
106
|
+
# that late-arriving subscriptions (e.g. inproc connect-before-subscribe)
|
|
107
|
+
# are respected: a message enqueued before the async subscription listener
|
|
108
|
+
# has processed SUBSCRIBE commands will still be delivered correctly.
|
|
109
|
+
#
|
|
110
|
+
# Per-connection queues use :block (Async::LimitedQueue) for
|
|
111
|
+
# backpressure: when a subscriber's queue is full, the publisher
|
|
112
|
+
# yields until the send pump drains it. This matches the old
|
|
113
|
+
# shared-queue behavior and keeps the publisher fiber-friendly.
|
|
114
|
+
#
|
|
115
|
+
# @param parts [Array<String>]
|
|
116
|
+
#
|
|
117
|
+
def fan_out_enqueue(parts)
|
|
118
|
+
@connections.each do |conn|
|
|
119
|
+
@conn_queues[conn].enqueue(parts)
|
|
132
120
|
end
|
|
133
121
|
end
|
|
134
122
|
|
|
123
|
+
|
|
135
124
|
def start_subscription_listener(conn)
|
|
136
125
|
@tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
|
|
137
126
|
loop do
|
|
@@ -139,14 +128,78 @@ module OMQ
|
|
|
139
128
|
next unless frame.command?
|
|
140
129
|
cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
|
|
141
130
|
case cmd.name
|
|
142
|
-
when "SUBSCRIBE"
|
|
143
|
-
|
|
131
|
+
when "SUBSCRIBE"
|
|
132
|
+
on_subscribe(conn, cmd.data)
|
|
133
|
+
when "CANCEL"
|
|
134
|
+
on_cancel(conn, cmd.data)
|
|
144
135
|
end
|
|
145
136
|
end
|
|
146
137
|
rescue *CONNECTION_LOST
|
|
147
138
|
@engine.connection_lost(conn)
|
|
148
139
|
end
|
|
149
140
|
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Starts a dedicated send pump for one subscriber connection.
|
|
144
|
+
# Uses write_wire (pre-encoded bytes) for non-encrypted TCP connections
|
|
145
|
+
# to avoid re-encoding the same message N times during fan-out.
|
|
146
|
+
# In conflate mode, drains the batch and keeps only the latest
|
|
147
|
+
# message per topic before writing.
|
|
148
|
+
#
|
|
149
|
+
# @param conn [Connection]
|
|
150
|
+
# @param q [Async::LimitedQueue, DropQueue]
|
|
151
|
+
#
|
|
152
|
+
def start_conn_send_pump(conn, q)
|
|
153
|
+
use_wire = conn.respond_to?(:write_wire) && !conn.encrypted?
|
|
154
|
+
task = @conflate ? start_conn_send_pump_conflate(conn, q) : start_conn_send_pump_normal(conn, q, use_wire)
|
|
155
|
+
@conn_send_tasks[conn] = task
|
|
156
|
+
@tasks << task
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def start_conn_send_pump_normal(conn, q, use_wire)
|
|
161
|
+
@engine.spawn_pump_task(annotation: "send pump") do
|
|
162
|
+
loop do
|
|
163
|
+
batch = [q.dequeue]
|
|
164
|
+
Routing.drain_send_queue(q, batch)
|
|
165
|
+
conn.flush if write_matching_batch(conn, batch, use_wire)
|
|
166
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
167
|
+
@engine.connection_lost(conn)
|
|
168
|
+
break
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def write_matching_batch(conn, batch, use_wire)
|
|
175
|
+
sent = false
|
|
176
|
+
batch.each do |parts|
|
|
177
|
+
next unless subscribed?(conn, parts.first || EMPTY_BINARY)
|
|
178
|
+
use_wire ? conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts)) : conn.write_message(parts)
|
|
179
|
+
sent = true
|
|
180
|
+
end
|
|
181
|
+
sent
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def start_conn_send_pump_conflate(conn, q)
|
|
186
|
+
@engine.spawn_pump_task(annotation: "send pump") do
|
|
187
|
+
loop do
|
|
188
|
+
batch = [q.dequeue]
|
|
189
|
+
Routing.drain_send_queue(q, batch)
|
|
190
|
+
# Keep only the latest message that matches the subscription.
|
|
191
|
+
latest = batch.reverse.find { |parts| subscribed?(conn, parts.first || EMPTY_BINARY) }
|
|
192
|
+
next unless latest
|
|
193
|
+
begin
|
|
194
|
+
conn.write_message(latest)
|
|
195
|
+
conn.flush
|
|
196
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
197
|
+
@engine.connection_lost(conn)
|
|
198
|
+
break
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
150
203
|
end
|
|
151
204
|
end
|
|
152
205
|
end
|
data/lib/omq/routing/pair.rb
CHANGED
|
@@ -4,25 +4,29 @@ module OMQ
|
|
|
4
4
|
module Routing
|
|
5
5
|
# PAIR socket routing: exclusive 1-to-1 bidirectional.
|
|
6
6
|
#
|
|
7
|
-
# Only one peer connection is allowed.
|
|
8
|
-
#
|
|
7
|
+
# Only one peer connection is allowed. Send and recv queues are
|
|
8
|
+
# created per-connection (and destroyed on disconnection) so
|
|
9
|
+
# HWM is consistent with multi-peer socket types.
|
|
9
10
|
#
|
|
10
11
|
class Pair
|
|
12
|
+
include FairRecv
|
|
11
13
|
|
|
12
14
|
# @param engine [Engine]
|
|
13
15
|
#
|
|
14
16
|
def initialize(engine)
|
|
15
|
-
@engine
|
|
16
|
-
@connection
|
|
17
|
-
@recv_queue
|
|
18
|
-
@send_queue
|
|
17
|
+
@engine = engine
|
|
18
|
+
@connection = nil
|
|
19
|
+
@recv_queue = FairQueue.new
|
|
20
|
+
@send_queue = nil # created per-connection
|
|
21
|
+
@staging_queue = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
22
|
+
@send_pump = nil
|
|
19
23
|
@tasks = []
|
|
20
|
-
@send_pump_idle = true
|
|
21
24
|
end
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
# @return [FairQueue]
|
|
24
28
|
#
|
|
25
|
-
attr_reader :recv_queue
|
|
29
|
+
attr_reader :recv_queue
|
|
26
30
|
|
|
27
31
|
# @param connection [Connection]
|
|
28
32
|
# @raise [RuntimeError] if a connection already exists
|
|
@@ -30,54 +34,77 @@ module OMQ
|
|
|
30
34
|
def connection_added(connection)
|
|
31
35
|
raise "PAIR allows only one peer" if @connection
|
|
32
36
|
@connection = connection
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
add_fair_recv_connection(connection)
|
|
39
|
+
|
|
40
|
+
unless connection.is_a?(Transport::Inproc::DirectPipe)
|
|
41
|
+
@send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
42
|
+
while (msg = @staging_queue.dequeue(timeout: 0))
|
|
43
|
+
@send_queue.enqueue(msg)
|
|
44
|
+
end
|
|
45
|
+
start_send_pump(connection)
|
|
46
|
+
end
|
|
36
47
|
end
|
|
37
48
|
|
|
49
|
+
|
|
38
50
|
# @param connection [Connection]
|
|
39
51
|
#
|
|
40
52
|
def connection_removed(connection)
|
|
41
53
|
if @connection == connection
|
|
42
54
|
@connection = nil
|
|
55
|
+
@recv_queue.remove_queue(connection)
|
|
56
|
+
@send_queue = nil
|
|
43
57
|
@send_pump&.stop
|
|
44
58
|
@send_pump = nil
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
|
|
62
|
+
|
|
48
63
|
# @param parts [Array<String>]
|
|
49
64
|
#
|
|
50
65
|
def enqueue(parts)
|
|
51
66
|
conn = @connection
|
|
52
67
|
if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
|
|
53
68
|
conn.send_message(parts)
|
|
54
|
-
|
|
69
|
+
elsif @send_queue
|
|
55
70
|
@send_queue.enqueue(parts)
|
|
71
|
+
else
|
|
72
|
+
@staging_queue.enqueue(parts)
|
|
56
73
|
end
|
|
57
74
|
end
|
|
58
75
|
|
|
76
|
+
|
|
77
|
+
# Stops all background tasks.
|
|
78
|
+
#
|
|
79
|
+
# @return [void]
|
|
59
80
|
#
|
|
60
81
|
def stop
|
|
61
82
|
@tasks.each(&:stop)
|
|
62
83
|
@tasks.clear
|
|
63
84
|
end
|
|
64
85
|
|
|
65
|
-
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] true when the staging and send queues are empty
|
|
88
|
+
#
|
|
89
|
+
def send_queues_drained?
|
|
90
|
+
@staging_queue.empty? && (@send_queue.nil? || @send_queue.empty?)
|
|
91
|
+
end
|
|
66
92
|
|
|
67
93
|
private
|
|
68
94
|
|
|
69
95
|
def start_send_pump(conn)
|
|
70
96
|
@send_pump = @engine.spawn_pump_task(annotation: "send pump") do
|
|
71
97
|
loop do
|
|
72
|
-
@send_pump_idle = true
|
|
73
98
|
batch = [@send_queue.dequeue]
|
|
74
|
-
@send_pump_idle = false
|
|
75
99
|
Routing.drain_send_queue(@send_queue, batch)
|
|
76
|
-
|
|
77
|
-
|
|
100
|
+
begin
|
|
101
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
102
|
+
conn.flush
|
|
103
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
104
|
+
@engine.connection_lost(conn)
|
|
105
|
+
break
|
|
106
|
+
end
|
|
78
107
|
end
|
|
79
|
-
rescue *CONNECTION_LOST
|
|
80
|
-
@engine.connection_lost(conn)
|
|
81
108
|
end
|
|
82
109
|
@tasks << @send_pump
|
|
83
110
|
end
|
data/lib/omq/routing/pub.rb
CHANGED
|
@@ -5,7 +5,8 @@ module OMQ
|
|
|
5
5
|
# PUB socket routing: fan-out to all subscribers.
|
|
6
6
|
#
|
|
7
7
|
# Listens for SUBSCRIBE/CANCEL commands from peers.
|
|
8
|
-
#
|
|
8
|
+
# Each subscriber gets its own bounded send queue; slow subscribers
|
|
9
|
+
# are muted via the socket's on_mute strategy (drop by default).
|
|
9
10
|
#
|
|
10
11
|
class Pub
|
|
11
12
|
include FanOut
|
|
@@ -18,9 +19,6 @@ module OMQ
|
|
|
18
19
|
init_fan_out(engine)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# @return [Async::LimitedQueue]
|
|
22
|
-
#
|
|
23
|
-
attr_reader :send_queue
|
|
24
22
|
|
|
25
23
|
# PUB is write-only.
|
|
26
24
|
#
|
|
@@ -28,28 +26,36 @@ module OMQ
|
|
|
28
26
|
raise "PUB sockets cannot receive"
|
|
29
27
|
end
|
|
30
28
|
|
|
29
|
+
|
|
31
30
|
# @param connection [Connection]
|
|
32
31
|
#
|
|
33
32
|
def connection_added(connection)
|
|
34
33
|
@connections << connection
|
|
35
34
|
@subscriptions[connection] = Set.new
|
|
36
35
|
start_subscription_listener(connection)
|
|
37
|
-
|
|
36
|
+
add_fan_out_send_connection(connection)
|
|
38
37
|
end
|
|
39
38
|
|
|
39
|
+
|
|
40
40
|
# @param connection [Connection]
|
|
41
41
|
#
|
|
42
42
|
def connection_removed(connection)
|
|
43
43
|
@connections.delete(connection)
|
|
44
44
|
@subscriptions.delete(connection)
|
|
45
|
+
remove_fan_out_send_connection(connection)
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
|
|
47
49
|
# @param parts [Array<String>]
|
|
48
50
|
#
|
|
49
51
|
def enqueue(parts)
|
|
50
|
-
|
|
52
|
+
fan_out_enqueue(parts)
|
|
51
53
|
end
|
|
52
54
|
|
|
55
|
+
|
|
56
|
+
# Stops all background tasks.
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
53
59
|
#
|
|
54
60
|
def stop
|
|
55
61
|
@tasks.each(&:stop)
|
data/lib/omq/routing/pull.rb
CHANGED
|
@@ -5,37 +5,45 @@ module OMQ
|
|
|
5
5
|
# PULL socket routing: fair-queue receive from PUSH peers.
|
|
6
6
|
#
|
|
7
7
|
class Pull
|
|
8
|
+
include FairRecv
|
|
8
9
|
# @param engine [Engine]
|
|
9
10
|
#
|
|
10
11
|
def initialize(engine)
|
|
11
12
|
@engine = engine
|
|
12
|
-
@recv_queue =
|
|
13
|
+
@recv_queue = FairQueue.new
|
|
13
14
|
@tasks = []
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
# @return [FairQueue]
|
|
17
19
|
#
|
|
18
20
|
attr_reader :recv_queue
|
|
19
21
|
|
|
20
22
|
# @param connection [Connection]
|
|
21
23
|
#
|
|
22
24
|
def connection_added(connection)
|
|
23
|
-
|
|
24
|
-
@tasks << task if task
|
|
25
|
+
add_fair_recv_connection(connection)
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
# @param connection [Connection]
|
|
28
30
|
#
|
|
29
31
|
def connection_removed(connection)
|
|
32
|
+
@recv_queue.remove_queue(connection)
|
|
30
33
|
# recv pump stops on EOFError
|
|
31
34
|
end
|
|
32
35
|
|
|
36
|
+
|
|
33
37
|
# PULL is read-only.
|
|
34
38
|
#
|
|
35
39
|
def enqueue(_parts)
|
|
36
40
|
raise "PULL sockets cannot send"
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
|
|
44
|
+
# Stops all background tasks.
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
39
47
|
#
|
|
40
48
|
def stop
|
|
41
49
|
@tasks.each(&:stop)
|
data/lib/omq/routing/push.rb
CHANGED
|
@@ -16,11 +16,6 @@ module OMQ
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
# @return [Async::LimitedQueue]
|
|
20
|
-
#
|
|
21
|
-
attr_reader :send_queue
|
|
22
|
-
|
|
23
|
-
|
|
24
19
|
# PUSH is write-only.
|
|
25
20
|
#
|
|
26
21
|
def recv_queue
|
|
@@ -32,9 +27,7 @@ module OMQ
|
|
|
32
27
|
#
|
|
33
28
|
def connection_added(connection)
|
|
34
29
|
@connections << connection
|
|
35
|
-
|
|
36
|
-
update_direct_pipe
|
|
37
|
-
start_send_pump unless @send_pump_started
|
|
30
|
+
add_round_robin_send_connection(connection)
|
|
38
31
|
start_reaper(connection)
|
|
39
32
|
end
|
|
40
33
|
|
|
@@ -43,7 +36,7 @@ module OMQ
|
|
|
43
36
|
#
|
|
44
37
|
def connection_removed(connection)
|
|
45
38
|
@connections.delete(connection)
|
|
46
|
-
|
|
39
|
+
remove_round_robin_send_connection(connection)
|
|
47
40
|
end
|
|
48
41
|
|
|
49
42
|
|
|
@@ -54,7 +47,7 @@ module OMQ
|
|
|
54
47
|
end
|
|
55
48
|
|
|
56
49
|
|
|
57
|
-
# Stops all background tasks (send
|
|
50
|
+
# Stops all background tasks (send pumps, reapers).
|
|
58
51
|
#
|
|
59
52
|
def stop
|
|
60
53
|
@tasks.each(&:stop)
|
|
@@ -76,8 +69,6 @@ module OMQ
|
|
|
76
69
|
@engine.connection_lost(conn)
|
|
77
70
|
end
|
|
78
71
|
end
|
|
79
|
-
|
|
80
|
-
|
|
81
72
|
end
|
|
82
73
|
end
|
|
83
74
|
end
|
data/lib/omq/routing/rep.rb
CHANGED
|
@@ -9,93 +9,83 @@ module OMQ
|
|
|
9
9
|
# on send.
|
|
10
10
|
#
|
|
11
11
|
class Rep
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
include FairRecv
|
|
13
|
+
|
|
14
14
|
EMPTY_FRAME = "".b.freeze
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
# @param engine [Engine]
|
|
18
|
+
#
|
|
16
19
|
def initialize(engine)
|
|
17
|
-
@engine
|
|
18
|
-
@recv_queue
|
|
19
|
-
@
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
@send_pump_idle = true
|
|
24
|
-
@written = Set.new
|
|
20
|
+
@engine = engine
|
|
21
|
+
@recv_queue = FairQueue.new
|
|
22
|
+
@pending_replies = []
|
|
23
|
+
@conn_queues = {} # connection => per-connection send queue
|
|
24
|
+
@conn_send_tasks = {} # connection => send pump task
|
|
25
|
+
@tasks = []
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
# @return [FairQueue]
|
|
28
30
|
#
|
|
29
|
-
attr_reader :recv_queue
|
|
31
|
+
attr_reader :recv_queue
|
|
30
32
|
|
|
31
33
|
# @param connection [Connection]
|
|
32
34
|
#
|
|
33
35
|
def connection_added(connection)
|
|
34
|
-
|
|
36
|
+
add_fair_recv_connection(connection) do |msg|
|
|
35
37
|
delimiter = msg.index(&:empty?) || msg.size
|
|
36
38
|
envelope = msg[0, delimiter]
|
|
37
39
|
body = msg[(delimiter + 1)..] || []
|
|
38
40
|
@pending_replies << { conn: connection, envelope: envelope }
|
|
39
41
|
body
|
|
40
42
|
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
|
|
44
|
+
q = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
45
|
+
@conn_queues[connection] = q
|
|
46
|
+
@conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
|
|
45
50
|
# @param connection [Connection]
|
|
46
51
|
#
|
|
47
52
|
def connection_removed(connection)
|
|
48
|
-
# Remove any pending replies for this connection
|
|
49
53
|
@pending_replies.reject! { |r| r[:conn] == connection }
|
|
54
|
+
@recv_queue.remove_queue(connection)
|
|
55
|
+
@conn_queues.delete(connection)
|
|
56
|
+
@conn_send_tasks.delete(connection)&.stop
|
|
50
57
|
end
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
|
|
60
|
+
# Enqueues a reply. Routes to the connection that sent the matching
|
|
61
|
+
# request by consuming the next pending_reply entry.
|
|
53
62
|
#
|
|
54
63
|
# @param parts [Array<String>]
|
|
55
64
|
#
|
|
56
65
|
def enqueue(parts)
|
|
57
|
-
@
|
|
66
|
+
reply_info = @pending_replies.shift
|
|
67
|
+
return unless reply_info
|
|
68
|
+
conn = reply_info[:conn]
|
|
69
|
+
@conn_queues[conn]&.enqueue([*reply_info[:envelope], EMPTY_FRAME, *parts])
|
|
58
70
|
end
|
|
59
71
|
|
|
72
|
+
|
|
73
|
+
# Stops all background tasks.
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
#
|
|
60
77
|
def stop
|
|
61
78
|
@tasks.each(&:stop)
|
|
62
79
|
@tasks.clear
|
|
63
80
|
end
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
reply_info = @pending_replies.shift
|
|
81
|
-
next unless reply_info
|
|
82
|
-
conn = reply_info[:conn]
|
|
83
|
-
begin
|
|
84
|
-
conn.write_message([*reply_info[:envelope], EMPTY_FRAME, *parts])
|
|
85
|
-
@written << conn
|
|
86
|
-
rescue *CONNECTION_LOST
|
|
87
|
-
# connection lost mid-write
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
@written.each do |conn|
|
|
92
|
-
conn.flush
|
|
93
|
-
rescue *CONNECTION_LOST
|
|
94
|
-
# connection lost mid-flush
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
82
|
+
|
|
83
|
+
# @return [Boolean] true when all per-connection send queues are empty
|
|
84
|
+
#
|
|
85
|
+
def send_queues_drained?
|
|
86
|
+
@conn_queues.values.all?(&:empty?)
|
|
98
87
|
end
|
|
88
|
+
|
|
99
89
|
end
|
|
100
90
|
end
|
|
101
91
|
end
|