omq 0.1.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 +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +15 -0
- data/README.md +145 -0
- data/lib/omq/pair.rb +13 -0
- data/lib/omq/pub_sub.rb +77 -0
- data/lib/omq/push_pull.rb +21 -0
- data/lib/omq/req_rep.rb +23 -0
- data/lib/omq/router_dealer.rb +36 -0
- data/lib/omq/socket.rb +178 -0
- data/lib/omq/version.rb +5 -0
- data/lib/omq/zmtp/codec/command.rb +207 -0
- data/lib/omq/zmtp/codec/frame.rb +104 -0
- data/lib/omq/zmtp/codec/greeting.rb +96 -0
- data/lib/omq/zmtp/codec.rb +18 -0
- data/lib/omq/zmtp/connection.rb +233 -0
- data/lib/omq/zmtp/engine.rb +339 -0
- data/lib/omq/zmtp/mechanism/null.rb +70 -0
- data/lib/omq/zmtp/options.rb +57 -0
- data/lib/omq/zmtp/reactor.rb +142 -0
- data/lib/omq/zmtp/readable.rb +29 -0
- data/lib/omq/zmtp/routing/dealer.rb +57 -0
- data/lib/omq/zmtp/routing/fan_out.rb +89 -0
- data/lib/omq/zmtp/routing/pair.rb +68 -0
- data/lib/omq/zmtp/routing/pub.rb +62 -0
- data/lib/omq/zmtp/routing/pull.rb +48 -0
- data/lib/omq/zmtp/routing/push.rb +57 -0
- data/lib/omq/zmtp/routing/rep.rb +83 -0
- data/lib/omq/zmtp/routing/req.rb +70 -0
- data/lib/omq/zmtp/routing/round_robin.rb +69 -0
- data/lib/omq/zmtp/routing/router.rb +88 -0
- data/lib/omq/zmtp/routing/sub.rb +80 -0
- data/lib/omq/zmtp/routing/xpub.rb +74 -0
- data/lib/omq/zmtp/routing/xsub.rb +80 -0
- data/lib/omq/zmtp/routing.rb +38 -0
- data/lib/omq/zmtp/transport/inproc.rb +299 -0
- data/lib/omq/zmtp/transport/ipc.rb +114 -0
- data/lib/omq/zmtp/transport/tcp.rb +98 -0
- data/lib/omq/zmtp/valid_peers.rb +21 -0
- data/lib/omq/zmtp/writable.rb +44 -0
- data/lib/omq/zmtp.rb +47 -0
- data/lib/omq.rb +19 -0
- metadata +110 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# Mixin for routing strategies that fan-out to subscribers.
|
|
7
|
+
#
|
|
8
|
+
# Manages per-connection subscription sets, subscription command
|
|
9
|
+
# listeners, and a send pump that delivers to all matching peers.
|
|
10
|
+
#
|
|
11
|
+
# Including classes must call `init_fan_out(engine)` from
|
|
12
|
+
# their #initialize.
|
|
13
|
+
#
|
|
14
|
+
module FanOut
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def init_fan_out(engine)
|
|
18
|
+
@connections = []
|
|
19
|
+
@subscriptions = {} # connection => Set of prefixes
|
|
20
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
21
|
+
@send_pump_started = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Boolean] whether the connection is subscribed to the topic
|
|
25
|
+
#
|
|
26
|
+
def subscribed?(conn, topic)
|
|
27
|
+
subs = @subscriptions[conn]
|
|
28
|
+
return false unless subs
|
|
29
|
+
subs.any? { |prefix| topic.b.start_with?(prefix.b) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Called when a subscription command is received from a peer.
|
|
33
|
+
# Override in subclasses to expose subscriptions to the
|
|
34
|
+
# application (e.g. XPUB enqueues to recv_queue).
|
|
35
|
+
#
|
|
36
|
+
# @param conn [Connection]
|
|
37
|
+
# @param prefix [String]
|
|
38
|
+
#
|
|
39
|
+
def on_subscribe(conn, prefix)
|
|
40
|
+
@subscriptions[conn] << prefix
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Called when a cancel command is received from a peer.
|
|
44
|
+
# Override in subclasses (e.g. XPUB enqueues to recv_queue).
|
|
45
|
+
#
|
|
46
|
+
# @param conn [Connection]
|
|
47
|
+
# @param prefix [String]
|
|
48
|
+
#
|
|
49
|
+
def on_cancel(conn, prefix)
|
|
50
|
+
@subscriptions[conn]&.delete(prefix)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def start_send_pump
|
|
54
|
+
@send_pump_started = true
|
|
55
|
+
@tasks << Reactor.spawn_pump do
|
|
56
|
+
loop do
|
|
57
|
+
parts = @send_queue.dequeue
|
|
58
|
+
topic = parts.first || "".b
|
|
59
|
+
@connections.each do |conn|
|
|
60
|
+
next unless subscribed?(conn, topic)
|
|
61
|
+
begin
|
|
62
|
+
conn.send_message(parts)
|
|
63
|
+
rescue IOError, EOFError
|
|
64
|
+
# connection dead — will be cleaned up
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start_subscription_listener(conn)
|
|
72
|
+
@tasks << Reactor.spawn_pump do
|
|
73
|
+
loop do
|
|
74
|
+
frame = conn.read_frame
|
|
75
|
+
next unless frame.command?
|
|
76
|
+
cmd = Codec::Command.from_body(frame.body)
|
|
77
|
+
case cmd.name
|
|
78
|
+
when "SUBSCRIBE" then on_subscribe(conn, cmd.data)
|
|
79
|
+
when "CANCEL" then on_cancel(conn, cmd.data)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
rescue EOFError
|
|
83
|
+
@engine.connection_lost(conn)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# PAIR socket routing: exclusive 1-to-1 bidirectional.
|
|
7
|
+
#
|
|
8
|
+
# Only one peer connection is allowed. Messages flow through
|
|
9
|
+
# internal send/recv queues backed by Async::LimitedQueue.
|
|
10
|
+
#
|
|
11
|
+
class Pair
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@connection = nil
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
20
|
+
@tasks = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Async::LimitedQueue]
|
|
24
|
+
#
|
|
25
|
+
attr_reader :recv_queue, :send_queue
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
# @raise [RuntimeError] if a connection already exists
|
|
29
|
+
#
|
|
30
|
+
def connection_added(connection)
|
|
31
|
+
raise "PAIR allows only one peer" if @connection
|
|
32
|
+
@connection = connection
|
|
33
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
34
|
+
@tasks << task if task
|
|
35
|
+
start_send_pump(connection)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param connection [Connection]
|
|
39
|
+
#
|
|
40
|
+
def connection_removed(connection)
|
|
41
|
+
@connection = nil if @connection == connection
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param parts [Array<String>]
|
|
45
|
+
#
|
|
46
|
+
def enqueue(parts)
|
|
47
|
+
@send_queue.enqueue(parts)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
def stop
|
|
52
|
+
@tasks.each(&:stop)
|
|
53
|
+
@tasks.clear
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def start_send_pump(conn)
|
|
59
|
+
@tasks << Reactor.spawn_pump do
|
|
60
|
+
loop { conn.send_message(@send_queue.dequeue) }
|
|
61
|
+
rescue EOFError, IOError
|
|
62
|
+
@engine.connection_lost(conn)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# PUB socket routing: fan-out to all subscribers.
|
|
7
|
+
#
|
|
8
|
+
# Listens for SUBSCRIBE/CANCEL commands from peers.
|
|
9
|
+
# Drops messages if a subscriber's connection write fails.
|
|
10
|
+
#
|
|
11
|
+
class Pub
|
|
12
|
+
include FanOut
|
|
13
|
+
|
|
14
|
+
# @param engine [Engine]
|
|
15
|
+
#
|
|
16
|
+
def initialize(engine)
|
|
17
|
+
@engine = engine
|
|
18
|
+
@tasks = []
|
|
19
|
+
init_fan_out(engine)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Async::LimitedQueue]
|
|
23
|
+
#
|
|
24
|
+
attr_reader :send_queue
|
|
25
|
+
|
|
26
|
+
# PUB is write-only.
|
|
27
|
+
#
|
|
28
|
+
def recv_queue
|
|
29
|
+
raise "PUB sockets cannot receive"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param connection [Connection]
|
|
33
|
+
#
|
|
34
|
+
def connection_added(connection)
|
|
35
|
+
@connections << connection
|
|
36
|
+
@subscriptions[connection] = Set.new
|
|
37
|
+
start_subscription_listener(connection)
|
|
38
|
+
start_send_pump unless @send_pump_started
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param connection [Connection]
|
|
42
|
+
#
|
|
43
|
+
def connection_removed(connection)
|
|
44
|
+
@connections.delete(connection)
|
|
45
|
+
@subscriptions.delete(connection)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param parts [Array<String>]
|
|
49
|
+
#
|
|
50
|
+
def enqueue(parts)
|
|
51
|
+
@send_queue.enqueue(parts)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
def stop
|
|
56
|
+
@tasks.each(&:stop)
|
|
57
|
+
@tasks.clear
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# PULL socket routing: fair-queue receive from PUSH peers.
|
|
7
|
+
#
|
|
8
|
+
class Pull
|
|
9
|
+
# @param engine [Engine]
|
|
10
|
+
#
|
|
11
|
+
def initialize(engine)
|
|
12
|
+
@engine = engine
|
|
13
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
14
|
+
@tasks = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Async::LimitedQueue]
|
|
18
|
+
#
|
|
19
|
+
attr_reader :recv_queue
|
|
20
|
+
|
|
21
|
+
# @param connection [Connection]
|
|
22
|
+
#
|
|
23
|
+
def connection_added(connection)
|
|
24
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
25
|
+
@tasks << task if task
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param connection [Connection]
|
|
29
|
+
#
|
|
30
|
+
def connection_removed(connection)
|
|
31
|
+
# recv pump stops on EOFError
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# PULL is read-only.
|
|
35
|
+
#
|
|
36
|
+
def enqueue(_parts)
|
|
37
|
+
raise "PULL sockets cannot send"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#
|
|
41
|
+
def stop
|
|
42
|
+
@tasks.each(&:stop)
|
|
43
|
+
@tasks.clear
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# PUSH socket routing: round-robin send to PULL peers.
|
|
7
|
+
#
|
|
8
|
+
class Push
|
|
9
|
+
include RoundRobin
|
|
10
|
+
|
|
11
|
+
# @param engine [Engine]
|
|
12
|
+
#
|
|
13
|
+
def initialize(engine)
|
|
14
|
+
@engine = engine
|
|
15
|
+
@tasks = []
|
|
16
|
+
init_round_robin(engine)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Async::LimitedQueue]
|
|
20
|
+
#
|
|
21
|
+
attr_reader :send_queue
|
|
22
|
+
|
|
23
|
+
# PUSH is write-only.
|
|
24
|
+
#
|
|
25
|
+
def recv_queue
|
|
26
|
+
raise "PUSH sockets cannot receive"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param connection [Connection]
|
|
30
|
+
#
|
|
31
|
+
def connection_added(connection)
|
|
32
|
+
@connections << connection
|
|
33
|
+
signal_connection_available
|
|
34
|
+
start_send_pump unless @send_pump_started
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param connection [Connection]
|
|
38
|
+
#
|
|
39
|
+
def connection_removed(connection)
|
|
40
|
+
@connections.delete(connection)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param parts [Array<String>]
|
|
44
|
+
#
|
|
45
|
+
def enqueue(parts)
|
|
46
|
+
@send_queue.enqueue(parts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
def stop
|
|
51
|
+
@tasks.each(&:stop)
|
|
52
|
+
@tasks.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# REP socket routing: fair-queue receive, reply routed back to sender.
|
|
7
|
+
#
|
|
8
|
+
# REP strips the routing envelope (everything up to and including the
|
|
9
|
+
# empty delimiter) on receive, saves it internally, and restores it
|
|
10
|
+
# on send.
|
|
11
|
+
#
|
|
12
|
+
class Rep
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
18
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
19
|
+
@pending_replies = []
|
|
20
|
+
@tasks = []
|
|
21
|
+
@send_pump_started = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Async::LimitedQueue]
|
|
25
|
+
#
|
|
26
|
+
attr_reader :recv_queue, :send_queue
|
|
27
|
+
|
|
28
|
+
# @param connection [Connection]
|
|
29
|
+
#
|
|
30
|
+
def connection_added(connection)
|
|
31
|
+
transform = ->(msg) {
|
|
32
|
+
envelope = []
|
|
33
|
+
while msg.first && !msg.first.empty?
|
|
34
|
+
envelope << msg.shift
|
|
35
|
+
end
|
|
36
|
+
msg.shift # remove empty delimiter
|
|
37
|
+
@pending_replies << { conn: connection, envelope: envelope }
|
|
38
|
+
msg
|
|
39
|
+
}
|
|
40
|
+
task = @engine.start_recv_pump(connection, @recv_queue, transform: transform)
|
|
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
|
+
private
|
|
66
|
+
|
|
67
|
+
def start_send_pump
|
|
68
|
+
@send_pump_started = true
|
|
69
|
+
@tasks << Reactor.spawn_pump do
|
|
70
|
+
loop do
|
|
71
|
+
parts = @send_queue.dequeue
|
|
72
|
+
reply_info = @pending_replies.shift
|
|
73
|
+
next unless reply_info
|
|
74
|
+
reply_info[:conn].send_message([*reply_info[:envelope], "".b, *parts])
|
|
75
|
+
end
|
|
76
|
+
rescue EOFError
|
|
77
|
+
# connection lost mid-write
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# REQ socket routing: round-robin send with strict send/recv alternation.
|
|
7
|
+
#
|
|
8
|
+
# REQ prepends an empty delimiter frame on send and strips it on receive.
|
|
9
|
+
#
|
|
10
|
+
class Req
|
|
11
|
+
include RoundRobin
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
18
|
+
@tasks = []
|
|
19
|
+
init_round_robin(engine)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Async::LimitedQueue]
|
|
23
|
+
#
|
|
24
|
+
attr_reader :recv_queue, :send_queue
|
|
25
|
+
|
|
26
|
+
# @param connection [Connection]
|
|
27
|
+
#
|
|
28
|
+
def connection_added(connection)
|
|
29
|
+
@connections << connection
|
|
30
|
+
signal_connection_available
|
|
31
|
+
task = @engine.start_recv_pump(connection, @recv_queue,
|
|
32
|
+
transform: method(:transform_recv))
|
|
33
|
+
@tasks << task if task
|
|
34
|
+
start_send_pump unless @send_pump_started
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param connection [Connection]
|
|
38
|
+
#
|
|
39
|
+
def connection_removed(connection)
|
|
40
|
+
@connections.delete(connection)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param parts [Array<String>]
|
|
44
|
+
#
|
|
45
|
+
def enqueue(parts)
|
|
46
|
+
@send_queue.enqueue(parts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
def stop
|
|
51
|
+
@tasks.each(&:stop)
|
|
52
|
+
@tasks.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# REQ prepends empty delimiter frame on the wire.
|
|
58
|
+
#
|
|
59
|
+
def transform_send(parts) = ["".b, *parts]
|
|
60
|
+
|
|
61
|
+
# REQ strips the leading empty delimiter frame on receive.
|
|
62
|
+
#
|
|
63
|
+
def transform_recv(msg)
|
|
64
|
+
msg.shift if msg.first&.empty?
|
|
65
|
+
msg
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# Mixin for routing strategies that send via round-robin.
|
|
7
|
+
#
|
|
8
|
+
# Provides reactive connection management: Async::Promise waits
|
|
9
|
+
# for the first connection, Array#cycle handles round-robin,
|
|
10
|
+
# and a new Promise is created when all connections drop.
|
|
11
|
+
#
|
|
12
|
+
# Including classes must call `init_round_robin(engine)` from
|
|
13
|
+
# their #initialize.
|
|
14
|
+
#
|
|
15
|
+
module RoundRobin
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def init_round_robin(engine)
|
|
19
|
+
@connections = []
|
|
20
|
+
@cycle = @connections.cycle
|
|
21
|
+
@connection_available = Async::Promise.new
|
|
22
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
23
|
+
@send_pump_started = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def signal_connection_available
|
|
27
|
+
unless @connection_available.resolved?
|
|
28
|
+
@connection_available.resolve(true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Blocks until a connection is available, then returns
|
|
33
|
+
# the next one in round-robin order.
|
|
34
|
+
#
|
|
35
|
+
# @return [Connection]
|
|
36
|
+
#
|
|
37
|
+
def next_connection
|
|
38
|
+
@cycle.next
|
|
39
|
+
rescue StopIteration
|
|
40
|
+
@connection_available = Async::Promise.new
|
|
41
|
+
@connection_available.wait
|
|
42
|
+
@cycle = @connections.cycle
|
|
43
|
+
retry
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Transforms parts before sending. Override in subclasses
|
|
47
|
+
# (e.g. REQ prepends an empty delimiter frame).
|
|
48
|
+
#
|
|
49
|
+
# @param parts [Array<String>]
|
|
50
|
+
# @return [Array<String>]
|
|
51
|
+
#
|
|
52
|
+
def transform_send(parts) = parts
|
|
53
|
+
|
|
54
|
+
def start_send_pump
|
|
55
|
+
@send_pump_started = true
|
|
56
|
+
@tasks << Reactor.spawn_pump do
|
|
57
|
+
loop do
|
|
58
|
+
parts = @send_queue.dequeue
|
|
59
|
+
conn = next_connection
|
|
60
|
+
conn.send_message(transform_send(parts))
|
|
61
|
+
end
|
|
62
|
+
rescue EOFError, IOError
|
|
63
|
+
# connection lost mid-write
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module OMQ
|
|
7
|
+
module ZMTP
|
|
8
|
+
module Routing
|
|
9
|
+
# ROUTER socket routing: identity-based routing.
|
|
10
|
+
#
|
|
11
|
+
# Prepends peer identity frame on receive. Uses first frame as
|
|
12
|
+
# routing identity on send.
|
|
13
|
+
#
|
|
14
|
+
class Router
|
|
15
|
+
# @param engine [Engine]
|
|
16
|
+
#
|
|
17
|
+
def initialize(engine)
|
|
18
|
+
@engine = engine
|
|
19
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
20
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
21
|
+
@connections_by_identity = {}
|
|
22
|
+
@tasks = []
|
|
23
|
+
@send_pump_started = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Async::LimitedQueue]
|
|
27
|
+
#
|
|
28
|
+
attr_reader :recv_queue, :send_queue
|
|
29
|
+
|
|
30
|
+
# @param connection [Connection]
|
|
31
|
+
#
|
|
32
|
+
def connection_added(connection)
|
|
33
|
+
identity = connection.peer_identity
|
|
34
|
+
identity = SecureRandom.bytes(5) if identity.nil? || identity.empty?
|
|
35
|
+
@connections_by_identity[identity] = connection
|
|
36
|
+
|
|
37
|
+
task = @engine.start_recv_pump(connection, @recv_queue,
|
|
38
|
+
transform: ->(msg) { [identity, *msg] })
|
|
39
|
+
@tasks << task if task
|
|
40
|
+
|
|
41
|
+
start_send_pump unless @send_pump_started
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param connection [Connection]
|
|
45
|
+
#
|
|
46
|
+
def connection_removed(connection)
|
|
47
|
+
@connections_by_identity.reject! { |_, c| c == connection }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Enqueues a message for sending.
|
|
51
|
+
#
|
|
52
|
+
# @param parts [Array<String>]
|
|
53
|
+
#
|
|
54
|
+
def enqueue(parts)
|
|
55
|
+
@send_queue.enqueue(parts)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stop
|
|
59
|
+
@tasks.each(&:stop)
|
|
60
|
+
@tasks.clear
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def start_send_pump
|
|
66
|
+
@send_pump_started = true
|
|
67
|
+
@tasks << Reactor.spawn_pump do
|
|
68
|
+
loop do
|
|
69
|
+
parts = @send_queue.dequeue
|
|
70
|
+
identity = parts.first
|
|
71
|
+
conn = @connections_by_identity[identity]
|
|
72
|
+
|
|
73
|
+
unless conn
|
|
74
|
+
if @engine.options.router_mandatory?
|
|
75
|
+
raise SocketError, "no route to identity #{identity.inspect}"
|
|
76
|
+
end
|
|
77
|
+
next # silently drop
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Send everything after the identity frame
|
|
81
|
+
conn.send_message(parts[1..])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|