omq 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +129 -0
- data/README.md +28 -3
- data/lib/omq/channel.rb +5 -5
- data/lib/omq/client_server.rb +10 -10
- data/lib/omq/engine.rb +702 -0
- data/lib/omq/options.rb +48 -0
- data/lib/omq/pair.rb +4 -4
- data/lib/omq/peer.rb +5 -5
- data/lib/omq/pub_sub.rb +18 -18
- data/lib/omq/push_pull.rb +6 -6
- data/lib/omq/queue_interface.rb +73 -0
- data/lib/omq/radio_dish.rb +6 -6
- data/lib/omq/reactor.rb +128 -0
- data/lib/omq/readable.rb +44 -0
- data/lib/omq/req_rep.rb +8 -8
- data/lib/omq/router_dealer.rb +8 -8
- data/lib/omq/routing/channel.rb +83 -0
- data/lib/omq/routing/client.rb +56 -0
- data/lib/omq/routing/dealer.rb +57 -0
- data/lib/omq/routing/dish.rb +78 -0
- data/lib/omq/routing/fan_out.rb +140 -0
- data/lib/omq/routing/gather.rb +46 -0
- data/lib/omq/routing/pair.rb +86 -0
- data/lib/omq/routing/peer.rb +101 -0
- data/lib/omq/routing/pub.rb +60 -0
- data/lib/omq/routing/pull.rb +46 -0
- data/lib/omq/routing/push.rb +81 -0
- data/lib/omq/routing/radio.rb +150 -0
- data/lib/omq/routing/rep.rb +101 -0
- data/lib/omq/routing/req.rb +65 -0
- data/lib/omq/routing/round_robin.rb +168 -0
- data/lib/omq/routing/router.rb +110 -0
- data/lib/omq/routing/scatter.rb +82 -0
- data/lib/omq/routing/server.rb +101 -0
- data/lib/omq/routing/sub.rb +78 -0
- data/lib/omq/routing/xpub.rb +72 -0
- data/lib/omq/routing/xsub.rb +83 -0
- data/lib/omq/routing.rb +66 -0
- data/lib/omq/scatter_gather.rb +8 -8
- data/lib/omq/single_frame.rb +18 -0
- data/lib/omq/socket.rb +32 -11
- data/lib/omq/transport/inproc.rb +355 -0
- data/lib/omq/transport/ipc.rb +117 -0
- data/lib/omq/transport/tcp.rb +111 -0
- data/lib/omq/transport/tls.rb +146 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +66 -0
- data/lib/omq.rb +64 -4
- metadata +34 -33
- data/lib/omq/zmtp/engine.rb +0 -551
- data/lib/omq/zmtp/options.rb +0 -48
- data/lib/omq/zmtp/reactor.rb +0 -131
- data/lib/omq/zmtp/readable.rb +0 -29
- data/lib/omq/zmtp/routing/channel.rb +0 -81
- data/lib/omq/zmtp/routing/client.rb +0 -56
- data/lib/omq/zmtp/routing/dealer.rb +0 -57
- data/lib/omq/zmtp/routing/dish.rb +0 -80
- data/lib/omq/zmtp/routing/fan_out.rb +0 -131
- data/lib/omq/zmtp/routing/gather.rb +0 -48
- data/lib/omq/zmtp/routing/pair.rb +0 -84
- data/lib/omq/zmtp/routing/peer.rb +0 -100
- data/lib/omq/zmtp/routing/pub.rb +0 -62
- data/lib/omq/zmtp/routing/pull.rb +0 -48
- data/lib/omq/zmtp/routing/push.rb +0 -80
- data/lib/omq/zmtp/routing/radio.rb +0 -139
- data/lib/omq/zmtp/routing/rep.rb +0 -101
- data/lib/omq/zmtp/routing/req.rb +0 -65
- data/lib/omq/zmtp/routing/round_robin.rb +0 -143
- data/lib/omq/zmtp/routing/router.rb +0 -109
- data/lib/omq/zmtp/routing/scatter.rb +0 -81
- data/lib/omq/zmtp/routing/server.rb +0 -100
- data/lib/omq/zmtp/routing/sub.rb +0 -80
- data/lib/omq/zmtp/routing/xpub.rb +0 -74
- data/lib/omq/zmtp/routing/xsub.rb +0 -86
- data/lib/omq/zmtp/routing.rb +0 -65
- data/lib/omq/zmtp/single_frame.rb +0 -20
- data/lib/omq/zmtp/transport/inproc.rb +0 -359
- data/lib/omq/zmtp/transport/ipc.rb +0 -118
- data/lib/omq/zmtp/transport/tcp.rb +0 -117
- data/lib/omq/zmtp/writable.rb +0 -61
- data/lib/omq/zmtp.rb +0 -81
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# RADIO socket routing: group-based fan-out to DISH peers.
|
|
6
|
+
#
|
|
7
|
+
# Like PUB/FanOut but with exact group matching and JOIN/LEAVE
|
|
8
|
+
# commands instead of SUBSCRIBE/CANCEL.
|
|
9
|
+
#
|
|
10
|
+
# Messages are sent as two frames on the wire:
|
|
11
|
+
# group (MORE=1) + body (MORE=0)
|
|
12
|
+
#
|
|
13
|
+
class Radio
|
|
14
|
+
|
|
15
|
+
# @param engine [Engine]
|
|
16
|
+
#
|
|
17
|
+
def initialize(engine)
|
|
18
|
+
@engine = engine
|
|
19
|
+
@connections = []
|
|
20
|
+
@groups = {} # connection => Set of joined groups
|
|
21
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
22
|
+
@send_pump_started = false
|
|
23
|
+
@conflate = engine.options.conflate
|
|
24
|
+
@tasks = []
|
|
25
|
+
@written = Set.new
|
|
26
|
+
@latest = {} if @conflate
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Async::LimitedQueue]
|
|
30
|
+
#
|
|
31
|
+
attr_reader :send_queue
|
|
32
|
+
|
|
33
|
+
# RADIO is write-only.
|
|
34
|
+
#
|
|
35
|
+
def recv_queue
|
|
36
|
+
raise "RADIO sockets cannot receive"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param connection [Connection]
|
|
40
|
+
#
|
|
41
|
+
def connection_added(connection)
|
|
42
|
+
@connections << connection
|
|
43
|
+
@groups[connection] = Set.new
|
|
44
|
+
start_group_listener(connection)
|
|
45
|
+
start_send_pump unless @send_pump_started
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param connection [Connection]
|
|
49
|
+
#
|
|
50
|
+
def connection_removed(connection)
|
|
51
|
+
@connections.delete(connection)
|
|
52
|
+
@groups.delete(connection)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Enqueues a message for sending.
|
|
56
|
+
#
|
|
57
|
+
# @param parts [Array<String>] [group, body]
|
|
58
|
+
#
|
|
59
|
+
def enqueue(parts)
|
|
60
|
+
@send_queue.enqueue(parts)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stop
|
|
64
|
+
@tasks.each(&:stop)
|
|
65
|
+
@tasks.clear
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def start_send_pump
|
|
71
|
+
@send_pump_started = true
|
|
72
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
73
|
+
loop do
|
|
74
|
+
@send_pump_idle = true
|
|
75
|
+
batch = [@send_queue.dequeue]
|
|
76
|
+
@send_pump_idle = false
|
|
77
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
78
|
+
|
|
79
|
+
@written.clear
|
|
80
|
+
|
|
81
|
+
if @conflate
|
|
82
|
+
# Keep only the last matching message per connection.
|
|
83
|
+
@latest.clear
|
|
84
|
+
batch.each do |parts|
|
|
85
|
+
group = parts[0]
|
|
86
|
+
body = parts[1] || EMPTY_BINARY
|
|
87
|
+
@connections.each do |conn|
|
|
88
|
+
next unless @groups[conn]&.include?(group)
|
|
89
|
+
@latest[conn] = [group, body]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
@latest.each do |conn, msg|
|
|
93
|
+
begin
|
|
94
|
+
conn.write_message(msg)
|
|
95
|
+
@written << conn
|
|
96
|
+
rescue *CONNECTION_LOST
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
batch.each do |parts|
|
|
101
|
+
group = parts[0]
|
|
102
|
+
body = parts[1] || EMPTY_BINARY
|
|
103
|
+
msg = [group, body]
|
|
104
|
+
wire_bytes = nil
|
|
105
|
+
|
|
106
|
+
@connections.each do |conn|
|
|
107
|
+
next unless @groups[conn]&.include?(group)
|
|
108
|
+
begin
|
|
109
|
+
if conn.respond_to?(:curve?) && conn.curve?
|
|
110
|
+
conn.write_message(msg)
|
|
111
|
+
elsif conn.respond_to?(:write_wire)
|
|
112
|
+
wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(msg)
|
|
113
|
+
conn.write_wire(wire_bytes)
|
|
114
|
+
else
|
|
115
|
+
conn.write_message(msg)
|
|
116
|
+
end
|
|
117
|
+
@written << conn
|
|
118
|
+
rescue *CONNECTION_LOST
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@written.each do |conn|
|
|
125
|
+
conn.flush
|
|
126
|
+
rescue *CONNECTION_LOST
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def start_group_listener(conn)
|
|
134
|
+
@tasks << @engine.spawn_pump_task(annotation: "group listener") do
|
|
135
|
+
loop do
|
|
136
|
+
frame = conn.read_frame
|
|
137
|
+
next unless frame.command?
|
|
138
|
+
cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
|
|
139
|
+
case cmd.name
|
|
140
|
+
when "JOIN" then @groups[conn]&.add(cmd.data)
|
|
141
|
+
when "LEAVE" then @groups[conn]&.delete(cmd.data)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
rescue *CONNECTION_LOST
|
|
145
|
+
@engine.connection_lost(conn)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# REP socket routing: fair-queue receive, reply routed back to sender.
|
|
6
|
+
#
|
|
7
|
+
# REP strips the routing envelope (everything up to and including the
|
|
8
|
+
# empty delimiter) on receive, saves it internally, and restores it
|
|
9
|
+
# on send.
|
|
10
|
+
#
|
|
11
|
+
class Rep
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
EMPTY_FRAME = "".b.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(engine)
|
|
17
|
+
@engine = engine
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
20
|
+
@pending_replies = []
|
|
21
|
+
@tasks = []
|
|
22
|
+
@send_pump_started = false
|
|
23
|
+
@send_pump_idle = true
|
|
24
|
+
@written = Set.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Async::LimitedQueue]
|
|
28
|
+
#
|
|
29
|
+
attr_reader :recv_queue, :send_queue
|
|
30
|
+
|
|
31
|
+
# @param connection [Connection]
|
|
32
|
+
#
|
|
33
|
+
def connection_added(connection)
|
|
34
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
35
|
+
delimiter = msg.index(&:empty?) || msg.size
|
|
36
|
+
envelope = msg[0, delimiter]
|
|
37
|
+
body = msg[(delimiter + 1)..] || []
|
|
38
|
+
@pending_replies << { conn: connection, envelope: envelope }
|
|
39
|
+
body
|
|
40
|
+
end
|
|
41
|
+
@tasks << task if task
|
|
42
|
+
start_send_pump unless @send_pump_started
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param connection [Connection]
|
|
46
|
+
#
|
|
47
|
+
def connection_removed(connection)
|
|
48
|
+
# Remove any pending replies for this connection
|
|
49
|
+
@pending_replies.reject! { |r| r[:conn] == connection }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Enqueues a reply for sending.
|
|
53
|
+
#
|
|
54
|
+
# @param parts [Array<String>]
|
|
55
|
+
#
|
|
56
|
+
def enqueue(parts)
|
|
57
|
+
@send_queue.enqueue(parts)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stop
|
|
61
|
+
@tasks.each(&:stop)
|
|
62
|
+
@tasks.clear
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def send_pump_idle? = @send_pump_idle
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def start_send_pump
|
|
70
|
+
@send_pump_started = true
|
|
71
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
72
|
+
loop do
|
|
73
|
+
@send_pump_idle = true
|
|
74
|
+
batch = [@send_queue.dequeue]
|
|
75
|
+
@send_pump_idle = false
|
|
76
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
77
|
+
|
|
78
|
+
@written.clear
|
|
79
|
+
batch.each do |parts|
|
|
80
|
+
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
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# REQ socket routing: round-robin send with strict send/recv alternation.
|
|
6
|
+
#
|
|
7
|
+
# REQ prepends an empty delimiter frame on send and strips it on receive.
|
|
8
|
+
#
|
|
9
|
+
class Req
|
|
10
|
+
include RoundRobin
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
17
|
+
@tasks = []
|
|
18
|
+
init_round_robin(engine)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Async::LimitedQueue]
|
|
22
|
+
#
|
|
23
|
+
attr_reader :recv_queue, :send_queue
|
|
24
|
+
|
|
25
|
+
# @param connection [Connection]
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
@connections << connection
|
|
29
|
+
signal_connection_available
|
|
30
|
+
update_direct_pipe
|
|
31
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
32
|
+
msg.first&.empty? ? msg[1..] : msg
|
|
33
|
+
end
|
|
34
|
+
@tasks << task if task
|
|
35
|
+
start_send_pump unless @send_pump_started
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param connection [Connection]
|
|
39
|
+
#
|
|
40
|
+
def connection_removed(connection)
|
|
41
|
+
@connections.delete(connection)
|
|
42
|
+
update_direct_pipe
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param parts [Array<String>]
|
|
46
|
+
#
|
|
47
|
+
def enqueue(parts)
|
|
48
|
+
enqueue_round_robin(parts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
def stop
|
|
53
|
+
@tasks.each(&:stop)
|
|
54
|
+
@tasks.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# REQ prepends empty delimiter frame on the wire.
|
|
60
|
+
#
|
|
61
|
+
def transform_send(parts) = [EMPTY_BINARY, *parts]
|
|
62
|
+
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Mixin for routing strategies that send via round-robin.
|
|
6
|
+
#
|
|
7
|
+
# Provides reactive connection management: Async::Promise waits
|
|
8
|
+
# for the first connection, Array#cycle handles round-robin,
|
|
9
|
+
# and a new Promise is created when all connections drop.
|
|
10
|
+
#
|
|
11
|
+
# Including classes must call `init_round_robin(engine)` from
|
|
12
|
+
# their #initialize.
|
|
13
|
+
#
|
|
14
|
+
module RoundRobin
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Initializes round-robin state for the including class.
|
|
19
|
+
#
|
|
20
|
+
# @param engine [Engine]
|
|
21
|
+
#
|
|
22
|
+
def init_round_robin(engine)
|
|
23
|
+
@connections = []
|
|
24
|
+
@cycle = @connections.cycle
|
|
25
|
+
@connection_available = Async::Promise.new
|
|
26
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
27
|
+
@send_pump_started = false
|
|
28
|
+
@send_pump_idle = true
|
|
29
|
+
@direct_pipe = nil
|
|
30
|
+
@written = Set.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Resolves the connection-available promise so blocked
|
|
35
|
+
# senders can proceed.
|
|
36
|
+
#
|
|
37
|
+
def signal_connection_available
|
|
38
|
+
unless @connection_available.resolved?
|
|
39
|
+
@connection_available.resolve(true)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Updates the direct-pipe shortcut for inproc single-peer bypass.
|
|
45
|
+
# Call from connection_added after @connections is updated.
|
|
46
|
+
#
|
|
47
|
+
def update_direct_pipe
|
|
48
|
+
if @connections.size == 1 && @connections.first.is_a?(Transport::Inproc::DirectPipe)
|
|
49
|
+
@direct_pipe = @connections.first
|
|
50
|
+
else
|
|
51
|
+
@direct_pipe = nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Enqueues directly to the inproc peer's recv queue if possible,
|
|
57
|
+
# otherwise falls back to the send queue for the send pump.
|
|
58
|
+
#
|
|
59
|
+
def enqueue_round_robin(parts)
|
|
60
|
+
pipe = @direct_pipe
|
|
61
|
+
if pipe&.direct_recv_queue
|
|
62
|
+
pipe.send_message(transform_send(parts))
|
|
63
|
+
else
|
|
64
|
+
@send_queue.enqueue(parts)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Blocks until a connection is available, then returns
|
|
70
|
+
# the next one in round-robin order.
|
|
71
|
+
#
|
|
72
|
+
# @return [Connection]
|
|
73
|
+
#
|
|
74
|
+
def next_connection
|
|
75
|
+
@cycle.next
|
|
76
|
+
rescue StopIteration
|
|
77
|
+
@connection_available = Async::Promise.new
|
|
78
|
+
@connection_available.wait
|
|
79
|
+
@cycle = @connections.cycle
|
|
80
|
+
retry
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Transforms parts before sending. Override in subclasses
|
|
85
|
+
# (e.g. REQ prepends an empty delimiter frame).
|
|
86
|
+
#
|
|
87
|
+
# @param parts [Array<String>]
|
|
88
|
+
# @return [Array<String>]
|
|
89
|
+
#
|
|
90
|
+
def transform_send(parts) = parts
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Starts the background send pump that dequeues messages
|
|
94
|
+
# and dispatches them round-robin across connections.
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean] true when the send pump is idle (not sending a batch)
|
|
97
|
+
def send_pump_idle? = @send_pump_idle
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def start_send_pump
|
|
101
|
+
@send_pump_started = true
|
|
102
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
103
|
+
loop do
|
|
104
|
+
@send_pump_idle = true
|
|
105
|
+
batch = [@send_queue.dequeue]
|
|
106
|
+
@send_pump_idle = false
|
|
107
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
108
|
+
|
|
109
|
+
if batch.size == 1
|
|
110
|
+
send_with_retry(batch[0])
|
|
111
|
+
else
|
|
112
|
+
send_batch(batch)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Sends a single message, retrying on a new connection if
|
|
120
|
+
# the current one is lost.
|
|
121
|
+
#
|
|
122
|
+
# @param parts [Array<String>]
|
|
123
|
+
#
|
|
124
|
+
def send_with_retry(parts)
|
|
125
|
+
conn = next_connection
|
|
126
|
+
conn.send_message(transform_send(parts))
|
|
127
|
+
rescue *CONNECTION_LOST
|
|
128
|
+
@engine.connection_lost(conn)
|
|
129
|
+
retry
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Sends a batch of messages, writing without flushing for
|
|
134
|
+
# throughput. Falls back to #send_with_retry on failure.
|
|
135
|
+
#
|
|
136
|
+
# @param batch [Array<Array<String>>]
|
|
137
|
+
#
|
|
138
|
+
def send_batch(batch)
|
|
139
|
+
@written.clear
|
|
140
|
+
batch.each_with_index do |parts, i|
|
|
141
|
+
conn = next_connection
|
|
142
|
+
begin
|
|
143
|
+
conn.write_message(transform_send(parts))
|
|
144
|
+
@written << conn
|
|
145
|
+
rescue *CONNECTION_LOST
|
|
146
|
+
@engine.connection_lost(conn)
|
|
147
|
+
# Flush what we've written so far
|
|
148
|
+
@written.each do |c|
|
|
149
|
+
c.flush
|
|
150
|
+
rescue *CONNECTION_LOST
|
|
151
|
+
# will be cleaned up
|
|
152
|
+
end
|
|
153
|
+
@written.clear
|
|
154
|
+
# Fall back to send_with_retry for this and remaining
|
|
155
|
+
send_with_retry(parts)
|
|
156
|
+
batch[(i + 1)..].each { |p| send_with_retry(p) }
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
@written.each do |conn|
|
|
161
|
+
conn.flush
|
|
162
|
+
rescue *CONNECTION_LOST
|
|
163
|
+
# will be cleaned up
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module OMQ
|
|
7
|
+
module Routing
|
|
8
|
+
# ROUTER socket routing: identity-based routing.
|
|
9
|
+
#
|
|
10
|
+
# Prepends peer identity frame on receive. Uses first frame as
|
|
11
|
+
# routing identity on send.
|
|
12
|
+
#
|
|
13
|
+
class Router
|
|
14
|
+
# @param engine [Engine]
|
|
15
|
+
#
|
|
16
|
+
def initialize(engine)
|
|
17
|
+
@engine = engine
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
20
|
+
@connections_by_identity = {}
|
|
21
|
+
@identity_by_connection = {}
|
|
22
|
+
@tasks = []
|
|
23
|
+
@send_pump_started = false
|
|
24
|
+
@send_pump_idle = true
|
|
25
|
+
@written = Set.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Async::LimitedQueue]
|
|
29
|
+
#
|
|
30
|
+
attr_reader :recv_queue, :send_queue
|
|
31
|
+
|
|
32
|
+
# @param connection [Connection]
|
|
33
|
+
#
|
|
34
|
+
def connection_added(connection)
|
|
35
|
+
identity = connection.peer_identity
|
|
36
|
+
identity = SecureRandom.bytes(5) if identity.nil? || identity.empty?
|
|
37
|
+
@connections_by_identity[identity] = connection
|
|
38
|
+
@identity_by_connection[connection] = identity
|
|
39
|
+
|
|
40
|
+
task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
|
|
41
|
+
[identity, *msg]
|
|
42
|
+
end
|
|
43
|
+
@tasks << task if task
|
|
44
|
+
|
|
45
|
+
start_send_pump unless @send_pump_started
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param connection [Connection]
|
|
49
|
+
#
|
|
50
|
+
def connection_removed(connection)
|
|
51
|
+
identity = @identity_by_connection.delete(connection)
|
|
52
|
+
@connections_by_identity.delete(identity) if identity
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Enqueues a message for sending.
|
|
56
|
+
#
|
|
57
|
+
# @param parts [Array<String>]
|
|
58
|
+
#
|
|
59
|
+
def enqueue(parts)
|
|
60
|
+
if @engine.options.router_mandatory?
|
|
61
|
+
identity = parts.first
|
|
62
|
+
unless @connections_by_identity[identity]
|
|
63
|
+
raise SocketError, "no route to identity #{identity.inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
@send_queue.enqueue(parts)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stop
|
|
70
|
+
@tasks.each(&:stop)
|
|
71
|
+
@tasks.clear
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def send_pump_idle? = @send_pump_idle
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def start_send_pump
|
|
79
|
+
@send_pump_started = true
|
|
80
|
+
@tasks << @engine.spawn_pump_task(annotation: "send pump") do
|
|
81
|
+
loop do
|
|
82
|
+
@send_pump_idle = true
|
|
83
|
+
batch = [@send_queue.dequeue]
|
|
84
|
+
@send_pump_idle = false
|
|
85
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
86
|
+
|
|
87
|
+
@written.clear
|
|
88
|
+
batch.each do |parts|
|
|
89
|
+
identity = parts.first
|
|
90
|
+
conn = @connections_by_identity[identity]
|
|
91
|
+
next unless conn # silently drop (peer may have disconnected)
|
|
92
|
+
begin
|
|
93
|
+
conn.write_message(parts[1..])
|
|
94
|
+
@written << conn
|
|
95
|
+
rescue *CONNECTION_LOST
|
|
96
|
+
# will be cleaned up
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@written.each do |conn|
|
|
101
|
+
conn.flush
|
|
102
|
+
rescue *CONNECTION_LOST
|
|
103
|
+
# will be cleaned up
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# SCATTER socket routing: round-robin send to GATHER peers.
|
|
6
|
+
#
|
|
7
|
+
class Scatter
|
|
8
|
+
include RoundRobin
|
|
9
|
+
|
|
10
|
+
# @param engine [Engine]
|
|
11
|
+
#
|
|
12
|
+
def initialize(engine)
|
|
13
|
+
@engine = engine
|
|
14
|
+
@tasks = []
|
|
15
|
+
init_round_robin(engine)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# @return [Async::LimitedQueue]
|
|
20
|
+
#
|
|
21
|
+
attr_reader :send_queue
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# SCATTER is write-only.
|
|
25
|
+
#
|
|
26
|
+
def recv_queue
|
|
27
|
+
raise "SCATTER sockets cannot receive"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# @param connection [Connection]
|
|
32
|
+
#
|
|
33
|
+
def connection_added(connection)
|
|
34
|
+
@connections << connection
|
|
35
|
+
signal_connection_available
|
|
36
|
+
update_direct_pipe
|
|
37
|
+
start_send_pump unless @send_pump_started
|
|
38
|
+
start_reaper(connection)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# @param connection [Connection]
|
|
43
|
+
#
|
|
44
|
+
def connection_removed(connection)
|
|
45
|
+
@connections.delete(connection)
|
|
46
|
+
update_direct_pipe
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# @param parts [Array<String>]
|
|
51
|
+
#
|
|
52
|
+
def enqueue(parts)
|
|
53
|
+
enqueue_round_robin(parts)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Stops all background tasks (send pump, reapers).
|
|
58
|
+
#
|
|
59
|
+
def stop
|
|
60
|
+
@tasks.each(&:stop)
|
|
61
|
+
@tasks.clear
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Detects peer disconnection on write-only sockets by
|
|
68
|
+
# blocking on a receive that only returns on disconnect.
|
|
69
|
+
#
|
|
70
|
+
# @param conn [Connection]
|
|
71
|
+
#
|
|
72
|
+
def start_reaper(conn)
|
|
73
|
+
return if conn.is_a?(Transport::Inproc::DirectPipe)
|
|
74
|
+
@tasks << @engine.spawn_pump_task(annotation: "reaper") do
|
|
75
|
+
conn.receive_message
|
|
76
|
+
rescue *CONNECTION_LOST
|
|
77
|
+
@engine.connection_lost(conn)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|