omq 0.22.1 → 0.24.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 +162 -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 +22 -8
- 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 +10 -10
- data/lib/omq/engine/socket_lifecycle.rb +26 -9
- data/lib/omq/engine.rb +202 -90
- 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/readable.rb +5 -1
- data/lib/omq/routing/channel.rb +110 -0
- data/lib/omq/routing/client.rb +70 -0
- data/lib/omq/routing/conn_send_pump.rb +5 -8
- data/lib/omq/routing/dealer.rb +3 -15
- data/lib/omq/routing/dish.rb +94 -0
- data/lib/omq/routing/fan_out.rb +12 -16
- data/lib/omq/routing/gather.rb +60 -0
- data/lib/omq/routing/pair.rb +7 -26
- data/lib/omq/routing/peer.rb +95 -0
- data/lib/omq/routing/pub.rb +2 -13
- data/lib/omq/routing/pull.rb +3 -15
- data/lib/omq/routing/push.rb +4 -13
- data/lib/omq/routing/radio.rb +187 -0
- data/lib/omq/routing/rep.rb +5 -19
- data/lib/omq/routing/req.rb +6 -18
- data/lib/omq/routing/round_robin.rb +15 -19
- data/lib/omq/routing/router.rb +5 -19
- data/lib/omq/routing/scatter.rb +76 -0
- data/lib/omq/routing/server.rb +90 -0
- data/lib/omq/routing/sub.rb +3 -15
- data/lib/omq/routing/xpub.rb +2 -13
- data/lib/omq/routing/xsub.rb +8 -25
- data/lib/omq/scatter_gather.rb +56 -0
- data/lib/omq/socket.rb +8 -23
- data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +26 -24
- data/lib/omq/transport/inproc.rb +22 -14
- 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/writable.rb +11 -42
- data/lib/omq.rb +9 -64
- metadata +17 -3
- 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, body]
|
|
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
|
|
data/lib/omq/readable.rb
CHANGED
|
@@ -14,7 +14,11 @@ module OMQ
|
|
|
14
14
|
# @raise [IO::TimeoutError] if read_timeout exceeded
|
|
15
15
|
#
|
|
16
16
|
def receive
|
|
17
|
-
|
|
17
|
+
if @engine.on_io_thread?
|
|
18
|
+
Reactor.run(timeout: @options.read_timeout) { @engine.dequeue_recv }
|
|
19
|
+
elsif (timeout = @options.read_timeout)
|
|
20
|
+
Async::Task.current.with_timeout(timeout, IO::TimeoutError) { @engine.dequeue_recv }
|
|
21
|
+
else
|
|
18
22
|
@engine.dequeue_recv
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -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 [Protocol::ZMTP::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::Pipe)
|
|
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 [Protocol::ZMTP::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::Pipe) && 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 [Protocol::ZMTP::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 [Protocol::ZMTP::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,16 +8,16 @@ 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
|
-
# @param conn [Connection]
|
|
15
|
+
# @param conn [Protocol::ZMTP::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
21
|
batch = []
|
|
22
22
|
|
|
23
23
|
loop do
|
|
@@ -38,9 +38,6 @@ module OMQ
|
|
|
38
38
|
batch.clear
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
|
-
|
|
42
|
-
tasks << task
|
|
43
|
-
task
|
|
44
41
|
end
|
|
45
42
|
|
|
46
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
|
|
|
@@ -43,16 +42,15 @@ module OMQ
|
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
# @param connection [Connection]
|
|
45
|
+
# @param connection [Protocol::ZMTP::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
|
|
|
54
52
|
|
|
55
|
-
# @param connection [Connection]
|
|
53
|
+
# @param connection [Protocol::ZMTP::Connection]
|
|
56
54
|
#
|
|
57
55
|
def connection_removed(connection)
|
|
58
56
|
@connections.delete(connection)
|
|
@@ -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 [Protocol::ZMTP::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 [Protocol::ZMTP::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
|
|
@@ -62,7 +61,7 @@ module OMQ
|
|
|
62
61
|
# Override in subclasses to expose subscriptions to the
|
|
63
62
|
# application (e.g. XPUB enqueues to recv_queue).
|
|
64
63
|
#
|
|
65
|
-
# @param conn [Connection]
|
|
64
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
66
65
|
# @param prefix [String]
|
|
67
66
|
#
|
|
68
67
|
def on_subscribe(conn, prefix)
|
|
@@ -75,7 +74,7 @@ module OMQ
|
|
|
75
74
|
# Called when a cancel command is received from a peer.
|
|
76
75
|
# Override in subclasses (e.g. XPUB enqueues to recv_queue).
|
|
77
76
|
#
|
|
78
|
-
# @param conn [Connection]
|
|
77
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
79
78
|
# @param prefix [String]
|
|
80
79
|
#
|
|
81
80
|
def on_cancel(conn, prefix)
|
|
@@ -87,7 +86,7 @@ module OMQ
|
|
|
87
86
|
# Creates a per-connection send queue and starts its send pump.
|
|
88
87
|
# Call from #connection_added.
|
|
89
88
|
#
|
|
90
|
-
# @param conn [Connection]
|
|
89
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
91
90
|
#
|
|
92
91
|
def add_fan_out_send_connection(conn)
|
|
93
92
|
q = Routing.build_queue(@engine.options.send_hwm, @engine.options.on_mute)
|
|
@@ -96,15 +95,15 @@ 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
|
-
# @param conn [Connection]
|
|
102
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
103
103
|
#
|
|
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
|
|
|
@@ -153,27 +152,24 @@ module OMQ
|
|
|
153
152
|
# In conflate mode, drains the batch and keeps only the latest
|
|
154
153
|
# message per topic before writing.
|
|
155
154
|
#
|
|
156
|
-
# @param conn [Connection]
|
|
155
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
157
156
|
# @param q [Async::LimitedQueue, DropQueue]
|
|
158
157
|
#
|
|
159
158
|
def start_conn_send_pump(conn, q)
|
|
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
|
|
|
173
169
|
# Send pump variant for non-conflate fan-out: dequeues, batch-drains,
|
|
174
170
|
# writes each subscribed message, then flushes once.
|
|
175
171
|
#
|
|
176
|
-
# @param conn [Connection]
|
|
172
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
177
173
|
# @param q [Async::LimitedQueue, DropQueue]
|
|
178
174
|
# @param use_wire [Boolean] true iff the encoded wire bytes can
|
|
179
175
|
# be shared across peers (unencrypted ZMTP)
|
|
@@ -223,7 +219,7 @@ module OMQ
|
|
|
223
219
|
# Send pump variant for conflate mode: keeps only the latest
|
|
224
220
|
# subscribed message per batch. Stale duplicates are dropped.
|
|
225
221
|
#
|
|
226
|
-
# @param conn [Connection]
|
|
222
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
227
223
|
# @param q [Async::LimitedQueue, DropQueue]
|
|
228
224
|
# @return [Async::Task]
|
|
229
225
|
#
|
|
@@ -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 [Protocol::ZMTP::Connection]
|
|
40
|
+
#
|
|
41
|
+
def connection_added(connection)
|
|
42
|
+
@engine.start_recv_pump(connection, @recv_queue)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# @param connection [Protocol::ZMTP::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
|