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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -0
  3. data/README.md +28 -3
  4. data/lib/omq/channel.rb +5 -5
  5. data/lib/omq/client_server.rb +10 -10
  6. data/lib/omq/engine.rb +702 -0
  7. data/lib/omq/options.rb +48 -0
  8. data/lib/omq/pair.rb +4 -4
  9. data/lib/omq/peer.rb +5 -5
  10. data/lib/omq/pub_sub.rb +18 -18
  11. data/lib/omq/push_pull.rb +6 -6
  12. data/lib/omq/queue_interface.rb +73 -0
  13. data/lib/omq/radio_dish.rb +6 -6
  14. data/lib/omq/reactor.rb +128 -0
  15. data/lib/omq/readable.rb +44 -0
  16. data/lib/omq/req_rep.rb +8 -8
  17. data/lib/omq/router_dealer.rb +8 -8
  18. data/lib/omq/routing/channel.rb +83 -0
  19. data/lib/omq/routing/client.rb +56 -0
  20. data/lib/omq/routing/dealer.rb +57 -0
  21. data/lib/omq/routing/dish.rb +78 -0
  22. data/lib/omq/routing/fan_out.rb +140 -0
  23. data/lib/omq/routing/gather.rb +46 -0
  24. data/lib/omq/routing/pair.rb +86 -0
  25. data/lib/omq/routing/peer.rb +101 -0
  26. data/lib/omq/routing/pub.rb +60 -0
  27. data/lib/omq/routing/pull.rb +46 -0
  28. data/lib/omq/routing/push.rb +81 -0
  29. data/lib/omq/routing/radio.rb +150 -0
  30. data/lib/omq/routing/rep.rb +101 -0
  31. data/lib/omq/routing/req.rb +65 -0
  32. data/lib/omq/routing/round_robin.rb +168 -0
  33. data/lib/omq/routing/router.rb +110 -0
  34. data/lib/omq/routing/scatter.rb +82 -0
  35. data/lib/omq/routing/server.rb +101 -0
  36. data/lib/omq/routing/sub.rb +78 -0
  37. data/lib/omq/routing/xpub.rb +72 -0
  38. data/lib/omq/routing/xsub.rb +83 -0
  39. data/lib/omq/routing.rb +66 -0
  40. data/lib/omq/scatter_gather.rb +8 -8
  41. data/lib/omq/single_frame.rb +18 -0
  42. data/lib/omq/socket.rb +32 -11
  43. data/lib/omq/transport/inproc.rb +355 -0
  44. data/lib/omq/transport/ipc.rb +117 -0
  45. data/lib/omq/transport/tcp.rb +111 -0
  46. data/lib/omq/transport/tls.rb +146 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq/writable.rb +66 -0
  49. data/lib/omq.rb +64 -4
  50. metadata +34 -33
  51. data/lib/omq/zmtp/engine.rb +0 -551
  52. data/lib/omq/zmtp/options.rb +0 -48
  53. data/lib/omq/zmtp/reactor.rb +0 -131
  54. data/lib/omq/zmtp/readable.rb +0 -29
  55. data/lib/omq/zmtp/routing/channel.rb +0 -81
  56. data/lib/omq/zmtp/routing/client.rb +0 -56
  57. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  58. data/lib/omq/zmtp/routing/dish.rb +0 -80
  59. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  60. data/lib/omq/zmtp/routing/gather.rb +0 -48
  61. data/lib/omq/zmtp/routing/pair.rb +0 -84
  62. data/lib/omq/zmtp/routing/peer.rb +0 -100
  63. data/lib/omq/zmtp/routing/pub.rb +0 -62
  64. data/lib/omq/zmtp/routing/pull.rb +0 -48
  65. data/lib/omq/zmtp/routing/push.rb +0 -80
  66. data/lib/omq/zmtp/routing/radio.rb +0 -139
  67. data/lib/omq/zmtp/routing/rep.rb +0 -101
  68. data/lib/omq/zmtp/routing/req.rb +0 -65
  69. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  70. data/lib/omq/zmtp/routing/router.rb +0 -109
  71. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  72. data/lib/omq/zmtp/routing/server.rb +0 -100
  73. data/lib/omq/zmtp/routing/sub.rb +0 -80
  74. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  75. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  76. data/lib/omq/zmtp/routing.rb +0 -65
  77. data/lib/omq/zmtp/single_frame.rb +0 -20
  78. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  79. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  80. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  81. data/lib/omq/zmtp/writable.rb +0 -61
  82. 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