omq 0.10.0 → 0.12.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +157 -0
  3. data/README.md +31 -4
  4. data/lib/omq/drop_queue.rb +54 -0
  5. data/lib/omq/engine.rb +103 -61
  6. data/lib/omq/monitor_event.rb +16 -0
  7. data/lib/omq/options.rb +6 -2
  8. data/lib/omq/pair.rb +2 -2
  9. data/lib/omq/pub_sub.rb +13 -12
  10. data/lib/omq/push_pull.rb +4 -4
  11. data/lib/omq/queue_interface.rb +73 -0
  12. data/lib/omq/readable.rb +2 -0
  13. data/lib/omq/req_rep.rb +4 -4
  14. data/lib/omq/router_dealer.rb +4 -4
  15. data/lib/omq/routing/dealer.rb +1 -1
  16. data/lib/omq/routing/fan_out.rb +26 -5
  17. data/lib/omq/routing/pair.rb +2 -2
  18. data/lib/omq/routing/pull.rb +1 -1
  19. data/lib/omq/routing/push.rb +2 -0
  20. data/lib/omq/routing/rep.rb +2 -2
  21. data/lib/omq/routing/req.rb +8 -3
  22. data/lib/omq/routing/round_robin.rb +4 -12
  23. data/lib/omq/routing/router.rb +2 -2
  24. data/lib/omq/routing/sub.rb +1 -2
  25. data/lib/omq/routing/xpub.rb +1 -1
  26. data/lib/omq/routing/xsub.rb +2 -2
  27. data/lib/omq/routing.rb +41 -11
  28. data/lib/omq/socket.rb +49 -2
  29. data/lib/omq/transport/inproc.rb +25 -7
  30. data/lib/omq/transport/ipc.rb +16 -4
  31. data/lib/omq/transport/tcp.rb +31 -8
  32. data/lib/omq/version.rb +1 -1
  33. data/lib/omq/writable.rb +1 -0
  34. data/lib/omq.rb +6 -16
  35. metadata +4 -15
  36. data/lib/omq/channel.rb +0 -14
  37. data/lib/omq/client_server.rb +0 -37
  38. data/lib/omq/peer.rb +0 -26
  39. data/lib/omq/radio_dish.rb +0 -74
  40. data/lib/omq/routing/channel.rb +0 -83
  41. data/lib/omq/routing/client.rb +0 -56
  42. data/lib/omq/routing/dish.rb +0 -78
  43. data/lib/omq/routing/gather.rb +0 -46
  44. data/lib/omq/routing/peer.rb +0 -101
  45. data/lib/omq/routing/radio.rb +0 -140
  46. data/lib/omq/routing/scatter.rb +0 -82
  47. data/lib/omq/routing/server.rb +0 -101
  48. data/lib/omq/scatter_gather.rb +0 -23
  49. data/lib/omq/single_frame.rb +0 -18
@@ -118,11 +118,12 @@ module OMQ
118
118
  "incompatible socket types: #{client_type} cannot connect to #{server_type}"
119
119
  end
120
120
 
121
- # Only PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
122
- # over inproc. All other types use only the direct recv queue
123
- # bypass for data, so no internal queues are needed.
121
+ # PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
122
+ # over inproc. QoS >= 1 needs command queues for ACK/NACK.
124
123
  needs_commands = COMMAND_TYPES.include?(client_type) ||
125
- COMMAND_TYPES.include?(server_type)
124
+ COMMAND_TYPES.include?(server_type) ||
125
+ client_engine.options.qos >= 1 ||
126
+ server_engine.options.qos >= 1
126
127
 
127
128
  if needs_commands
128
129
  a_to_b = Async::Queue.new
@@ -301,13 +302,30 @@ module OMQ
301
302
 
302
303
  # Receives a multi-frame message.
303
304
  #
305
+ # When a block is given, command items ([:command, cmd]) are
306
+ # yielded as command frames — matching the Protocol::ZMTP::Connection
307
+ # interface. Without a block, commands are silently skipped if
308
+ # the pipe has command queues.
309
+ #
304
310
  # @return [Array<String>]
305
311
  # @raise [EOFError] if closed
306
312
  #
307
313
  def receive_message
308
- msg = @receive_queue.dequeue
309
- raise EOFError, "connection closed" if msg.nil?
310
- msg
314
+ loop do
315
+ item = @receive_queue.dequeue
316
+ raise EOFError, "connection closed" if item.nil?
317
+
318
+ if item.is_a?(Array) && item.first == :command
319
+ if block_given?
320
+ cmd = item[1]
321
+ frame = Protocol::ZMTP::Codec::Frame.new(cmd.to_body, command: true)
322
+ yield frame
323
+ end
324
+ next
325
+ end
326
+
327
+ return item
328
+ end
311
329
  end
312
330
 
313
331
 
@@ -92,12 +92,24 @@ module OMQ
92
92
  end
93
93
 
94
94
 
95
- # Registers the accept loop task owned by the engine.
95
+ # Spawns an accept loop task under +parent_task+.
96
+ # Yields an IO::Stream-wrapped client socket for each accepted connection.
96
97
  #
97
- # @param task [Async::Task]
98
+ # @param parent_task [Async::Task]
99
+ # @yieldparam io [IO::Stream::Buffered]
98
100
  #
99
- def accept_task=(task)
100
- @task = task
101
+ def start_accept_loops(parent_task, &on_accepted)
102
+ @task = parent_task.async(transient: true, annotation: "ipc accept #{@endpoint}") do
103
+ loop do
104
+ client = @server.accept
105
+ Async::Task.current.defer_stop { on_accepted.call(IO::Stream::Buffered.wrap(client)) }
106
+ end
107
+ rescue Async::Stop
108
+ rescue IOError
109
+ # server closed
110
+ ensure
111
+ @server.close rescue nil
112
+ end
101
113
  end
102
114
 
103
115
 
@@ -17,7 +17,7 @@ module OMQ
17
17
  # @return [Listener]
18
18
  #
19
19
  def bind(endpoint, engine)
20
- host, port = parse_endpoint(endpoint)
20
+ host, port = self.parse_endpoint(endpoint)
21
21
  host = "0.0.0.0" if host == "*"
22
22
 
23
23
  addrs = Addrinfo.getaddrinfo(host, port, nil, :STREAM, nil, ::Socket::AI_PASSIVE)
@@ -43,14 +43,23 @@ module OMQ
43
43
  # @param engine [Engine]
44
44
  # @return [void]
45
45
  #
46
+ # Validates that the endpoint's host can be resolved.
47
+ #
48
+ # @param endpoint [String]
49
+ # @return [void]
50
+ #
51
+ def validate_endpoint!(endpoint)
52
+ host, _port = parse_endpoint(endpoint)
53
+ Addrinfo.getaddrinfo(host, nil, nil, :STREAM) if host
54
+ end
55
+
56
+
46
57
  def connect(endpoint, engine)
47
- host, port = parse_endpoint(endpoint)
58
+ host, port = self.parse_endpoint(endpoint)
48
59
  sock = TCPSocket.new(host, port)
49
60
  engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
50
61
  end
51
62
 
52
- private
53
-
54
63
  # Parses a TCP endpoint URI into host and port.
55
64
  #
56
65
  # @param endpoint [String]
@@ -90,12 +99,26 @@ module OMQ
90
99
  end
91
100
 
92
101
 
93
- # Registers accept loop tasks owned by the engine.
102
+ # Spawns accept loop tasks under +parent_task+.
103
+ # Yields an IO::Stream-wrapped client socket for each accepted connection.
94
104
  #
95
- # @param tasks [Array<Async::Task>]
105
+ # @param parent_task [Async::Task]
106
+ # @yieldparam io [IO::Stream::Buffered]
96
107
  #
97
- def accept_tasks=(tasks)
98
- @tasks = tasks
108
+ def start_accept_loops(parent_task, &on_accepted)
109
+ @tasks = @servers.map do |server|
110
+ parent_task.async(transient: true, annotation: "tcp accept #{@endpoint}") do
111
+ loop do
112
+ client = server.accept
113
+ Async::Task.current.defer_stop { on_accepted.call(IO::Stream::Buffered.wrap(client)) }
114
+ end
115
+ rescue Async::Stop
116
+ rescue IOError
117
+ # server closed
118
+ ensure
119
+ server.close rescue nil
120
+ end
121
+ end
99
122
  end
100
123
 
101
124
 
data/lib/omq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.10.0"
4
+ VERSION = "0.12.0"
5
5
  end
data/lib/omq/writable.rb CHANGED
@@ -6,6 +6,7 @@ module OMQ
6
6
  # Pure Ruby Writable mixin. Enqueues messages to the engine's send path.
7
7
  #
8
8
  module Writable
9
+ include QueueWritable
9
10
  # Sends a message.
10
11
  #
11
12
  # @param message [String, Array<String>] message parts
data/lib/omq.rb CHANGED
@@ -10,6 +10,7 @@ require "protocol/zmtp"
10
10
  require "io/stream"
11
11
 
12
12
  require_relative "omq/version"
13
+ require_relative "omq/monitor_event"
13
14
 
14
15
  module OMQ
15
16
  # Raised when an internal pump task crashes unexpectedly.
@@ -18,6 +19,8 @@ module OMQ
18
19
  class SocketDeadError < RuntimeError; end
19
20
 
20
21
  # Errors raised when a peer disconnects or resets the connection.
22
+ # Not frozen at load time — transport plugins append to this before
23
+ # the first bind/connect, which freezes both arrays.
21
24
  CONNECTION_LOST = [
22
25
  EOFError,
23
26
  IOError,
@@ -26,7 +29,7 @@ module OMQ
26
29
  Errno::ECONNABORTED,
27
30
  Errno::ENOTCONN,
28
31
  IO::Stream::ConnectionResetError,
29
- ].freeze
32
+ ]
30
33
 
31
34
  # Errors raised when a peer cannot be reached.
32
35
  CONNECTION_FAILED = [
@@ -36,7 +39,7 @@ module OMQ
36
39
  Errno::EHOSTUNREACH,
37
40
  Errno::ENETUNREACH,
38
41
  Socket::ResolutionError,
39
- ].freeze
42
+ ]
40
43
  end
41
44
 
42
45
  # Transport
@@ -61,16 +64,8 @@ require_relative "omq/routing/xpub"
61
64
  require_relative "omq/routing/xsub"
62
65
  require_relative "omq/routing/push"
63
66
  require_relative "omq/routing/pull"
64
- require_relative "omq/routing/scatter"
65
- require_relative "omq/routing/gather"
66
- require_relative "omq/routing/channel"
67
- require_relative "omq/routing/client"
68
- require_relative "omq/routing/server"
69
- require_relative "omq/routing/radio"
70
- require_relative "omq/routing/dish"
71
- require_relative "omq/routing/peer"
72
- require_relative "omq/single_frame"
73
67
  require_relative "omq/engine"
68
+ require_relative "omq/queue_interface"
74
69
  require_relative "omq/readable"
75
70
  require_relative "omq/writable"
76
71
 
@@ -81,11 +76,6 @@ require_relative "omq/router_dealer"
81
76
  require_relative "omq/pub_sub"
82
77
  require_relative "omq/push_pull"
83
78
  require_relative "omq/pair"
84
- require_relative "omq/scatter_gather"
85
- require_relative "omq/channel"
86
- require_relative "omq/client_server"
87
- require_relative "omq/radio_dish"
88
- require_relative "omq/peer"
89
79
 
90
80
  # For the purists.
91
81
  ØMQ = OMQ
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -65,43 +65,32 @@ files:
65
65
  - LICENSE
66
66
  - README.md
67
67
  - lib/omq.rb
68
- - lib/omq/channel.rb
69
- - lib/omq/client_server.rb
68
+ - lib/omq/drop_queue.rb
70
69
  - lib/omq/engine.rb
70
+ - lib/omq/monitor_event.rb
71
71
  - lib/omq/options.rb
72
72
  - lib/omq/pair.rb
73
- - lib/omq/peer.rb
74
73
  - lib/omq/pub_sub.rb
75
74
  - lib/omq/push_pull.rb
76
- - lib/omq/radio_dish.rb
75
+ - lib/omq/queue_interface.rb
77
76
  - lib/omq/reactor.rb
78
77
  - lib/omq/readable.rb
79
78
  - lib/omq/req_rep.rb
80
79
  - lib/omq/router_dealer.rb
81
80
  - lib/omq/routing.rb
82
- - lib/omq/routing/channel.rb
83
- - lib/omq/routing/client.rb
84
81
  - lib/omq/routing/dealer.rb
85
- - lib/omq/routing/dish.rb
86
82
  - lib/omq/routing/fan_out.rb
87
- - lib/omq/routing/gather.rb
88
83
  - lib/omq/routing/pair.rb
89
- - lib/omq/routing/peer.rb
90
84
  - lib/omq/routing/pub.rb
91
85
  - lib/omq/routing/pull.rb
92
86
  - lib/omq/routing/push.rb
93
- - lib/omq/routing/radio.rb
94
87
  - lib/omq/routing/rep.rb
95
88
  - lib/omq/routing/req.rb
96
89
  - lib/omq/routing/round_robin.rb
97
90
  - lib/omq/routing/router.rb
98
- - lib/omq/routing/scatter.rb
99
- - lib/omq/routing/server.rb
100
91
  - lib/omq/routing/sub.rb
101
92
  - lib/omq/routing/xpub.rb
102
93
  - lib/omq/routing/xsub.rb
103
- - lib/omq/scatter_gather.rb
104
- - lib/omq/single_frame.rb
105
94
  - lib/omq/socket.rb
106
95
  - lib/omq/transport/inproc.rb
107
96
  - lib/omq/transport/ipc.rb
data/lib/omq/channel.rb DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- class CHANNEL < Socket
5
- include Readable
6
- include Writable
7
- include SingleFrame
8
-
9
- def initialize(endpoints = nil, linger: 0)
10
- _init_engine(:CHANNEL, linger: linger)
11
- _attach(endpoints, default: :connect)
12
- end
13
- end
14
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- class CLIENT < Socket
5
- include Readable
6
- include Writable
7
- include SingleFrame
8
-
9
- def initialize(endpoints = nil, linger: 0)
10
- _init_engine(:CLIENT, linger: linger)
11
- _attach(endpoints, default: :connect)
12
- end
13
- end
14
-
15
- class SERVER < Socket
16
- include Readable
17
- include Writable
18
- include SingleFrame
19
-
20
- def initialize(endpoints = nil, linger: 0)
21
- _init_engine(:SERVER, linger: linger)
22
- _attach(endpoints, default: :bind)
23
- end
24
-
25
- # Sends a message to a specific peer by routing ID.
26
- #
27
- # @param routing_id [String] 4-byte routing ID
28
- # @param message [String] message body
29
- # @return [self]
30
- #
31
- def send_to(routing_id, message)
32
- parts = [routing_id.b.freeze, message.b.freeze]
33
- with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
34
- self
35
- end
36
- end
37
- end
data/lib/omq/peer.rb DELETED
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- class PEER < Socket
5
- include Readable
6
- include Writable
7
- include SingleFrame
8
-
9
- def initialize(endpoints = nil, linger: 0)
10
- _init_engine(:PEER, linger: linger)
11
- _attach(endpoints, default: :connect)
12
- end
13
-
14
- # Sends a message to a specific peer by routing ID.
15
- #
16
- # @param routing_id [String] 4-byte routing ID
17
- # @param message [String] message body
18
- # @return [self]
19
- #
20
- def send_to(routing_id, message)
21
- parts = [routing_id.b.freeze, message.b.freeze]
22
- with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
23
- self
24
- end
25
- end
26
- end
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OMQ
4
- class RADIO < Socket
5
- include Writable
6
-
7
- def initialize(endpoints = nil, linger: 0, conflate: false)
8
- _init_engine(:RADIO, linger: linger, conflate: conflate)
9
- _attach(endpoints, default: :bind)
10
- end
11
-
12
- # Publishes a message to a group.
13
- #
14
- # @param group [String] group name
15
- # @param body [String] message body
16
- # @return [self]
17
- #
18
- def publish(group, body)
19
- with_timeout(@options.write_timeout) do
20
- @engine.enqueue_send([group.b.freeze, body.b.freeze])
21
- end
22
- self
23
- end
24
-
25
- # Sends a message to a group.
26
- #
27
- # @param message [String] message body (requires group: kwarg)
28
- # @param group [String] group name
29
- # @return [self]
30
- #
31
- def send(message, group: nil)
32
- raise ArgumentError, "RADIO requires a group (use group: kwarg, publish, or << [group, body])" unless group
33
- publish(group, message)
34
- end
35
-
36
- # Sends a message to a group via [group, body] array.
37
- #
38
- # @param message [Array<String>] [group, body]
39
- # @return [self]
40
- #
41
- def <<(message)
42
- raise ArgumentError, "RADIO requires [group, body] array" unless message.is_a?(Array) && message.size == 2
43
- publish(message[0], message[1])
44
- end
45
- end
46
-
47
- class DISH < Socket
48
- include Readable
49
-
50
- def initialize(endpoints = nil, linger: 0, group: nil)
51
- _init_engine(:DISH, linger: linger)
52
- _attach(endpoints, default: :connect)
53
- join(group) if group
54
- end
55
-
56
- # Joins a group.
57
- #
58
- # @param group [String]
59
- # @return [void]
60
- #
61
- def join(group)
62
- @engine.routing.join(group)
63
- end
64
-
65
- # Leaves a group.
66
- #
67
- # @param group [String]
68
- # @return [void]
69
- #
70
- def leave(group)
71
- @engine.routing.leave(group)
72
- end
73
- end
74
- end
@@ -1,83 +0,0 @@
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
-
9
- # @param engine [Engine]
10
- #
11
- def initialize(engine)
12
- @engine = engine
13
- @connection = nil
14
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
15
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
16
- @tasks = []
17
- @send_pump_idle = true
18
- end
19
-
20
- # @return [Async::LimitedQueue]
21
- #
22
- attr_reader :recv_queue, :send_queue
23
-
24
- # @param connection [Connection]
25
- # @raise [RuntimeError] if a connection already exists
26
- #
27
- def connection_added(connection)
28
- raise "CHANNEL allows only one peer" if @connection
29
- @connection = connection
30
- task = @engine.start_recv_pump(connection, @recv_queue)
31
- @tasks << task if task
32
- start_send_pump(connection) unless connection.is_a?(Transport::Inproc::DirectPipe)
33
- end
34
-
35
- # @param connection [Connection]
36
- #
37
- def connection_removed(connection)
38
- if @connection == connection
39
- @connection = nil
40
- @send_pump&.stop
41
- @send_pump = nil
42
- end
43
- end
44
-
45
- # @param parts [Array<String>]
46
- #
47
- def enqueue(parts)
48
- conn = @connection
49
- if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
50
- conn.send_message(parts)
51
- else
52
- @send_queue.enqueue(parts)
53
- end
54
- end
55
-
56
- #
57
- def stop
58
- @tasks.each(&:stop)
59
- @tasks.clear
60
- end
61
-
62
- def send_pump_idle? = @send_pump_idle
63
-
64
- private
65
-
66
- def start_send_pump(conn)
67
- @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
68
- loop do
69
- @send_pump_idle = true
70
- batch = [@send_queue.dequeue]
71
- @send_pump_idle = false
72
- Routing.drain_send_queue(@send_queue, batch)
73
- batch.each { |parts| conn.write_message(parts) }
74
- conn.flush
75
- end
76
- rescue *CONNECTION_LOST
77
- @engine.connection_lost(conn)
78
- end
79
- @tasks << @send_pump
80
- end
81
- end
82
- end
83
- end
@@ -1,56 +0,0 @@
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
- # @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
- end
55
- end
56
- end
@@ -1,78 +0,0 @@
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