omq 0.22.0 → 0.23.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -0
  3. data/README.md +17 -21
  4. data/lib/omq/channel.rb +35 -0
  5. data/lib/omq/client_server.rb +72 -0
  6. data/lib/omq/constants.rb +68 -0
  7. data/lib/omq/engine/connection_lifecycle.rb +11 -3
  8. data/lib/omq/engine/heartbeat.rb +3 -4
  9. data/lib/omq/engine/maintenance.rb +4 -5
  10. data/lib/omq/engine/reconnect.rb +12 -11
  11. data/lib/omq/engine/recv_pump.rb +3 -4
  12. data/lib/omq/engine/socket_lifecycle.rb +26 -9
  13. data/lib/omq/engine.rb +196 -85
  14. data/lib/omq/peer.rb +49 -0
  15. data/lib/omq/pub_sub.rb +2 -2
  16. data/lib/omq/radio_dish.rb +122 -0
  17. data/lib/omq/reactor.rb +14 -5
  18. data/lib/omq/routing/channel.rb +110 -0
  19. data/lib/omq/routing/client.rb +70 -0
  20. data/lib/omq/routing/conn_send_pump.rb +12 -10
  21. data/lib/omq/routing/dealer.rb +1 -13
  22. data/lib/omq/routing/dish.rb +94 -0
  23. data/lib/omq/routing/fan_out.rb +14 -13
  24. data/lib/omq/routing/gather.rb +60 -0
  25. data/lib/omq/routing/pair.rb +7 -24
  26. data/lib/omq/routing/peer.rb +95 -0
  27. data/lib/omq/routing/pub.rb +0 -11
  28. data/lib/omq/routing/pull.rb +1 -13
  29. data/lib/omq/routing/push.rb +1 -10
  30. data/lib/omq/routing/radio.rb +187 -0
  31. data/lib/omq/routing/rep.rb +10 -20
  32. data/lib/omq/routing/req.rb +5 -17
  33. data/lib/omq/routing/round_robin.rb +17 -18
  34. data/lib/omq/routing/router.rb +3 -17
  35. data/lib/omq/routing/scatter.rb +77 -0
  36. data/lib/omq/routing/server.rb +90 -0
  37. data/lib/omq/routing/sub.rb +1 -13
  38. data/lib/omq/routing/xpub.rb +0 -11
  39. data/lib/omq/routing/xsub.rb +6 -23
  40. data/lib/omq/routing.rb +5 -2
  41. data/lib/omq/scatter_gather.rb +56 -0
  42. data/lib/omq/socket.rb +8 -23
  43. data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
  44. data/lib/omq/transport/inproc.rb +11 -3
  45. data/lib/omq/transport/ipc.rb +41 -13
  46. data/lib/omq/transport/tcp.rb +59 -23
  47. data/lib/omq/transport/udp.rb +281 -0
  48. data/lib/omq/version.rb +1 -1
  49. data/lib/omq.rb +9 -64
  50. metadata +16 -2
  51. data/lib/omq/monitor_event.rb +0 -16
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OMQ RADIO/DISH socket types with UDP transport (ZeroMQ RFC 48).
4
+ #
5
+ # Not loaded by +require "omq"+; opt in with:
6
+ #
7
+ # require "omq/radio_dish"
8
+ #
9
+ # Loading this file also registers the +udp://+ transport.
10
+
11
+ require "omq"
12
+ require_relative "routing/radio"
13
+ require_relative "routing/dish"
14
+ require_relative "transport/udp"
15
+
16
+ module OMQ
17
+ # Group-based publisher socket (ZeroMQ RFC 48).
18
+ #
19
+ # Sends messages to DISH peers that have joined the target group.
20
+ # Supports both TCP and UDP transports.
21
+ class RADIO < Socket
22
+ include Writable
23
+
24
+ # Creates a new RADIO socket.
25
+ #
26
+ # @param endpoints [String, Array<String>, nil] endpoint(s) to bind to
27
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
28
+ # @param on_mute [Symbol] behaviour when HWM is reached (+:drop_newest+ or +:block+)
29
+ # @param conflate [Boolean] if true, keep only the latest message per group per peer
30
+ # @param backend [Object, nil] optional transport backend
31
+ def initialize(endpoints = nil, linger: Float::INFINITY, on_mute: :drop_newest, conflate: false, backend: nil)
32
+ init_engine(:RADIO, on_mute: on_mute, conflate: conflate, backend: backend)
33
+ @options.linger = linger
34
+ attach_endpoints(endpoints, default: :bind)
35
+ end
36
+
37
+
38
+ # Publishes a message to a group.
39
+ #
40
+ # @param group [String] group name
41
+ # @param body [String] message body
42
+ # @return [self]
43
+ #
44
+ def publish(group, body)
45
+ parts = [group.b.freeze, body.b.freeze]
46
+ Reactor.run timeout: @options.write_timeout do
47
+ @engine.enqueue_send(parts)
48
+ end
49
+ self
50
+ end
51
+
52
+
53
+ # Sends a message to a group.
54
+ #
55
+ # @param message [String] message body (requires group: kwarg)
56
+ # @param group [String] group name
57
+ # @return [self]
58
+ #
59
+ def send(message, group: nil)
60
+ raise ArgumentError, "RADIO requires a group (use group: kwarg, publish, or << [group, body])" unless group
61
+ publish(group, message)
62
+ end
63
+
64
+
65
+ # Sends a message to a group via [group, body] array.
66
+ #
67
+ # @param message [Array<String>] [group, body]
68
+ # @return [self]
69
+ #
70
+ def <<(message)
71
+ raise ArgumentError, "RADIO requires [group, body] array" unless message.is_a?(Array) && message.size == 2
72
+ publish(message[0], message[1])
73
+ end
74
+ end
75
+
76
+
77
+ # Group-based subscriber socket (ZeroMQ RFC 48).
78
+ #
79
+ # Receives messages from RADIO peers for joined groups.
80
+ # Supports both TCP and UDP transports.
81
+ class DISH < Socket
82
+ include Readable
83
+
84
+ # Creates a new DISH socket.
85
+ #
86
+ # @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
87
+ # @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
88
+ # @param group [String, nil] initial group to join
89
+ # @param on_mute [Symbol] behaviour when HWM is reached (+:block+ or +:drop_newest+)
90
+ # @param backend [Object, nil] optional transport backend
91
+ def initialize(endpoints = nil, linger: Float::INFINITY, group: nil, on_mute: :block, backend: nil)
92
+ init_engine(:DISH, on_mute: on_mute, backend: backend)
93
+ @options.linger = linger
94
+ attach_endpoints(endpoints, default: :connect)
95
+ join(group) if group
96
+ end
97
+
98
+
99
+ # Joins a group.
100
+ #
101
+ # @param group [String]
102
+ # @return [void]
103
+ #
104
+ def join(group)
105
+ @engine.routing.join(group)
106
+ end
107
+
108
+
109
+ # Leaves a group.
110
+ #
111
+ # @param group [String]
112
+ # @return [void]
113
+ #
114
+ def leave(group)
115
+ @engine.routing.leave(group)
116
+ end
117
+ end
118
+
119
+
120
+ Routing.register(:RADIO, Routing::Radio)
121
+ Routing.register(:DISH, Routing::Dish)
122
+ end
data/lib/omq/reactor.rb CHANGED
@@ -25,6 +25,11 @@ module OMQ
25
25
 
26
26
 
27
27
  class << self
28
+ # @return [Hash{Numeric => Integer}] linger value → active socket count
29
+ #
30
+ attr_reader :lingers
31
+
32
+
28
33
  # Returns the root Async task inside the shared IO thread.
29
34
  # Starts the thread exactly once (double-checked lock).
30
35
  #
@@ -69,7 +74,7 @@ module OMQ
69
74
  else
70
75
  result = Async::Promise.new
71
76
  root_task # ensure started
72
- @work_queue.push([block, result, timeout])
77
+ @work_queue << [block, result, timeout]
73
78
  result.wait
74
79
  end
75
80
  end
@@ -89,9 +94,12 @@ module OMQ
89
94
  # @param seconds [Numeric, nil] linger value
90
95
  #
91
96
  def untrack_linger(seconds)
92
- key = seconds || 0
97
+ key = seconds || 0
93
98
  @lingers[key] -= 1
94
- @lingers.delete(key) if @lingers[key] <= 0
99
+
100
+ if @lingers[key] <= 0
101
+ @lingers.delete(key)
102
+ end
95
103
  end
96
104
 
97
105
 
@@ -103,13 +111,14 @@ module OMQ
103
111
  return unless @thread&.alive?
104
112
 
105
113
  max_linger = @lingers.empty? ? 0 : @lingers.keys.max
106
- @work_queue&.push(nil)
114
+
115
+ @work_queue << nil if @work_queue
107
116
  @thread&.join(max_linger + 1)
108
117
 
109
118
  @thread = nil
110
119
  @root_task = nil
111
120
  @work_queue = nil
112
- @lingers = Hash.new(0)
121
+ @lingers.clear
113
122
  end
114
123
 
115
124
 
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # CHANNEL socket routing: exclusive 1-to-1 bidirectional.
6
+ #
7
+ class Channel
8
+ # @return [Async::LimitedQueue]
9
+ #
10
+ attr_reader :recv_queue
11
+
12
+
13
+ # @param engine [Engine]
14
+ #
15
+ def initialize(engine)
16
+ @engine = engine
17
+ @connection = nil
18
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
+ @send_queue = nil
20
+ @staging_queue = Routing.build_queue(engine.options.send_hwm, :block)
21
+ end
22
+
23
+
24
+ # Dequeues the next received message. Blocks until one is available.
25
+ #
26
+ # @return [Array<String>, nil]
27
+ #
28
+ def dequeue_recv
29
+ @recv_queue.dequeue
30
+ end
31
+
32
+
33
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel.
34
+ #
35
+ # @return [void]
36
+ #
37
+ def unblock_recv
38
+ @recv_queue.enqueue(nil)
39
+ end
40
+
41
+
42
+ # @param connection [Connection]
43
+ # @raise [RuntimeError] if a connection already exists
44
+ #
45
+ def connection_added(connection)
46
+ raise "CHANNEL allows only one peer" if @connection
47
+ @connection = connection
48
+
49
+ @engine.start_recv_pump(connection, @recv_queue)
50
+
51
+ unless connection.is_a?(Transport::Inproc::DirectPipe)
52
+ @send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
53
+ while (msg = @staging_queue.dequeue(timeout: 0))
54
+ @send_queue.enqueue(msg)
55
+ end
56
+ start_send_pump(connection)
57
+ end
58
+ end
59
+
60
+
61
+ # @param connection [Connection]
62
+ #
63
+ def connection_removed(connection)
64
+ if @connection == connection
65
+ @connection = nil
66
+ @send_queue = nil
67
+ end
68
+ end
69
+
70
+
71
+ # @param parts [Array<String>]
72
+ #
73
+ def enqueue(parts)
74
+ conn = @connection
75
+ if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
76
+ conn.send_message(parts)
77
+ elsif @send_queue
78
+ @send_queue.enqueue(parts)
79
+ else
80
+ @staging_queue.enqueue(parts)
81
+ end
82
+ end
83
+
84
+
85
+ # True when the staging and send queues are empty.
86
+ #
87
+ def send_queues_drained?
88
+ @staging_queue.empty? && (@send_queue.nil? || @send_queue.empty?)
89
+ end
90
+
91
+
92
+ private
93
+
94
+
95
+ def start_send_pump(conn)
96
+ @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
97
+ batch = []
98
+
99
+ loop do
100
+ Routing.dequeue_batch(@send_queue, batch)
101
+ batch.each { |parts| conn.write_message(parts) }
102
+ conn.flush
103
+ batch.clear
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # CLIENT socket routing: round-robin send, fair-queue receive.
6
+ #
7
+ # Same as DEALER — no envelope manipulation.
8
+ #
9
+ class Client
10
+ include RoundRobin
11
+
12
+
13
+ # @return [Async::LimitedQueue]
14
+ #
15
+ attr_reader :recv_queue
16
+
17
+
18
+ # @param engine [Engine]
19
+ #
20
+ def initialize(engine)
21
+ @engine = engine
22
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
23
+ init_round_robin(engine)
24
+ end
25
+
26
+
27
+ # Dequeues the next received message. Blocks until one is available.
28
+ #
29
+ # @return [Array<String>, nil]
30
+ #
31
+ def dequeue_recv
32
+ @recv_queue.dequeue
33
+ end
34
+
35
+
36
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel.
37
+ #
38
+ # @return [void]
39
+ #
40
+ def unblock_recv
41
+ @recv_queue.enqueue(nil)
42
+ end
43
+
44
+
45
+ # @param connection [Connection]
46
+ #
47
+ def connection_added(connection)
48
+ @connections << connection
49
+ @engine.start_recv_pump(connection, @recv_queue)
50
+ add_round_robin_send_connection(connection)
51
+ end
52
+
53
+
54
+ # @param connection [Connection]
55
+ #
56
+ def connection_removed(connection)
57
+ @connections.delete(connection)
58
+ remove_round_robin_send_connection(connection)
59
+ end
60
+
61
+
62
+ # @param parts [Array<String>]
63
+ #
64
+ def enqueue(parts)
65
+ enqueue_round_robin(parts)
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -8,19 +8,20 @@ module OMQ
8
8
  # include the RoundRobin mixin.
9
9
  #
10
10
  module ConnSendPump
11
- # Spawns the pump task and registers it in +tasks+.
11
+ # Spawns the pump task on the connection's lifecycle barrier so it
12
+ # is torn down with the rest of the connection's pumps.
12
13
  #
13
14
  # @param engine [Engine]
14
15
  # @param conn [Connection]
15
16
  # @param q [Async::LimitedQueue]
16
- # @param tasks [Array]
17
17
  # @return [Async::Task]
18
18
  #
19
- def self.start(engine, conn, q, tasks)
20
- task = engine.spawn_conn_pump_task(conn, annotation: "send pump") do
19
+ def self.start(engine, conn, q)
20
+ engine.spawn_conn_pump_task(conn, annotation: "send pump") do
21
+ batch = []
22
+
21
23
  loop do
22
- batch = [q.dequeue]
23
- Routing.drain_send_queue(q, batch)
24
+ Routing.dequeue_batch(q, batch)
24
25
 
25
26
  if batch.size == 1
26
27
  conn.write_message batch.first
@@ -30,12 +31,13 @@ module OMQ
30
31
 
31
32
  conn.flush
32
33
 
33
- batch.each { |parts| engine.emit_verbose_msg_sent(conn, parts) }
34
+ batch.each do |parts|
35
+ engine.emit_verbose_msg_sent(conn, parts)
36
+ end
37
+
38
+ batch.clear
34
39
  end
35
40
  end
36
-
37
- tasks << task
38
- task
39
41
  end
40
42
 
41
43
  end
@@ -20,7 +20,6 @@ module OMQ
20
20
  def initialize(engine)
21
21
  @engine = engine
22
22
  @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
23
- @tasks = []
24
23
  init_round_robin(engine)
25
24
  end
26
25
 
@@ -46,8 +45,7 @@ module OMQ
46
45
  # @param connection [Connection]
47
46
  #
48
47
  def connection_added(connection)
49
- task = @engine.start_recv_pump(connection, @recv_queue)
50
- @tasks << task if task
48
+ @engine.start_recv_pump(connection, @recv_queue)
51
49
  add_round_robin_send_connection(connection)
52
50
  end
53
51
 
@@ -66,16 +64,6 @@ module OMQ
66
64
  enqueue_round_robin(parts)
67
65
  end
68
66
 
69
-
70
- # Stops all background tasks.
71
- #
72
- # @return [void]
73
- #
74
- def stop
75
- @tasks.each(&:stop)
76
- @tasks.clear
77
- end
78
-
79
67
  end
80
68
  end
81
69
  end
@@ -0,0 +1,94 @@
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
+ # @return [Async::LimitedQueue]
12
+ #
13
+ attr_reader :recv_queue
14
+
15
+
16
+ # @param engine [Engine]
17
+ #
18
+ def initialize(engine)
19
+ @engine = engine
20
+ @connections = []
21
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
22
+ @groups = Set.new
23
+ end
24
+
25
+
26
+ # Dequeues the next received message. Blocks until one is available.
27
+ #
28
+ # @return [Array<String>, nil]
29
+ #
30
+ def dequeue_recv
31
+ @recv_queue.dequeue
32
+ end
33
+
34
+
35
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel.
36
+ #
37
+ # @return [void]
38
+ #
39
+ def unblock_recv
40
+ @recv_queue.enqueue(nil)
41
+ end
42
+
43
+
44
+ # @param connection [Connection]
45
+ #
46
+ def connection_added(connection)
47
+ @connections << connection
48
+ @groups.each do |group|
49
+ connection.send_command(Protocol::ZMTP::Codec::Command.join(group))
50
+ end
51
+ @engine.start_recv_pump(connection, @recv_queue)
52
+ end
53
+
54
+
55
+ # @param connection [Connection]
56
+ #
57
+ def connection_removed(connection)
58
+ @connections.delete(connection)
59
+ end
60
+
61
+
62
+ # DISH is read-only.
63
+ #
64
+ def enqueue(_parts)
65
+ raise "DISH sockets cannot send"
66
+ end
67
+
68
+
69
+ # Joins a group.
70
+ #
71
+ # @param group [String]
72
+ #
73
+ def join(group)
74
+ @groups << group
75
+ @connections.each do |conn|
76
+ conn.send_command(Protocol::ZMTP::Codec::Command.join(group))
77
+ end
78
+ end
79
+
80
+
81
+ # Leaves a group.
82
+ #
83
+ # @param group [String]
84
+ #
85
+ def leave(group)
86
+ @groups.delete(group)
87
+ @connections.each do |conn|
88
+ conn.send_command(Protocol::ZMTP::Codec::Command.leave(group))
89
+ end
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -41,7 +41,6 @@ module OMQ
41
41
  @subscriptions = {} # connection => Set of prefixes
42
42
  @subscribe_all = Set.new # connections subscribed to "" (match-all fast path)
43
43
  @conn_queues = {} # connection => per-connection send queue
44
- @conn_send_tasks = {} # connection => send pump task
45
44
  @conflate = engine.options.conflate
46
45
  @subscriber_joined = Async::Promise.new
47
46
  @latest = {} if @conflate
@@ -96,7 +95,8 @@ module OMQ
96
95
  end
97
96
 
98
97
 
99
- # Stops the per-connection send pump and removes the queue.
98
+ # Removes the per-connection send queue. The pump itself is torn
99
+ # down by the per-connection lifecycle barrier.
100
100
  # Call from #connection_removed.
101
101
  #
102
102
  # @param conn [Connection]
@@ -104,7 +104,6 @@ module OMQ
104
104
  def remove_fan_out_send_connection(conn)
105
105
  @subscribe_all.delete(conn)
106
106
  @conn_queues.delete(conn)
107
- @conn_send_tasks.delete(conn)&.stop
108
107
  end
109
108
 
110
109
 
@@ -130,7 +129,7 @@ module OMQ
130
129
 
131
130
 
132
131
  def start_subscription_listener(conn)
133
- @tasks << @engine.spawn_conn_pump_task(conn, annotation: "subscription listener") do
132
+ @engine.spawn_conn_pump_task(conn, annotation: "subscription listener") do
134
133
  loop do
135
134
  frame = conn.read_frame
136
135
 
@@ -160,13 +159,10 @@ module OMQ
160
159
  use_wire = conn.respond_to?(:write_wire) && !conn.encrypted?
161
160
 
162
161
  if @conflate
163
- task = start_conn_send_pump_conflate(conn, q)
162
+ start_conn_send_pump_conflate(conn, q)
164
163
  else
165
- task = start_conn_send_pump_normal(conn, q, use_wire)
164
+ start_conn_send_pump_normal(conn, q, use_wire)
166
165
  end
167
-
168
- @conn_send_tasks[conn] = task
169
- @tasks << task
170
166
  end
171
167
 
172
168
 
@@ -181,14 +177,16 @@ module OMQ
181
177
  #
182
178
  def start_conn_send_pump_normal(conn, q, use_wire)
183
179
  @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
180
+ batch = []
181
+
184
182
  loop do
185
- batch = [q.dequeue]
186
- Routing.drain_send_queue(q, batch)
183
+ Routing.dequeue_batch(q, batch)
187
184
 
188
185
  if write_matching_batch(conn, batch, use_wire)
189
186
  conn.flush
190
187
  batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
191
188
  end
189
+ batch.clear
192
190
  end
193
191
  end
194
192
  end
@@ -227,14 +225,17 @@ module OMQ
227
225
  #
228
226
  def start_conn_send_pump_conflate(conn, q)
229
227
  @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
228
+ batch = []
229
+
230
230
  loop do
231
- batch = [q.dequeue]
232
- Routing.drain_send_queue(q, batch)
231
+ Routing.dequeue_batch(q, batch)
233
232
 
234
233
  # Keep only the latest message that matches the subscription.
235
234
  latest = batch.reverse.find do |parts|
236
235
  subscribed?(conn, parts.first || EMPTY_BINARY)
237
236
  end
237
+
238
+ batch.clear
238
239
  next unless latest
239
240
 
240
241
  conn.write_message(latest)
@@ -0,0 +1,60 @@
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
+ # @return [Async::LimitedQueue]
9
+ #
10
+ attr_reader :recv_queue
11
+
12
+
13
+ # @param engine [Engine]
14
+ #
15
+ def initialize(engine)
16
+ @engine = engine
17
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
18
+ end
19
+
20
+
21
+ # Dequeues the next received message. Blocks until one is available.
22
+ #
23
+ # @return [Array<String>, nil]
24
+ #
25
+ def dequeue_recv
26
+ @recv_queue.dequeue
27
+ end
28
+
29
+
30
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel.
31
+ #
32
+ # @return [void]
33
+ #
34
+ def unblock_recv
35
+ @recv_queue.enqueue(nil)
36
+ end
37
+
38
+
39
+ # @param connection [Connection]
40
+ #
41
+ def connection_added(connection)
42
+ @engine.start_recv_pump(connection, @recv_queue)
43
+ end
44
+
45
+
46
+ # @param connection [Connection]
47
+ #
48
+ def connection_removed(connection)
49
+ end
50
+
51
+
52
+ # GATHER is read-only.
53
+ #
54
+ def enqueue(_parts)
55
+ raise "GATHER sockets cannot send"
56
+ end
57
+
58
+ end
59
+ end
60
+ end