omq 0.11.0 → 0.13.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 +143 -0
- data/README.md +3 -1
- data/lib/omq/drop_queue.rb +54 -0
- data/lib/omq/engine/connection_setup.rb +47 -0
- data/lib/omq/engine/heartbeat.rb +40 -0
- data/lib/omq/engine/reconnect.rb +56 -0
- data/lib/omq/engine/recv_pump.rb +76 -0
- data/lib/omq/engine.rb +145 -371
- data/lib/omq/monitor_event.rb +16 -0
- data/lib/omq/options.rb +5 -3
- data/lib/omq/pub_sub.rb +9 -8
- data/lib/omq/routing/conn_send_pump.rb +36 -0
- data/lib/omq/routing/dealer.rb +8 -10
- data/lib/omq/routing/fair_queue.rb +144 -0
- data/lib/omq/routing/fair_recv.rb +27 -0
- data/lib/omq/routing/fan_out.rb +116 -63
- data/lib/omq/routing/pair.rb +39 -20
- data/lib/omq/routing/pub.rb +5 -7
- data/lib/omq/routing/pull.rb +5 -4
- data/lib/omq/routing/push.rb +3 -10
- data/lib/omq/routing/rep.rb +31 -51
- data/lib/omq/routing/req.rb +15 -12
- data/lib/omq/routing/round_robin.rb +82 -72
- data/lib/omq/routing/router.rb +23 -48
- data/lib/omq/routing/sub.rb +8 -6
- data/lib/omq/routing/xpub.rb +8 -4
- data/lib/omq/routing/xsub.rb +43 -27
- data/lib/omq/routing.rb +44 -11
- data/lib/omq/socket.rb +46 -5
- data/lib/omq/transport/inproc/direct_pipe.rb +162 -0
- data/lib/omq/transport/inproc.rb +37 -200
- data/lib/omq/transport/ipc.rb +16 -4
- data/lib/omq/transport/tcp.rb +31 -8
- data/lib/omq/version.rb +1 -1
- data/lib/omq.rb +5 -19
- metadata +11 -16
- data/lib/omq/channel.rb +0 -14
- data/lib/omq/client_server.rb +0 -37
- data/lib/omq/peer.rb +0 -26
- data/lib/omq/radio_dish.rb +0 -74
- data/lib/omq/routing/channel.rb +0 -83
- data/lib/omq/routing/client.rb +0 -56
- data/lib/omq/routing/dish.rb +0 -78
- data/lib/omq/routing/gather.rb +0 -46
- data/lib/omq/routing/peer.rb +0 -101
- data/lib/omq/routing/radio.rb +0 -150
- data/lib/omq/routing/scatter.rb +0 -82
- data/lib/omq/routing/server.rb +0 -101
- data/lib/omq/scatter_gather.rb +0 -23
- data/lib/omq/single_frame.rb +0 -18
- data/lib/omq/transport/tls.rb +0 -146
data/lib/omq/options.rb
CHANGED
|
@@ -23,10 +23,11 @@ module OMQ
|
|
|
23
23
|
@heartbeat_interval = nil # seconds, nil = disabled
|
|
24
24
|
@heartbeat_ttl = nil # seconds, nil = use heartbeat_interval
|
|
25
25
|
@heartbeat_timeout = nil # seconds, nil = use heartbeat_interval
|
|
26
|
-
@max_message_size =
|
|
26
|
+
@max_message_size = 1 << 20 # bytes (1 MiB default)
|
|
27
27
|
@conflate = false
|
|
28
|
+
@on_mute = :block # :block, :drop_newest, :drop_oldest
|
|
28
29
|
@mechanism = Protocol::ZMTP::Mechanism::Null.new
|
|
29
|
-
@
|
|
30
|
+
@qos = 0 # 0 = fire-and-forget, 1 = at-least-once (see omq-qos gem)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
attr_accessor :send_hwm, :recv_hwm,
|
|
@@ -36,8 +37,9 @@ module OMQ
|
|
|
36
37
|
:reconnect_interval,
|
|
37
38
|
:heartbeat_interval, :heartbeat_ttl, :heartbeat_timeout,
|
|
38
39
|
:max_message_size,
|
|
40
|
+
:on_mute,
|
|
39
41
|
:mechanism,
|
|
40
|
-
:
|
|
42
|
+
:qos
|
|
41
43
|
|
|
42
44
|
alias_method :router_mandatory?, :router_mandatory
|
|
43
45
|
alias_method :recv_timeout, :read_timeout
|
data/lib/omq/pub_sub.rb
CHANGED
|
@@ -4,8 +4,8 @@ module OMQ
|
|
|
4
4
|
class PUB < Socket
|
|
5
5
|
include Writable
|
|
6
6
|
|
|
7
|
-
def initialize(endpoints = nil, linger: 0, conflate: false, backend: nil)
|
|
8
|
-
_init_engine(:PUB, linger: linger, conflate: conflate, backend: backend)
|
|
7
|
+
def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, conflate: false, backend: nil)
|
|
8
|
+
_init_engine(:PUB, linger: linger, on_mute: on_mute, conflate: conflate, backend: backend)
|
|
9
9
|
_attach(endpoints, default: :bind)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -23,9 +23,10 @@ module OMQ
|
|
|
23
23
|
# @param linger [Integer]
|
|
24
24
|
# @param subscribe [String, nil] subscription prefix; +nil+ (default)
|
|
25
25
|
# means no subscription — call {#subscribe} explicitly.
|
|
26
|
+
# @param on_mute [Symbol] :block (default), :drop_newest, or :drop_oldest
|
|
26
27
|
#
|
|
27
|
-
def initialize(endpoints = nil, linger: 0, subscribe: nil, backend: nil)
|
|
28
|
-
_init_engine(:SUB, linger: linger, backend: backend)
|
|
28
|
+
def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
|
|
29
|
+
_init_engine(:SUB, linger: linger, on_mute: on_mute, backend: backend)
|
|
29
30
|
_attach(endpoints, default: :connect)
|
|
30
31
|
self.subscribe(subscribe) unless subscribe.nil?
|
|
31
32
|
end
|
|
@@ -53,8 +54,8 @@ module OMQ
|
|
|
53
54
|
include Readable
|
|
54
55
|
include Writable
|
|
55
56
|
|
|
56
|
-
def initialize(endpoints = nil, linger: 0, backend: nil)
|
|
57
|
-
_init_engine(:XPUB, linger: linger, backend: backend)
|
|
57
|
+
def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, backend: nil)
|
|
58
|
+
_init_engine(:XPUB, linger: linger, on_mute: on_mute, backend: backend)
|
|
58
59
|
_attach(endpoints, default: :bind)
|
|
59
60
|
end
|
|
60
61
|
end
|
|
@@ -68,8 +69,8 @@ module OMQ
|
|
|
68
69
|
# @param subscribe [String, nil] subscription prefix; +nil+ (default)
|
|
69
70
|
# means no subscription — send a subscribe frame explicitly.
|
|
70
71
|
#
|
|
71
|
-
def initialize(endpoints = nil, linger: 0, subscribe: nil, backend: nil)
|
|
72
|
-
_init_engine(:XSUB, linger: linger, backend: backend)
|
|
72
|
+
def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
|
|
73
|
+
_init_engine(:XSUB, linger: linger, on_mute: on_mute, backend: backend)
|
|
73
74
|
_attach(endpoints, default: :connect)
|
|
74
75
|
send("\x01#{subscribe}".b) unless subscribe.nil?
|
|
75
76
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Starts a dedicated send pump for one per-connection send queue.
|
|
6
|
+
#
|
|
7
|
+
# Used by Router and Rep, which have per-connection queues but do not
|
|
8
|
+
# include the RoundRobin mixin.
|
|
9
|
+
#
|
|
10
|
+
class ConnSendPump
|
|
11
|
+
# Spawns the pump task and registers it in +tasks+.
|
|
12
|
+
#
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
# @param conn [Connection]
|
|
15
|
+
# @param q [Async::LimitedQueue]
|
|
16
|
+
# @param tasks [Array]
|
|
17
|
+
# @return [Async::Task]
|
|
18
|
+
#
|
|
19
|
+
def self.start(engine, conn, q, tasks)
|
|
20
|
+
task = engine.spawn_pump_task(annotation: "send pump") do
|
|
21
|
+
loop do
|
|
22
|
+
batch = [q.dequeue]
|
|
23
|
+
Routing.drain_send_queue(q, batch)
|
|
24
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
25
|
+
conn.flush
|
|
26
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
27
|
+
engine.connection_lost(conn)
|
|
28
|
+
break
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
tasks << task
|
|
32
|
+
task
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/omq/routing/dealer.rb
CHANGED
|
@@ -8,36 +8,35 @@ module OMQ
|
|
|
8
8
|
#
|
|
9
9
|
class Dealer
|
|
10
10
|
include RoundRobin
|
|
11
|
+
include FairRecv
|
|
11
12
|
|
|
12
13
|
# @param engine [Engine]
|
|
13
14
|
#
|
|
14
15
|
def initialize(engine)
|
|
15
16
|
@engine = engine
|
|
16
|
-
@recv_queue =
|
|
17
|
+
@recv_queue = FairQueue.new
|
|
17
18
|
@tasks = []
|
|
18
19
|
init_round_robin(engine)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
# @return [
|
|
22
|
+
# @return [FairQueue]
|
|
22
23
|
#
|
|
23
|
-
attr_reader :recv_queue
|
|
24
|
+
attr_reader :recv_queue
|
|
24
25
|
|
|
25
26
|
# @param connection [Connection]
|
|
26
27
|
#
|
|
27
28
|
def connection_added(connection)
|
|
28
29
|
@connections << connection
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
32
|
-
@tasks << task if task
|
|
33
|
-
start_send_pump unless @send_pump_started
|
|
30
|
+
add_fair_recv_connection(connection)
|
|
31
|
+
add_round_robin_send_connection(connection)
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
# @param connection [Connection]
|
|
37
35
|
#
|
|
38
36
|
def connection_removed(connection)
|
|
39
37
|
@connections.delete(connection)
|
|
40
|
-
|
|
38
|
+
@recv_queue.remove_queue(connection)
|
|
39
|
+
remove_round_robin_send_connection(connection)
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
# @param parts [Array<String>]
|
|
@@ -51,7 +50,6 @@ module OMQ
|
|
|
51
50
|
@tasks.each(&:stop)
|
|
52
51
|
@tasks.clear
|
|
53
52
|
end
|
|
54
|
-
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Per-connection recv queue aggregator.
|
|
6
|
+
#
|
|
7
|
+
# Maintains one bounded queue per connected peer. #dequeue
|
|
8
|
+
# returns the next available message from any peer in fair
|
|
9
|
+
# round-robin order, blocking until one arrives.
|
|
10
|
+
#
|
|
11
|
+
# Recv pumps do not enqueue directly — they write through a
|
|
12
|
+
# SignalingQueue wrapper, which also wakes a blocked #dequeue.
|
|
13
|
+
#
|
|
14
|
+
class FairQueue
|
|
15
|
+
def initialize
|
|
16
|
+
@queues = [] # ordered list of per-connection inner queues
|
|
17
|
+
@mapping = {} # connection => inner queue
|
|
18
|
+
@cycle = @queues.cycle # live reference — sees adds/removes
|
|
19
|
+
@condition = Async::Condition.new
|
|
20
|
+
@pending = 0 # signals received before #dequeue waits
|
|
21
|
+
@closed = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Registers a per-connection queue. Called when a connection is added.
|
|
25
|
+
#
|
|
26
|
+
# @param conn [Connection]
|
|
27
|
+
# @param q [Async::LimitedQueue]
|
|
28
|
+
#
|
|
29
|
+
def add_queue(conn, q)
|
|
30
|
+
@mapping[conn] = q
|
|
31
|
+
@queues << q
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Removes the per-connection queue for a disconnected peer.
|
|
35
|
+
#
|
|
36
|
+
# If the queue is empty it is removed immediately. If it still has
|
|
37
|
+
# pending messages it is kept in @queues so the application can drain
|
|
38
|
+
# them via #dequeue; it will be cleaned up lazily by try_dequeue once
|
|
39
|
+
# it is empty.
|
|
40
|
+
#
|
|
41
|
+
# @param conn [Connection]
|
|
42
|
+
#
|
|
43
|
+
def remove_queue(conn)
|
|
44
|
+
q = @mapping.delete(conn)
|
|
45
|
+
return unless q
|
|
46
|
+
@queues.delete(q) if q.empty?
|
|
47
|
+
# Non-empty orphaned queues stay in @queues until drained
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Wakes a blocked #dequeue. Called by SignalingQueue after each enqueue.
|
|
51
|
+
#
|
|
52
|
+
def signal
|
|
53
|
+
@pending += 1
|
|
54
|
+
@condition.signal
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the next message from any per-connection queue, in fair
|
|
58
|
+
# round-robin order. Blocks until a message is available.
|
|
59
|
+
#
|
|
60
|
+
# Pass +timeout: 0+ for a non-blocking poll (returns nil immediately
|
|
61
|
+
# if no messages are available).
|
|
62
|
+
#
|
|
63
|
+
# @param timeout [Numeric, nil] 0 = non-blocking, nil = block forever
|
|
64
|
+
# @return [Array<String>, nil]
|
|
65
|
+
#
|
|
66
|
+
def dequeue(timeout: nil)
|
|
67
|
+
return try_dequeue if timeout == 0
|
|
68
|
+
|
|
69
|
+
loop do
|
|
70
|
+
return nil if @closed && @queues.all?(&:empty?)
|
|
71
|
+
|
|
72
|
+
msg = try_dequeue
|
|
73
|
+
return msg if msg
|
|
74
|
+
|
|
75
|
+
if @pending > 0
|
|
76
|
+
@pending -= 1
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@condition.wait
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Injects a nil sentinel to unblock a waiting #dequeue.
|
|
85
|
+
# Called by Engine on close or fatal error.
|
|
86
|
+
#
|
|
87
|
+
def push(nil_sentinel)
|
|
88
|
+
@closed = true
|
|
89
|
+
@condition.signal
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
#
|
|
94
|
+
def empty?
|
|
95
|
+
@queues.all?(&:empty?)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Tries each per-connection queue once in round-robin order.
|
|
101
|
+
# Returns the first message found, or nil if all are empty.
|
|
102
|
+
# Lazily removes empty orphaned queues (disconnected peers that have
|
|
103
|
+
# been fully drained).
|
|
104
|
+
#
|
|
105
|
+
def try_dequeue
|
|
106
|
+
@queues.size.times do
|
|
107
|
+
q = begin
|
|
108
|
+
@cycle.next
|
|
109
|
+
rescue StopIteration
|
|
110
|
+
@cycle = @queues.cycle
|
|
111
|
+
break
|
|
112
|
+
end
|
|
113
|
+
msg = q.dequeue(timeout: 0)
|
|
114
|
+
return msg if msg
|
|
115
|
+
if q.empty? && !@mapping.value?(q)
|
|
116
|
+
@queues.delete(q)
|
|
117
|
+
break
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Wraps a per-connection bounded queue so that each #enqueue also
|
|
126
|
+
# signals the FairQueue to wake a blocked #dequeue.
|
|
127
|
+
#
|
|
128
|
+
class SignalingQueue
|
|
129
|
+
def initialize(inner, fair_queue)
|
|
130
|
+
@inner = inner
|
|
131
|
+
@fair = fair_queue
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def enqueue(msg)
|
|
135
|
+
@inner.enqueue(msg)
|
|
136
|
+
@fair.signal
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def dequeue(timeout: nil) = @inner.dequeue(timeout: timeout)
|
|
140
|
+
def empty? = @inner.empty?
|
|
141
|
+
def push(item) = @inner.push(item)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Mixin that adds per-connection recv queue setup for fair-queued sockets.
|
|
6
|
+
#
|
|
7
|
+
# Including classes must have @engine, @recv_queue (FairQueue), and @tasks.
|
|
8
|
+
#
|
|
9
|
+
module FairRecv
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# Creates a per-connection recv queue, registers it with @recv_queue,
|
|
13
|
+
# and starts a recv pump for the connection. Called from #connection_added.
|
|
14
|
+
#
|
|
15
|
+
# @param conn [Connection]
|
|
16
|
+
# @yield [msg] optional per-message transform
|
|
17
|
+
#
|
|
18
|
+
def add_fair_recv_connection(conn, &transform)
|
|
19
|
+
conn_q = Routing.build_queue(@engine.options.recv_hwm, :block)
|
|
20
|
+
signaling = SignalingQueue.new(conn_q, @recv_queue)
|
|
21
|
+
@recv_queue.add_queue(conn, conn_q)
|
|
22
|
+
task = @engine.start_recv_pump(conn, signaling, &transform)
|
|
23
|
+
@tasks << task if task
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/omq/routing/fan_out.rb
CHANGED
|
@@ -5,7 +5,13 @@ 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.
|
|
@@ -13,20 +19,26 @@ module OMQ
|
|
|
13
19
|
module FanOut
|
|
14
20
|
attr_reader :subscriber_joined
|
|
15
21
|
|
|
22
|
+
# True when all per-connection send queues are empty.
|
|
23
|
+
# Used by Engine#drain_send_queues during linger.
|
|
24
|
+
#
|
|
25
|
+
def send_queues_drained?
|
|
26
|
+
@conn_queues.values.all?(&:empty?)
|
|
27
|
+
end
|
|
28
|
+
|
|
16
29
|
private
|
|
17
30
|
|
|
18
31
|
def init_fan_out(engine)
|
|
19
|
-
@connections =
|
|
32
|
+
@connections = Set.new
|
|
20
33
|
@subscriptions = {} # connection => Set of prefixes
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
@send_pump_idle = true
|
|
34
|
+
@conn_queues = {} # connection => per-connection send queue
|
|
35
|
+
@conn_send_tasks = {} # connection => send pump task
|
|
24
36
|
@conflate = engine.options.conflate
|
|
25
37
|
@subscriber_joined = Async::Promise.new
|
|
26
|
-
@written = Set.new
|
|
27
38
|
@latest = {} if @conflate
|
|
28
39
|
end
|
|
29
40
|
|
|
41
|
+
|
|
30
42
|
# @return [Boolean] whether the connection is subscribed to the topic
|
|
31
43
|
#
|
|
32
44
|
def subscribed?(conn, topic)
|
|
@@ -57,69 +69,50 @@ module OMQ
|
|
|
57
69
|
@subscriptions[conn]&.delete(prefix)
|
|
58
70
|
end
|
|
59
71
|
|
|
60
|
-
# @return [Boolean] true when the send pump is idle (not sending a batch)
|
|
61
|
-
def send_pump_idle? = @send_pump_idle
|
|
62
72
|
|
|
73
|
+
# Creates a per-connection send queue and starts its send pump.
|
|
74
|
+
# Call from #connection_added.
|
|
75
|
+
#
|
|
76
|
+
# @param conn [Connection]
|
|
77
|
+
#
|
|
78
|
+
def add_fan_out_send_connection(conn)
|
|
79
|
+
q = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
80
|
+
@conn_queues[conn] = q
|
|
81
|
+
start_conn_send_pump(conn, q)
|
|
82
|
+
end
|
|
63
83
|
|
|
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
84
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
85
|
+
# Stops the per-connection send pump and removes the queue.
|
|
86
|
+
# Call from #connection_removed.
|
|
87
|
+
#
|
|
88
|
+
# @param conn [Connection]
|
|
89
|
+
#
|
|
90
|
+
def remove_fan_out_send_connection(conn)
|
|
91
|
+
@conn_queues.delete(conn)
|
|
92
|
+
@conn_send_tasks.delete(conn)&.stop
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Fans a message out to every connected peer's send queue.
|
|
97
|
+
# Subscription filtering happens in the per-connection send pump so
|
|
98
|
+
# that late-arriving subscriptions (e.g. inproc connect-before-subscribe)
|
|
99
|
+
# are respected: a message enqueued before the async subscription listener
|
|
100
|
+
# has processed SUBSCRIBE commands will still be delivered correctly.
|
|
101
|
+
#
|
|
102
|
+
# Per-connection queues use :block (Async::LimitedQueue) for
|
|
103
|
+
# backpressure: when a subscriber's queue is full, the publisher
|
|
104
|
+
# yields until the send pump drains it. This matches the old
|
|
105
|
+
# shared-queue behavior and keeps the publisher fiber-friendly.
|
|
106
|
+
#
|
|
107
|
+
# @param parts [Array<String>]
|
|
108
|
+
#
|
|
109
|
+
def fan_out_enqueue(parts)
|
|
110
|
+
@connections.each do |conn|
|
|
111
|
+
@conn_queues[conn]&.enqueue(parts)
|
|
120
112
|
end
|
|
121
113
|
end
|
|
122
114
|
|
|
115
|
+
|
|
123
116
|
def start_subscription_listener(conn)
|
|
124
117
|
@tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
|
|
125
118
|
loop do
|
|
@@ -135,6 +128,66 @@ module OMQ
|
|
|
135
128
|
@engine.connection_lost(conn)
|
|
136
129
|
end
|
|
137
130
|
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Starts a dedicated send pump for one subscriber connection.
|
|
134
|
+
# Uses write_wire (pre-encoded bytes) for non-CURVE TCP connections
|
|
135
|
+
# to avoid re-encoding the same message N times during fan-out.
|
|
136
|
+
# In conflate mode, drains the batch and keeps only the latest
|
|
137
|
+
# message per topic before writing.
|
|
138
|
+
#
|
|
139
|
+
# @param conn [Connection]
|
|
140
|
+
# @param q [Async::LimitedQueue, DropQueue]
|
|
141
|
+
#
|
|
142
|
+
def start_conn_send_pump(conn, q)
|
|
143
|
+
use_wire = conn.respond_to?(:write_wire) && !(conn.respond_to?(:curve?) && conn.curve?)
|
|
144
|
+
task = @conflate ? start_conn_send_pump_conflate(conn, q) : start_conn_send_pump_normal(conn, q, use_wire)
|
|
145
|
+
@conn_send_tasks[conn] = task
|
|
146
|
+
@tasks << task
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def start_conn_send_pump_normal(conn, q, use_wire)
|
|
150
|
+
@engine.spawn_pump_task(annotation: "send pump") do
|
|
151
|
+
loop do
|
|
152
|
+
batch = [q.dequeue]
|
|
153
|
+
Routing.drain_send_queue(q, batch)
|
|
154
|
+
conn.flush if write_matching_batch(conn, batch, use_wire)
|
|
155
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
156
|
+
@engine.connection_lost(conn)
|
|
157
|
+
break
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def write_matching_batch(conn, batch, use_wire)
|
|
163
|
+
sent = false
|
|
164
|
+
batch.each do |parts|
|
|
165
|
+
next unless subscribed?(conn, parts.first || EMPTY_BINARY)
|
|
166
|
+
use_wire ? conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts)) : conn.write_message(parts)
|
|
167
|
+
sent = true
|
|
168
|
+
end
|
|
169
|
+
sent
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def start_conn_send_pump_conflate(conn, q)
|
|
174
|
+
@engine.spawn_pump_task(annotation: "send pump") do
|
|
175
|
+
loop do
|
|
176
|
+
batch = [q.dequeue]
|
|
177
|
+
Routing.drain_send_queue(q, batch)
|
|
178
|
+
# Keep only the latest message that matches the subscription.
|
|
179
|
+
latest = batch.reverse.find { |parts| subscribed?(conn, parts.first || EMPTY_BINARY) }
|
|
180
|
+
next unless latest
|
|
181
|
+
begin
|
|
182
|
+
conn.write_message(latest)
|
|
183
|
+
conn.flush
|
|
184
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
185
|
+
@engine.connection_lost(conn)
|
|
186
|
+
break
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
138
191
|
end
|
|
139
192
|
end
|
|
140
193
|
end
|
data/lib/omq/routing/pair.rb
CHANGED
|
@@ -4,25 +4,28 @@ 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
|
-
# @return [
|
|
26
|
+
# @return [FairQueue]
|
|
24
27
|
#
|
|
25
|
-
attr_reader :recv_queue
|
|
28
|
+
attr_reader :recv_queue
|
|
26
29
|
|
|
27
30
|
# @param connection [Connection]
|
|
28
31
|
# @raise [RuntimeError] if a connection already exists
|
|
@@ -30,9 +33,16 @@ module OMQ
|
|
|
30
33
|
def connection_added(connection)
|
|
31
34
|
raise "PAIR allows only one peer" if @connection
|
|
32
35
|
@connection = connection
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
add_fair_recv_connection(connection)
|
|
38
|
+
|
|
39
|
+
unless connection.is_a?(Transport::Inproc::DirectPipe)
|
|
40
|
+
@send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
41
|
+
while (msg = @staging_queue.dequeue(timeout: 0))
|
|
42
|
+
@send_queue.enqueue(msg)
|
|
43
|
+
end
|
|
44
|
+
start_send_pump(connection)
|
|
45
|
+
end
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
# @param connection [Connection]
|
|
@@ -40,6 +50,8 @@ module OMQ
|
|
|
40
50
|
def connection_removed(connection)
|
|
41
51
|
if @connection == connection
|
|
42
52
|
@connection = nil
|
|
53
|
+
@recv_queue.remove_queue(connection)
|
|
54
|
+
@send_queue = nil
|
|
43
55
|
@send_pump&.stop
|
|
44
56
|
@send_pump = nil
|
|
45
57
|
end
|
|
@@ -51,8 +63,10 @@ module OMQ
|
|
|
51
63
|
conn = @connection
|
|
52
64
|
if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
|
|
53
65
|
conn.send_message(parts)
|
|
54
|
-
|
|
66
|
+
elsif @send_queue
|
|
55
67
|
@send_queue.enqueue(parts)
|
|
68
|
+
else
|
|
69
|
+
@staging_queue.enqueue(parts)
|
|
56
70
|
end
|
|
57
71
|
end
|
|
58
72
|
|
|
@@ -62,22 +76,27 @@ module OMQ
|
|
|
62
76
|
@tasks.clear
|
|
63
77
|
end
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
# True when the staging and send queues are empty.
|
|
80
|
+
#
|
|
81
|
+
def send_queues_drained?
|
|
82
|
+
@staging_queue.empty? && (@send_queue.nil? || @send_queue.empty?)
|
|
83
|
+
end
|
|
66
84
|
|
|
67
85
|
private
|
|
68
86
|
|
|
69
87
|
def start_send_pump(conn)
|
|
70
88
|
@send_pump = @engine.spawn_pump_task(annotation: "send pump") do
|
|
71
89
|
loop do
|
|
72
|
-
@send_pump_idle = true
|
|
73
90
|
batch = [@send_queue.dequeue]
|
|
74
|
-
@send_pump_idle = false
|
|
75
91
|
Routing.drain_send_queue(@send_queue, batch)
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
begin
|
|
93
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
94
|
+
conn.flush
|
|
95
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
96
|
+
@engine.connection_lost(conn)
|
|
97
|
+
break
|
|
98
|
+
end
|
|
78
99
|
end
|
|
79
|
-
rescue *CONNECTION_LOST
|
|
80
|
-
@engine.connection_lost(conn)
|
|
81
100
|
end
|
|
82
101
|
@tasks << @send_pump
|
|
83
102
|
end
|