omq 0.22.0 → 0.23.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 +139 -0
- data/README.md +17 -21
- data/lib/omq/channel.rb +35 -0
- data/lib/omq/client_server.rb +72 -0
- data/lib/omq/constants.rb +68 -0
- data/lib/omq/engine/connection_lifecycle.rb +11 -3
- data/lib/omq/engine/heartbeat.rb +3 -4
- data/lib/omq/engine/maintenance.rb +4 -5
- data/lib/omq/engine/reconnect.rb +12 -11
- data/lib/omq/engine/recv_pump.rb +3 -4
- data/lib/omq/engine/socket_lifecycle.rb +26 -9
- data/lib/omq/engine.rb +196 -85
- data/lib/omq/peer.rb +49 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +122 -0
- data/lib/omq/reactor.rb +14 -5
- data/lib/omq/routing/channel.rb +110 -0
- data/lib/omq/routing/client.rb +70 -0
- data/lib/omq/routing/conn_send_pump.rb +12 -10
- data/lib/omq/routing/dealer.rb +1 -13
- data/lib/omq/routing/dish.rb +94 -0
- data/lib/omq/routing/fan_out.rb +14 -13
- data/lib/omq/routing/gather.rb +60 -0
- data/lib/omq/routing/pair.rb +7 -24
- data/lib/omq/routing/peer.rb +95 -0
- data/lib/omq/routing/pub.rb +0 -11
- data/lib/omq/routing/pull.rb +1 -13
- data/lib/omq/routing/push.rb +1 -10
- data/lib/omq/routing/radio.rb +187 -0
- data/lib/omq/routing/rep.rb +10 -20
- data/lib/omq/routing/req.rb +5 -17
- data/lib/omq/routing/round_robin.rb +17 -18
- data/lib/omq/routing/router.rb +3 -17
- data/lib/omq/routing/scatter.rb +77 -0
- data/lib/omq/routing/server.rb +90 -0
- data/lib/omq/routing/sub.rb +1 -13
- data/lib/omq/routing/xpub.rb +0 -11
- data/lib/omq/routing/xsub.rb +6 -23
- data/lib/omq/routing.rb +5 -2
- data/lib/omq/scatter_gather.rb +56 -0
- data/lib/omq/socket.rb +8 -23
- data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
- data/lib/omq/transport/inproc.rb +11 -3
- data/lib/omq/transport/ipc.rb +41 -13
- data/lib/omq/transport/tcp.rb +59 -23
- data/lib/omq/transport/udp.rb +281 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq.rb +9 -64
- metadata +16 -2
- data/lib/omq/monitor_event.rb +0 -16
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OMQ RADIO/DISH socket types with UDP transport (ZeroMQ RFC 48).
|
|
4
|
+
#
|
|
5
|
+
# Not loaded by +require "omq"+; opt in with:
|
|
6
|
+
#
|
|
7
|
+
# require "omq/radio_dish"
|
|
8
|
+
#
|
|
9
|
+
# Loading this file also registers the +udp://+ transport.
|
|
10
|
+
|
|
11
|
+
require "omq"
|
|
12
|
+
require_relative "routing/radio"
|
|
13
|
+
require_relative "routing/dish"
|
|
14
|
+
require_relative "transport/udp"
|
|
15
|
+
|
|
16
|
+
module OMQ
|
|
17
|
+
# Group-based publisher socket (ZeroMQ RFC 48).
|
|
18
|
+
#
|
|
19
|
+
# Sends messages to DISH peers that have joined the target group.
|
|
20
|
+
# Supports both TCP and UDP transports.
|
|
21
|
+
class RADIO < Socket
|
|
22
|
+
include Writable
|
|
23
|
+
|
|
24
|
+
# Creates a new RADIO socket.
|
|
25
|
+
#
|
|
26
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to bind to
|
|
27
|
+
# @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
|
|
28
|
+
# @param on_mute [Symbol] behaviour when HWM is reached (+:drop_newest+ or +:block+)
|
|
29
|
+
# @param conflate [Boolean] if true, keep only the latest message per group per peer
|
|
30
|
+
# @param backend [Object, nil] optional transport backend
|
|
31
|
+
def initialize(endpoints = nil, linger: Float::INFINITY, on_mute: :drop_newest, conflate: false, backend: nil)
|
|
32
|
+
init_engine(:RADIO, on_mute: on_mute, conflate: conflate, backend: backend)
|
|
33
|
+
@options.linger = linger
|
|
34
|
+
attach_endpoints(endpoints, default: :bind)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Publishes a message to a group.
|
|
39
|
+
#
|
|
40
|
+
# @param group [String] group name
|
|
41
|
+
# @param body [String] message body
|
|
42
|
+
# @return [self]
|
|
43
|
+
#
|
|
44
|
+
def publish(group, body)
|
|
45
|
+
parts = [group.b.freeze, body.b.freeze]
|
|
46
|
+
Reactor.run timeout: @options.write_timeout do
|
|
47
|
+
@engine.enqueue_send(parts)
|
|
48
|
+
end
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Sends a message to a group.
|
|
54
|
+
#
|
|
55
|
+
# @param message [String] message body (requires group: kwarg)
|
|
56
|
+
# @param group [String] group name
|
|
57
|
+
# @return [self]
|
|
58
|
+
#
|
|
59
|
+
def send(message, group: nil)
|
|
60
|
+
raise ArgumentError, "RADIO requires a group (use group: kwarg, publish, or << [group, body])" unless group
|
|
61
|
+
publish(group, message)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Sends a message to a group via [group, body] array.
|
|
66
|
+
#
|
|
67
|
+
# @param message [Array<String>] [group, body]
|
|
68
|
+
# @return [self]
|
|
69
|
+
#
|
|
70
|
+
def <<(message)
|
|
71
|
+
raise ArgumentError, "RADIO requires [group, body] array" unless message.is_a?(Array) && message.size == 2
|
|
72
|
+
publish(message[0], message[1])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Group-based subscriber socket (ZeroMQ RFC 48).
|
|
78
|
+
#
|
|
79
|
+
# Receives messages from RADIO peers for joined groups.
|
|
80
|
+
# Supports both TCP and UDP transports.
|
|
81
|
+
class DISH < Socket
|
|
82
|
+
include Readable
|
|
83
|
+
|
|
84
|
+
# Creates a new DISH socket.
|
|
85
|
+
#
|
|
86
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
|
|
87
|
+
# @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
|
|
88
|
+
# @param group [String, nil] initial group to join
|
|
89
|
+
# @param on_mute [Symbol] behaviour when HWM is reached (+:block+ or +:drop_newest+)
|
|
90
|
+
# @param backend [Object, nil] optional transport backend
|
|
91
|
+
def initialize(endpoints = nil, linger: Float::INFINITY, group: nil, on_mute: :block, backend: nil)
|
|
92
|
+
init_engine(:DISH, on_mute: on_mute, backend: backend)
|
|
93
|
+
@options.linger = linger
|
|
94
|
+
attach_endpoints(endpoints, default: :connect)
|
|
95
|
+
join(group) if group
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Joins a group.
|
|
100
|
+
#
|
|
101
|
+
# @param group [String]
|
|
102
|
+
# @return [void]
|
|
103
|
+
#
|
|
104
|
+
def join(group)
|
|
105
|
+
@engine.routing.join(group)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Leaves a group.
|
|
110
|
+
#
|
|
111
|
+
# @param group [String]
|
|
112
|
+
# @return [void]
|
|
113
|
+
#
|
|
114
|
+
def leave(group)
|
|
115
|
+
@engine.routing.leave(group)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
Routing.register(:RADIO, Routing::Radio)
|
|
121
|
+
Routing.register(:DISH, Routing::Dish)
|
|
122
|
+
end
|
data/lib/omq/reactor.rb
CHANGED
|
@@ -25,6 +25,11 @@ module OMQ
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class << self
|
|
28
|
+
# @return [Hash{Numeric => Integer}] linger value → active socket count
|
|
29
|
+
#
|
|
30
|
+
attr_reader :lingers
|
|
31
|
+
|
|
32
|
+
|
|
28
33
|
# Returns the root Async task inside the shared IO thread.
|
|
29
34
|
# Starts the thread exactly once (double-checked lock).
|
|
30
35
|
#
|
|
@@ -69,7 +74,7 @@ module OMQ
|
|
|
69
74
|
else
|
|
70
75
|
result = Async::Promise.new
|
|
71
76
|
root_task # ensure started
|
|
72
|
-
@work_queue
|
|
77
|
+
@work_queue << [block, result, timeout]
|
|
73
78
|
result.wait
|
|
74
79
|
end
|
|
75
80
|
end
|
|
@@ -89,9 +94,12 @@ module OMQ
|
|
|
89
94
|
# @param seconds [Numeric, nil] linger value
|
|
90
95
|
#
|
|
91
96
|
def untrack_linger(seconds)
|
|
92
|
-
key
|
|
97
|
+
key = seconds || 0
|
|
93
98
|
@lingers[key] -= 1
|
|
94
|
-
|
|
99
|
+
|
|
100
|
+
if @lingers[key] <= 0
|
|
101
|
+
@lingers.delete(key)
|
|
102
|
+
end
|
|
95
103
|
end
|
|
96
104
|
|
|
97
105
|
|
|
@@ -103,13 +111,14 @@ module OMQ
|
|
|
103
111
|
return unless @thread&.alive?
|
|
104
112
|
|
|
105
113
|
max_linger = @lingers.empty? ? 0 : @lingers.keys.max
|
|
106
|
-
|
|
114
|
+
|
|
115
|
+
@work_queue << nil if @work_queue
|
|
107
116
|
@thread&.join(max_linger + 1)
|
|
108
117
|
|
|
109
118
|
@thread = nil
|
|
110
119
|
@root_task = nil
|
|
111
120
|
@work_queue = nil
|
|
112
|
-
@lingers
|
|
121
|
+
@lingers.clear
|
|
113
122
|
end
|
|
114
123
|
|
|
115
124
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# CHANNEL socket routing: exclusive 1-to-1 bidirectional.
|
|
6
|
+
#
|
|
7
|
+
class Channel
|
|
8
|
+
# @return [Async::LimitedQueue]
|
|
9
|
+
#
|
|
10
|
+
attr_reader :recv_queue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@connection = nil
|
|
18
|
+
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
19
|
+
@send_queue = nil
|
|
20
|
+
@staging_queue = Routing.build_queue(engine.options.send_hwm, :block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Dequeues the next received message. Blocks until one is available.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<String>, nil]
|
|
27
|
+
#
|
|
28
|
+
def dequeue_recv
|
|
29
|
+
@recv_queue.dequeue
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Wakes a blocked {#dequeue_recv} with a nil sentinel.
|
|
34
|
+
#
|
|
35
|
+
# @return [void]
|
|
36
|
+
#
|
|
37
|
+
def unblock_recv
|
|
38
|
+
@recv_queue.enqueue(nil)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# @param connection [Connection]
|
|
43
|
+
# @raise [RuntimeError] if a connection already exists
|
|
44
|
+
#
|
|
45
|
+
def connection_added(connection)
|
|
46
|
+
raise "CHANNEL allows only one peer" if @connection
|
|
47
|
+
@connection = connection
|
|
48
|
+
|
|
49
|
+
@engine.start_recv_pump(connection, @recv_queue)
|
|
50
|
+
|
|
51
|
+
unless connection.is_a?(Transport::Inproc::DirectPipe)
|
|
52
|
+
@send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
|
|
53
|
+
while (msg = @staging_queue.dequeue(timeout: 0))
|
|
54
|
+
@send_queue.enqueue(msg)
|
|
55
|
+
end
|
|
56
|
+
start_send_pump(connection)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# @param connection [Connection]
|
|
62
|
+
#
|
|
63
|
+
def connection_removed(connection)
|
|
64
|
+
if @connection == connection
|
|
65
|
+
@connection = nil
|
|
66
|
+
@send_queue = nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# @param parts [Array<String>]
|
|
72
|
+
#
|
|
73
|
+
def enqueue(parts)
|
|
74
|
+
conn = @connection
|
|
75
|
+
if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
|
|
76
|
+
conn.send_message(parts)
|
|
77
|
+
elsif @send_queue
|
|
78
|
+
@send_queue.enqueue(parts)
|
|
79
|
+
else
|
|
80
|
+
@staging_queue.enqueue(parts)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# True when the staging and send queues are empty.
|
|
86
|
+
#
|
|
87
|
+
def send_queues_drained?
|
|
88
|
+
@staging_queue.empty? && (@send_queue.nil? || @send_queue.empty?)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def start_send_pump(conn)
|
|
96
|
+
@engine.spawn_conn_pump_task(conn, annotation: "send pump") do
|
|
97
|
+
batch = []
|
|
98
|
+
|
|
99
|
+
loop do
|
|
100
|
+
Routing.dequeue_batch(@send_queue, batch)
|
|
101
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
102
|
+
conn.flush
|
|
103
|
+
batch.clear
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# CLIENT socket routing: round-robin send, fair-queue receive.
|
|
6
|
+
#
|
|
7
|
+
# Same as DEALER — no envelope manipulation.
|
|
8
|
+
#
|
|
9
|
+
class Client
|
|
10
|
+
include RoundRobin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# @return [Async::LimitedQueue]
|
|
14
|
+
#
|
|
15
|
+
attr_reader :recv_queue
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# @param engine [Engine]
|
|
19
|
+
#
|
|
20
|
+
def initialize(engine)
|
|
21
|
+
@engine = engine
|
|
22
|
+
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
23
|
+
init_round_robin(engine)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Dequeues the next received message. Blocks until one is available.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<String>, nil]
|
|
30
|
+
#
|
|
31
|
+
def dequeue_recv
|
|
32
|
+
@recv_queue.dequeue
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Wakes a blocked {#dequeue_recv} with a nil sentinel.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
40
|
+
def unblock_recv
|
|
41
|
+
@recv_queue.enqueue(nil)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# @param connection [Connection]
|
|
46
|
+
#
|
|
47
|
+
def connection_added(connection)
|
|
48
|
+
@connections << connection
|
|
49
|
+
@engine.start_recv_pump(connection, @recv_queue)
|
|
50
|
+
add_round_robin_send_connection(connection)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# @param connection [Connection]
|
|
55
|
+
#
|
|
56
|
+
def connection_removed(connection)
|
|
57
|
+
@connections.delete(connection)
|
|
58
|
+
remove_round_robin_send_connection(connection)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# @param parts [Array<String>]
|
|
63
|
+
#
|
|
64
|
+
def enqueue(parts)
|
|
65
|
+
enqueue_round_robin(parts)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -8,19 +8,20 @@ module OMQ
|
|
|
8
8
|
# include the RoundRobin mixin.
|
|
9
9
|
#
|
|
10
10
|
module ConnSendPump
|
|
11
|
-
# Spawns the pump task
|
|
11
|
+
# Spawns the pump task on the connection's lifecycle barrier so it
|
|
12
|
+
# is torn down with the rest of the connection's pumps.
|
|
12
13
|
#
|
|
13
14
|
# @param engine [Engine]
|
|
14
15
|
# @param conn [Connection]
|
|
15
16
|
# @param q [Async::LimitedQueue]
|
|
16
|
-
# @param tasks [Array]
|
|
17
17
|
# @return [Async::Task]
|
|
18
18
|
#
|
|
19
|
-
def self.start(engine, conn, q
|
|
20
|
-
|
|
19
|
+
def self.start(engine, conn, q)
|
|
20
|
+
engine.spawn_conn_pump_task(conn, annotation: "send pump") do
|
|
21
|
+
batch = []
|
|
22
|
+
|
|
21
23
|
loop do
|
|
22
|
-
batch
|
|
23
|
-
Routing.drain_send_queue(q, batch)
|
|
24
|
+
Routing.dequeue_batch(q, batch)
|
|
24
25
|
|
|
25
26
|
if batch.size == 1
|
|
26
27
|
conn.write_message batch.first
|
|
@@ -30,12 +31,13 @@ module OMQ
|
|
|
30
31
|
|
|
31
32
|
conn.flush
|
|
32
33
|
|
|
33
|
-
batch.each
|
|
34
|
+
batch.each do |parts|
|
|
35
|
+
engine.emit_verbose_msg_sent(conn, parts)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
batch.clear
|
|
34
39
|
end
|
|
35
40
|
end
|
|
36
|
-
|
|
37
|
-
tasks << task
|
|
38
|
-
task
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
end
|
data/lib/omq/routing/dealer.rb
CHANGED
|
@@ -20,7 +20,6 @@ module OMQ
|
|
|
20
20
|
def initialize(engine)
|
|
21
21
|
@engine = engine
|
|
22
22
|
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
23
|
-
@tasks = []
|
|
24
23
|
init_round_robin(engine)
|
|
25
24
|
end
|
|
26
25
|
|
|
@@ -46,8 +45,7 @@ module OMQ
|
|
|
46
45
|
# @param connection [Connection]
|
|
47
46
|
#
|
|
48
47
|
def connection_added(connection)
|
|
49
|
-
|
|
50
|
-
@tasks << task if task
|
|
48
|
+
@engine.start_recv_pump(connection, @recv_queue)
|
|
51
49
|
add_round_robin_send_connection(connection)
|
|
52
50
|
end
|
|
53
51
|
|
|
@@ -66,16 +64,6 @@ module OMQ
|
|
|
66
64
|
enqueue_round_robin(parts)
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
# Stops all background tasks.
|
|
71
|
-
#
|
|
72
|
-
# @return [void]
|
|
73
|
-
#
|
|
74
|
-
def stop
|
|
75
|
-
@tasks.each(&:stop)
|
|
76
|
-
@tasks.clear
|
|
77
|
-
end
|
|
78
|
-
|
|
79
67
|
end
|
|
80
68
|
end
|
|
81
69
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
# @return [Async::LimitedQueue]
|
|
12
|
+
#
|
|
13
|
+
attr_reader :recv_queue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# @param engine [Engine]
|
|
17
|
+
#
|
|
18
|
+
def initialize(engine)
|
|
19
|
+
@engine = engine
|
|
20
|
+
@connections = []
|
|
21
|
+
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
22
|
+
@groups = Set.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Dequeues the next received message. Blocks until one is available.
|
|
27
|
+
#
|
|
28
|
+
# @return [Array<String>, nil]
|
|
29
|
+
#
|
|
30
|
+
def dequeue_recv
|
|
31
|
+
@recv_queue.dequeue
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Wakes a blocked {#dequeue_recv} with a nil sentinel.
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
38
|
+
#
|
|
39
|
+
def unblock_recv
|
|
40
|
+
@recv_queue.enqueue(nil)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# @param connection [Connection]
|
|
45
|
+
#
|
|
46
|
+
def connection_added(connection)
|
|
47
|
+
@connections << connection
|
|
48
|
+
@groups.each do |group|
|
|
49
|
+
connection.send_command(Protocol::ZMTP::Codec::Command.join(group))
|
|
50
|
+
end
|
|
51
|
+
@engine.start_recv_pump(connection, @recv_queue)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# @param connection [Connection]
|
|
56
|
+
#
|
|
57
|
+
def connection_removed(connection)
|
|
58
|
+
@connections.delete(connection)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# DISH is read-only.
|
|
63
|
+
#
|
|
64
|
+
def enqueue(_parts)
|
|
65
|
+
raise "DISH sockets cannot send"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Joins a group.
|
|
70
|
+
#
|
|
71
|
+
# @param group [String]
|
|
72
|
+
#
|
|
73
|
+
def join(group)
|
|
74
|
+
@groups << group
|
|
75
|
+
@connections.each do |conn|
|
|
76
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.join(group))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Leaves a group.
|
|
82
|
+
#
|
|
83
|
+
# @param group [String]
|
|
84
|
+
#
|
|
85
|
+
def leave(group)
|
|
86
|
+
@groups.delete(group)
|
|
87
|
+
@connections.each do |conn|
|
|
88
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.leave(group))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/omq/routing/fan_out.rb
CHANGED
|
@@ -41,7 +41,6 @@ module OMQ
|
|
|
41
41
|
@subscriptions = {} # connection => Set of prefixes
|
|
42
42
|
@subscribe_all = Set.new # connections subscribed to "" (match-all fast path)
|
|
43
43
|
@conn_queues = {} # connection => per-connection send queue
|
|
44
|
-
@conn_send_tasks = {} # connection => send pump task
|
|
45
44
|
@conflate = engine.options.conflate
|
|
46
45
|
@subscriber_joined = Async::Promise.new
|
|
47
46
|
@latest = {} if @conflate
|
|
@@ -96,7 +95,8 @@ module OMQ
|
|
|
96
95
|
end
|
|
97
96
|
|
|
98
97
|
|
|
99
|
-
#
|
|
98
|
+
# Removes the per-connection send queue. The pump itself is torn
|
|
99
|
+
# down by the per-connection lifecycle barrier.
|
|
100
100
|
# Call from #connection_removed.
|
|
101
101
|
#
|
|
102
102
|
# @param conn [Connection]
|
|
@@ -104,7 +104,6 @@ module OMQ
|
|
|
104
104
|
def remove_fan_out_send_connection(conn)
|
|
105
105
|
@subscribe_all.delete(conn)
|
|
106
106
|
@conn_queues.delete(conn)
|
|
107
|
-
@conn_send_tasks.delete(conn)&.stop
|
|
108
107
|
end
|
|
109
108
|
|
|
110
109
|
|
|
@@ -130,7 +129,7 @@ module OMQ
|
|
|
130
129
|
|
|
131
130
|
|
|
132
131
|
def start_subscription_listener(conn)
|
|
133
|
-
@
|
|
132
|
+
@engine.spawn_conn_pump_task(conn, annotation: "subscription listener") do
|
|
134
133
|
loop do
|
|
135
134
|
frame = conn.read_frame
|
|
136
135
|
|
|
@@ -160,13 +159,10 @@ module OMQ
|
|
|
160
159
|
use_wire = conn.respond_to?(:write_wire) && !conn.encrypted?
|
|
161
160
|
|
|
162
161
|
if @conflate
|
|
163
|
-
|
|
162
|
+
start_conn_send_pump_conflate(conn, q)
|
|
164
163
|
else
|
|
165
|
-
|
|
164
|
+
start_conn_send_pump_normal(conn, q, use_wire)
|
|
166
165
|
end
|
|
167
|
-
|
|
168
|
-
@conn_send_tasks[conn] = task
|
|
169
|
-
@tasks << task
|
|
170
166
|
end
|
|
171
167
|
|
|
172
168
|
|
|
@@ -181,14 +177,16 @@ module OMQ
|
|
|
181
177
|
#
|
|
182
178
|
def start_conn_send_pump_normal(conn, q, use_wire)
|
|
183
179
|
@engine.spawn_conn_pump_task(conn, annotation: "send pump") do
|
|
180
|
+
batch = []
|
|
181
|
+
|
|
184
182
|
loop do
|
|
185
|
-
batch
|
|
186
|
-
Routing.drain_send_queue(q, batch)
|
|
183
|
+
Routing.dequeue_batch(q, batch)
|
|
187
184
|
|
|
188
185
|
if write_matching_batch(conn, batch, use_wire)
|
|
189
186
|
conn.flush
|
|
190
187
|
batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
|
|
191
188
|
end
|
|
189
|
+
batch.clear
|
|
192
190
|
end
|
|
193
191
|
end
|
|
194
192
|
end
|
|
@@ -227,14 +225,17 @@ module OMQ
|
|
|
227
225
|
#
|
|
228
226
|
def start_conn_send_pump_conflate(conn, q)
|
|
229
227
|
@engine.spawn_conn_pump_task(conn, annotation: "send pump") do
|
|
228
|
+
batch = []
|
|
229
|
+
|
|
230
230
|
loop do
|
|
231
|
-
batch
|
|
232
|
-
Routing.drain_send_queue(q, batch)
|
|
231
|
+
Routing.dequeue_batch(q, batch)
|
|
233
232
|
|
|
234
233
|
# Keep only the latest message that matches the subscription.
|
|
235
234
|
latest = batch.reverse.find do |parts|
|
|
236
235
|
subscribed?(conn, parts.first || EMPTY_BINARY)
|
|
237
236
|
end
|
|
237
|
+
|
|
238
|
+
batch.clear
|
|
238
239
|
next unless latest
|
|
239
240
|
|
|
240
241
|
conn.write_message(latest)
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
# @return [Async::LimitedQueue]
|
|
9
|
+
#
|
|
10
|
+
attr_reader :recv_queue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Dequeues the next received message. Blocks until one is available.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<String>, nil]
|
|
24
|
+
#
|
|
25
|
+
def dequeue_recv
|
|
26
|
+
@recv_queue.dequeue
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Wakes a blocked {#dequeue_recv} with a nil sentinel.
|
|
31
|
+
#
|
|
32
|
+
# @return [void]
|
|
33
|
+
#
|
|
34
|
+
def unblock_recv
|
|
35
|
+
@recv_queue.enqueue(nil)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# @param connection [Connection]
|
|
40
|
+
#
|
|
41
|
+
def connection_added(connection)
|
|
42
|
+
@engine.start_recv_pump(connection, @recv_queue)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# @param connection [Connection]
|
|
47
|
+
#
|
|
48
|
+
def connection_removed(connection)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# GATHER is read-only.
|
|
53
|
+
#
|
|
54
|
+
def enqueue(_parts)
|
|
55
|
+
raise "GATHER sockets cannot send"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|