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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # DEALER socket routing: round-robin send, fair-queue receive.
6
+ #
7
+ # No envelope manipulation — messages pass through unchanged.
8
+ #
9
+ class Dealer
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)
32
+ @tasks << task if task
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
+ update_direct_pipe
41
+ end
42
+
43
+ # @param parts [Array<String>]
44
+ #
45
+ def enqueue(parts)
46
+ enqueue_round_robin(parts)
47
+ end
48
+
49
+ #
50
+ def stop
51
+ @tasks.each(&:stop)
52
+ @tasks.clear
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,78 @@
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
+
12
+ # @param engine [Engine]
13
+ #
14
+ def initialize(engine)
15
+ @engine = engine
16
+ @connections = []
17
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
+ @groups = Set.new
19
+ @tasks = []
20
+ end
21
+
22
+ # @return [Async::LimitedQueue]
23
+ #
24
+ attr_reader :recv_queue
25
+
26
+ # @param connection [Connection]
27
+ #
28
+ def connection_added(connection)
29
+ @connections << connection
30
+ # Send existing group memberships to new peer
31
+ @groups.each do |group|
32
+ connection.send_command(Protocol::ZMTP::Codec::Command.join(group))
33
+ end
34
+ task = @engine.start_recv_pump(connection, @recv_queue)
35
+ @tasks << task if task
36
+ end
37
+
38
+ # @param connection [Connection]
39
+ #
40
+ def connection_removed(connection)
41
+ @connections.delete(connection)
42
+ end
43
+
44
+ # DISH is read-only.
45
+ #
46
+ def enqueue(_parts)
47
+ raise "DISH sockets cannot send"
48
+ end
49
+
50
+ # Joins a group.
51
+ #
52
+ # @param group [String]
53
+ #
54
+ def join(group)
55
+ @groups << group
56
+ @connections.each do |conn|
57
+ conn.send_command(Protocol::ZMTP::Codec::Command.join(group))
58
+ end
59
+ end
60
+
61
+ # Leaves a group.
62
+ #
63
+ # @param group [String]
64
+ #
65
+ def leave(group)
66
+ @groups.delete(group)
67
+ @connections.each do |conn|
68
+ conn.send_command(Protocol::ZMTP::Codec::Command.leave(group))
69
+ end
70
+ end
71
+
72
+ def stop
73
+ @tasks.each(&:stop)
74
+ @tasks.clear
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Mixin for routing strategies that fan-out to subscribers.
6
+ #
7
+ # Manages per-connection subscription sets, subscription command
8
+ # listeners, and a send pump that delivers to all matching peers.
9
+ #
10
+ # Including classes must call `init_fan_out(engine)` from
11
+ # their #initialize.
12
+ #
13
+ module FanOut
14
+ attr_reader :subscriber_joined
15
+
16
+ private
17
+
18
+ def init_fan_out(engine)
19
+ @connections = []
20
+ @subscriptions = {} # connection => Set of prefixes
21
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
22
+ @send_pump_started = false
23
+ @send_pump_idle = true
24
+ @conflate = engine.options.conflate
25
+ @subscriber_joined = Async::Promise.new
26
+ @written = Set.new
27
+ @latest = {} if @conflate
28
+ end
29
+
30
+ # @return [Boolean] whether the connection is subscribed to the topic
31
+ #
32
+ def subscribed?(conn, topic)
33
+ subs = @subscriptions[conn]
34
+ return false unless subs
35
+ subs.any? { |prefix| topic.start_with?(prefix) }
36
+ end
37
+
38
+ # Called when a subscription command is received from a peer.
39
+ # Override in subclasses to expose subscriptions to the
40
+ # application (e.g. XPUB enqueues to recv_queue).
41
+ #
42
+ # @param conn [Connection]
43
+ # @param prefix [String]
44
+ #
45
+ def on_subscribe(conn, prefix)
46
+ @subscriptions[conn] << prefix.b.freeze
47
+ @subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
48
+ end
49
+
50
+ # Called when a cancel command is received from a peer.
51
+ # Override in subclasses (e.g. XPUB enqueues to recv_queue).
52
+ #
53
+ # @param conn [Connection]
54
+ # @param prefix [String]
55
+ #
56
+ def on_cancel(conn, prefix)
57
+ @subscriptions[conn]&.delete(prefix)
58
+ end
59
+
60
+ # @return [Boolean] true when the send pump is idle (not sending a batch)
61
+ def send_pump_idle? = @send_pump_idle
62
+
63
+
64
+ def start_send_pump
65
+ @send_pump_started = true
66
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
67
+ loop do
68
+ @send_pump_idle = true
69
+ batch = [@send_queue.dequeue]
70
+ @send_pump_idle = false
71
+ Routing.drain_send_queue(@send_queue, batch)
72
+
73
+ @written.clear
74
+
75
+ if @conflate
76
+ # Keep only the last matching message per connection.
77
+ @latest.clear
78
+ batch.each do |parts|
79
+ topic = parts.first || EMPTY_BINARY
80
+ @connections.each do |conn|
81
+ next unless subscribed?(conn, topic)
82
+ @latest[conn] = parts
83
+ end
84
+ end
85
+ @latest.each do |conn, parts|
86
+ begin
87
+ conn.write_message(parts)
88
+ @written << conn
89
+ rescue *CONNECTION_LOST
90
+ end
91
+ end
92
+ else
93
+ batch.each do |parts|
94
+ topic = parts.first || EMPTY_BINARY
95
+ wire_bytes = nil
96
+
97
+ @connections.each do |conn|
98
+ next unless subscribed?(conn, topic)
99
+ begin
100
+ if conn.respond_to?(:curve?) && conn.curve?
101
+ conn.write_message(parts)
102
+ elsif conn.respond_to?(:write_wire)
103
+ wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(parts)
104
+ conn.write_wire(wire_bytes)
105
+ else
106
+ conn.write_message(parts)
107
+ end
108
+ @written << conn
109
+ rescue *CONNECTION_LOST
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ @written.each do |conn|
116
+ conn.flush
117
+ rescue *CONNECTION_LOST
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def start_subscription_listener(conn)
124
+ @tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
125
+ loop do
126
+ frame = conn.read_frame
127
+ next unless frame.command?
128
+ cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
129
+ case cmd.name
130
+ when "SUBSCRIBE" then on_subscribe(conn, cmd.data)
131
+ when "CANCEL" then on_cancel(conn, cmd.data)
132
+ end
133
+ end
134
+ rescue *CONNECTION_LOST
135
+ @engine.connection_lost(conn)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,46 @@
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
+ # @param engine [Engine]
9
+ #
10
+ def initialize(engine)
11
+ @engine = engine
12
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
13
+ @tasks = []
14
+ end
15
+
16
+ # @return [Async::LimitedQueue]
17
+ #
18
+ attr_reader :recv_queue
19
+
20
+ # @param connection [Connection]
21
+ #
22
+ def connection_added(connection)
23
+ task = @engine.start_recv_pump(connection, @recv_queue)
24
+ @tasks << task if task
25
+ end
26
+
27
+ # @param connection [Connection]
28
+ #
29
+ def connection_removed(connection)
30
+ # recv pump stops on CONNECTION_LOST
31
+ end
32
+
33
+ # GATHER is read-only.
34
+ #
35
+ def enqueue(_parts)
36
+ raise "GATHER sockets cannot send"
37
+ end
38
+
39
+ #
40
+ def stop
41
+ @tasks.each(&:stop)
42
+ @tasks.clear
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # PAIR socket routing: exclusive 1-to-1 bidirectional.
6
+ #
7
+ # Only one peer connection is allowed. Messages flow through
8
+ # internal send/recv queues backed by Async::LimitedQueue.
9
+ #
10
+ class Pair
11
+
12
+ # @param engine [Engine]
13
+ #
14
+ def initialize(engine)
15
+ @engine = engine
16
+ @connection = nil
17
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
19
+ @tasks = []
20
+ @send_pump_idle = true
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) unless connection.is_a?(Transport::Inproc::DirectPipe)
36
+ end
37
+
38
+ # @param connection [Connection]
39
+ #
40
+ def connection_removed(connection)
41
+ if @connection == connection
42
+ @connection = nil
43
+ @send_pump&.stop
44
+ @send_pump = nil
45
+ end
46
+ end
47
+
48
+ # @param parts [Array<String>]
49
+ #
50
+ def enqueue(parts)
51
+ conn = @connection
52
+ if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
53
+ conn.send_message(parts)
54
+ else
55
+ @send_queue.enqueue(parts)
56
+ end
57
+ end
58
+
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(conn)
70
+ @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
71
+ loop do
72
+ @send_pump_idle = true
73
+ batch = [@send_queue.dequeue]
74
+ @send_pump_idle = false
75
+ Routing.drain_send_queue(@send_queue, batch)
76
+ batch.each { |parts| conn.write_message(parts) }
77
+ conn.flush
78
+ end
79
+ rescue *CONNECTION_LOST
80
+ @engine.connection_lost(conn)
81
+ end
82
+ @tasks << @send_pump
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module Routing
7
+ # PEER socket routing: bidirectional multi-peer 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 Peer
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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # PUB socket routing: fan-out to all subscribers.
6
+ #
7
+ # Listens for SUBSCRIBE/CANCEL commands from peers.
8
+ # Drops messages if a subscriber's connection write fails.
9
+ #
10
+ class Pub
11
+ include FanOut
12
+
13
+ # @param engine [Engine]
14
+ #
15
+ def initialize(engine)
16
+ @engine = engine
17
+ @tasks = []
18
+ init_fan_out(engine)
19
+ end
20
+
21
+ # @return [Async::LimitedQueue]
22
+ #
23
+ attr_reader :send_queue
24
+
25
+ # PUB is write-only.
26
+ #
27
+ def recv_queue
28
+ raise "PUB sockets cannot receive"
29
+ end
30
+
31
+ # @param connection [Connection]
32
+ #
33
+ def connection_added(connection)
34
+ @connections << connection
35
+ @subscriptions[connection] = Set.new
36
+ start_subscription_listener(connection)
37
+ start_send_pump unless @send_pump_started
38
+ end
39
+
40
+ # @param connection [Connection]
41
+ #
42
+ def connection_removed(connection)
43
+ @connections.delete(connection)
44
+ @subscriptions.delete(connection)
45
+ end
46
+
47
+ # @param parts [Array<String>]
48
+ #
49
+ def enqueue(parts)
50
+ @send_queue.enqueue(parts)
51
+ end
52
+
53
+ #
54
+ def stop
55
+ @tasks.each(&:stop)
56
+ @tasks.clear
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # PULL socket routing: fair-queue receive from PUSH peers.
6
+ #
7
+ class Pull
8
+ # @param engine [Engine]
9
+ #
10
+ def initialize(engine)
11
+ @engine = engine
12
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
13
+ @tasks = []
14
+ end
15
+
16
+ # @return [Async::LimitedQueue]
17
+ #
18
+ attr_reader :recv_queue
19
+
20
+ # @param connection [Connection]
21
+ #
22
+ def connection_added(connection)
23
+ task = @engine.start_recv_pump(connection, @recv_queue)
24
+ @tasks << task if task
25
+ end
26
+
27
+ # @param connection [Connection]
28
+ #
29
+ def connection_removed(connection)
30
+ # recv pump stops on EOFError
31
+ end
32
+
33
+ # PULL is read-only.
34
+ #
35
+ def enqueue(_parts)
36
+ raise "PULL sockets cannot send"
37
+ end
38
+
39
+ #
40
+ def stop
41
+ @tasks.each(&:stop)
42
+ @tasks.clear
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # PUSH socket routing: round-robin send to PULL peers.
6
+ #
7
+ class Push
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
+ # PUSH is write-only.
25
+ #
26
+ def recv_queue
27
+ raise "PUSH 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. Without
68
+ # this, a dead peer is only noticed on the next send — which
69
+ # may succeed if the kernel send buffer absorbs the data.
70
+ #
71
+ def start_reaper(conn)
72
+ return if conn.is_a?(Transport::Inproc::DirectPipe)
73
+ @tasks << @engine.spawn_pump_task(annotation: "reaper") do
74
+ conn.receive_message # blocks until peer disconnects
75
+ rescue *CONNECTION_LOST
76
+ @engine.connection_lost(conn)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end