omq 0.8.0 → 0.10.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -0
  3. data/README.md +9 -49
  4. data/lib/omq/channel.rb +3 -3
  5. data/lib/omq/client_server.rb +6 -6
  6. data/lib/omq/engine.rb +641 -0
  7. data/lib/omq/options.rb +46 -0
  8. data/lib/omq/pair.rb +2 -2
  9. data/lib/omq/peer.rb +3 -3
  10. data/lib/omq/pub_sub.rb +6 -6
  11. data/lib/omq/push_pull.rb +2 -2
  12. data/lib/omq/radio_dish.rb +2 -2
  13. data/lib/omq/reactor.rb +128 -0
  14. data/lib/omq/readable.rb +42 -0
  15. data/lib/omq/req_rep.rb +4 -4
  16. data/lib/omq/router_dealer.rb +4 -4
  17. data/lib/omq/routing/channel.rb +83 -0
  18. data/lib/omq/routing/client.rb +56 -0
  19. data/lib/omq/routing/dealer.rb +57 -0
  20. data/lib/omq/routing/dish.rb +78 -0
  21. data/lib/omq/routing/fan_out.rb +131 -0
  22. data/lib/omq/routing/gather.rb +46 -0
  23. data/lib/omq/routing/pair.rb +86 -0
  24. data/lib/omq/routing/peer.rb +101 -0
  25. data/lib/omq/routing/pub.rb +60 -0
  26. data/lib/omq/routing/pull.rb +46 -0
  27. data/lib/omq/routing/push.rb +81 -0
  28. data/lib/omq/routing/radio.rb +140 -0
  29. data/lib/omq/routing/rep.rb +101 -0
  30. data/lib/omq/routing/req.rb +65 -0
  31. data/lib/omq/routing/round_robin.rb +168 -0
  32. data/lib/omq/routing/router.rb +110 -0
  33. data/lib/omq/routing/scatter.rb +82 -0
  34. data/lib/omq/routing/server.rb +101 -0
  35. data/lib/omq/routing/sub.rb +78 -0
  36. data/lib/omq/routing/xpub.rb +72 -0
  37. data/lib/omq/routing/xsub.rb +83 -0
  38. data/lib/omq/routing.rb +66 -0
  39. data/lib/omq/scatter_gather.rb +4 -4
  40. data/lib/omq/single_frame.rb +18 -0
  41. data/lib/omq/socket.rb +24 -9
  42. data/lib/omq/transport/inproc.rb +355 -0
  43. data/lib/omq/transport/ipc.rb +117 -0
  44. data/lib/omq/transport/tcp.rb +111 -0
  45. data/lib/omq/version.rb +1 -1
  46. data/lib/omq/writable.rb +65 -0
  47. data/lib/omq.rb +60 -4
  48. metadata +38 -58
  49. data/exe/omq +0 -6
  50. data/lib/omq/cli/base_runner.rb +0 -459
  51. data/lib/omq/cli/channel.rb +0 -8
  52. data/lib/omq/cli/client_server.rb +0 -111
  53. data/lib/omq/cli/config.rb +0 -54
  54. data/lib/omq/cli/formatter.rb +0 -75
  55. data/lib/omq/cli/pair.rb +0 -31
  56. data/lib/omq/cli/peer.rb +0 -8
  57. data/lib/omq/cli/pipe.rb +0 -265
  58. data/lib/omq/cli/pub_sub.rb +0 -14
  59. data/lib/omq/cli/push_pull.rb +0 -14
  60. data/lib/omq/cli/radio_dish.rb +0 -27
  61. data/lib/omq/cli/req_rep.rb +0 -83
  62. data/lib/omq/cli/router_dealer.rb +0 -76
  63. data/lib/omq/cli/scatter_gather.rb +0 -14
  64. data/lib/omq/cli.rb +0 -540
  65. data/lib/omq/zmtp/engine.rb +0 -551
  66. data/lib/omq/zmtp/options.rb +0 -48
  67. data/lib/omq/zmtp/reactor.rb +0 -131
  68. data/lib/omq/zmtp/readable.rb +0 -29
  69. data/lib/omq/zmtp/routing/channel.rb +0 -81
  70. data/lib/omq/zmtp/routing/client.rb +0 -56
  71. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  72. data/lib/omq/zmtp/routing/dish.rb +0 -80
  73. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  74. data/lib/omq/zmtp/routing/gather.rb +0 -48
  75. data/lib/omq/zmtp/routing/pair.rb +0 -84
  76. data/lib/omq/zmtp/routing/peer.rb +0 -100
  77. data/lib/omq/zmtp/routing/pub.rb +0 -62
  78. data/lib/omq/zmtp/routing/pull.rb +0 -48
  79. data/lib/omq/zmtp/routing/push.rb +0 -80
  80. data/lib/omq/zmtp/routing/radio.rb +0 -139
  81. data/lib/omq/zmtp/routing/rep.rb +0 -101
  82. data/lib/omq/zmtp/routing/req.rb +0 -65
  83. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  84. data/lib/omq/zmtp/routing/router.rb +0 -109
  85. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  86. data/lib/omq/zmtp/routing/server.rb +0 -100
  87. data/lib/omq/zmtp/routing/sub.rb +0 -80
  88. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  89. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  90. data/lib/omq/zmtp/routing.rb +0 -65
  91. data/lib/omq/zmtp/single_frame.rb +0 -20
  92. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  93. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  94. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  95. data/lib/omq/zmtp/writable.rb +0 -61
  96. data/lib/omq/zmtp.rb +0 -81
@@ -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
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module Routing
7
+ # SERVER socket routing: identity-based routing with auto-generated
8
+ # 4-byte routing IDs.
9
+ #
10
+ # Prepends routing ID on receive. Strips routing ID on send and
11
+ # routes to the identified connection.
12
+ #
13
+ class Server
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_routing_id = {}
21
+ @routing_id_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
+ routing_id = SecureRandom.bytes(4)
36
+ @connections_by_routing_id[routing_id] = connection
37
+ @routing_id_by_connection[connection] = routing_id
38
+
39
+ task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
40
+ [routing_id, *msg]
41
+ end
42
+ @tasks << task if task
43
+
44
+ start_send_pump unless @send_pump_started
45
+ end
46
+
47
+ # @param connection [Connection]
48
+ #
49
+ def connection_removed(connection)
50
+ routing_id = @routing_id_by_connection.delete(connection)
51
+ @connections_by_routing_id.delete(routing_id) if routing_id
52
+ end
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
+ routing_id = parts.first
81
+ conn = @connections_by_routing_id[routing_id]
82
+ next unless conn # silently drop if peer gone
83
+ begin
84
+ conn.write_message(parts[1..])
85
+ @written << conn
86
+ rescue *CONNECTION_LOST
87
+ # will be cleaned up
88
+ end
89
+ end
90
+
91
+ @written.each do |conn|
92
+ conn.flush
93
+ rescue *CONNECTION_LOST
94
+ # will be cleaned up
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # SUB socket routing: subscription-based receive from PUB peers.
6
+ #
7
+ # Sends SUBSCRIBE/CANCEL commands to connected PUB peers.
8
+ #
9
+ class Sub
10
+
11
+ # @param engine [Engine]
12
+ #
13
+ def initialize(engine)
14
+ @engine = engine
15
+ @connections = []
16
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
17
+ @subscriptions = Set.new
18
+ @tasks = []
19
+ end
20
+
21
+ # @return [Async::LimitedQueue]
22
+ #
23
+ attr_reader :recv_queue
24
+
25
+ # @param connection [Connection]
26
+ #
27
+ def connection_added(connection)
28
+ @connections << connection
29
+ # Send existing subscriptions to new peer
30
+ @subscriptions.each do |prefix|
31
+ connection.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
32
+ end
33
+ task = @engine.start_recv_pump(connection, @recv_queue)
34
+ @tasks << task if task
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_removed(connection)
40
+ @connections.delete(connection)
41
+ end
42
+
43
+ # SUB is read-only.
44
+ #
45
+ def enqueue(_parts)
46
+ raise "SUB sockets cannot send"
47
+ end
48
+
49
+ # Subscribes to a topic prefix.
50
+ #
51
+ # @param prefix [String]
52
+ #
53
+ def subscribe(prefix)
54
+ @subscriptions << prefix
55
+ @connections.each do |conn|
56
+ conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
57
+ end
58
+ end
59
+
60
+ # Unsubscribes from a topic prefix.
61
+ #
62
+ # @param prefix [String]
63
+ #
64
+ def unsubscribe(prefix)
65
+ @subscriptions.delete(prefix)
66
+ @connections.each do |conn|
67
+ conn.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix))
68
+ end
69
+ end
70
+
71
+ def stop
72
+ @tasks.each(&:stop)
73
+ @tasks.clear
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # XPUB socket routing: like PUB but exposes subscription messages.
6
+ #
7
+ # Subscription/unsubscription messages from peers are delivered to
8
+ # the application as data frames: \x01 + prefix for subscribe,
9
+ # \x00 + prefix for unsubscribe.
10
+ #
11
+ class XPub
12
+ include FanOut
13
+
14
+ # @param engine [Engine]
15
+ #
16
+ def initialize(engine)
17
+ @engine = engine
18
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
+ @tasks = []
20
+ init_fan_out(engine)
21
+ end
22
+
23
+ # @return [Async::LimitedQueue]
24
+ #
25
+ attr_reader :recv_queue, :send_queue
26
+
27
+ # @param connection [Connection]
28
+ #
29
+ def connection_added(connection)
30
+ @connections << connection
31
+ @subscriptions[connection] = Set.new
32
+ start_subscription_listener(connection)
33
+ start_send_pump unless @send_pump_started
34
+ end
35
+
36
+ # @param connection [Connection]
37
+ #
38
+ def connection_removed(connection)
39
+ @connections.delete(connection)
40
+ @subscriptions.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
+ # Expose subscription to application as data message.
58
+ #
59
+ def on_subscribe(conn, prefix)
60
+ super
61
+ @recv_queue.enqueue(["\x01#{prefix}".b])
62
+ end
63
+
64
+ # Expose unsubscription to application as data message.
65
+ #
66
+ def on_cancel(conn, prefix)
67
+ super
68
+ @recv_queue.enqueue(["\x00#{prefix}".b])
69
+ end
70
+ end
71
+ end
72
+ end