omq 0.12.0 → 0.14.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/README.md +27 -0
  4. data/lib/omq/drop_queue.rb +3 -0
  5. data/lib/omq/engine/connection_setup.rb +70 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/maintenance.rb +35 -0
  8. data/lib/omq/engine/reconnect.rb +82 -0
  9. data/lib/omq/engine/recv_pump.rb +119 -0
  10. data/lib/omq/engine.rb +139 -304
  11. data/lib/omq/options.rb +44 -0
  12. data/lib/omq/pair.rb +6 -0
  13. data/lib/omq/pub_sub.rb +25 -0
  14. data/lib/omq/push_pull.rb +17 -0
  15. data/lib/omq/queue_interface.rb +1 -0
  16. data/lib/omq/readable.rb +2 -0
  17. data/lib/omq/req_rep.rb +13 -0
  18. data/lib/omq/router_dealer.rb +12 -0
  19. data/lib/omq/routing/conn_send_pump.rb +36 -0
  20. data/lib/omq/routing/dealer.rb +15 -10
  21. data/lib/omq/routing/fair_queue.rb +172 -0
  22. data/lib/omq/routing/fair_recv.rb +27 -0
  23. data/lib/omq/routing/fan_out.rb +127 -74
  24. data/lib/omq/routing/pair.rb +47 -20
  25. data/lib/omq/routing/pub.rb +12 -6
  26. data/lib/omq/routing/pull.rb +12 -4
  27. data/lib/omq/routing/push.rb +3 -12
  28. data/lib/omq/routing/rep.rb +41 -51
  29. data/lib/omq/routing/req.rb +15 -10
  30. data/lib/omq/routing/round_robin.rb +82 -63
  31. data/lib/omq/routing/router.rb +32 -48
  32. data/lib/omq/routing/sub.rb +18 -5
  33. data/lib/omq/routing/xpub.rb +15 -3
  34. data/lib/omq/routing/xsub.rb +53 -27
  35. data/lib/omq/routing.rb +29 -11
  36. data/lib/omq/socket.rb +25 -7
  37. data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
  38. data/lib/omq/transport/inproc.rb +41 -217
  39. data/lib/omq/transport/ipc.rb +7 -1
  40. data/lib/omq/transport/tcp.rb +12 -7
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +2 -0
  43. data/lib/omq.rb +4 -1
  44. metadata +14 -5
@@ -8,43 +8,45 @@ module OMQ
8
8
  #
9
9
  class Req
10
10
  include RoundRobin
11
+ include FairRecv
11
12
 
12
13
  # @param engine [Engine]
13
14
  #
14
15
  def initialize(engine)
15
16
  @engine = engine
16
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
17
+ @recv_queue = FairQueue.new
17
18
  @tasks = []
18
19
  @state = :ready # :ready or :waiting_reply
19
20
  init_round_robin(engine)
20
21
  end
21
22
 
22
- # @return [Async::LimitedQueue]
23
+
24
+ # @return [FairQueue]
23
25
  #
24
- attr_reader :recv_queue, :send_queue
26
+ attr_reader :recv_queue
25
27
 
26
28
 
27
29
  # @param connection [Connection]
28
30
  #
29
31
  def connection_added(connection)
30
32
  @connections << connection
31
- signal_connection_available
32
- update_direct_pipe
33
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
33
+ add_fair_recv_connection(connection) do |msg|
34
34
  @state = :ready
35
35
  msg.first&.empty? ? msg[1..] : msg
36
36
  end
37
- @tasks << task if task
38
- start_send_pump unless @send_pump_started
37
+ add_round_robin_send_connection(connection)
39
38
  end
40
39
 
40
+
41
41
  # @param connection [Connection]
42
42
  #
43
43
  def connection_removed(connection)
44
44
  @connections.delete(connection)
45
- update_direct_pipe
45
+ @recv_queue.remove_queue(connection)
46
+ remove_round_robin_send_connection(connection)
46
47
  end
47
48
 
49
+
48
50
  # @param parts [Array<String>]
49
51
  #
50
52
  def enqueue(parts)
@@ -53,6 +55,10 @@ module OMQ
53
55
  enqueue_round_robin(parts)
54
56
  end
55
57
 
58
+
59
+ # Stops all background tasks.
60
+ #
61
+ # @return [void]
56
62
  #
57
63
  def stop
58
64
  @tasks.each(&:stop)
@@ -64,7 +70,6 @@ module OMQ
64
70
  # REQ prepends empty delimiter frame on the wire.
65
71
  #
66
72
  def transform_send(parts) = [EMPTY_BINARY, *parts]
67
-
68
73
  end
69
74
  end
70
75
  end
@@ -8,16 +8,22 @@ module OMQ
8
8
  # for the first connection, Array#cycle handles round-robin,
9
9
  # and a new Promise is created when all connections drop.
10
10
  #
11
+ # Each connected peer gets its own bounded send queue and a
12
+ # dedicated send pump fiber, ensuring HWM is enforced per peer.
13
+ #
11
14
  # Including classes must call `init_round_robin(engine)` from
12
15
  # their #initialize.
13
16
  #
14
17
  module RoundRobin
15
- # @return [Boolean] true when the send pump is idle (not sending a batch)
16
- def send_pump_idle? = @send_pump_idle
18
+ # @return [Boolean] true when the staging queue and all per-connection
19
+ # send queues are empty
20
+ #
21
+ def send_queues_drained?
22
+ @staging_queue.empty? && @conn_queues.values.all?(&:empty?)
23
+ end
17
24
 
18
25
  private
19
26
 
20
-
21
27
  # Initializes round-robin state for the including class.
22
28
  #
23
29
  # @param engine [Engine]
@@ -26,11 +32,37 @@ module OMQ
26
32
  @connections = []
27
33
  @cycle = @connections.cycle
28
34
  @connection_available = Async::Promise.new
29
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
30
- @send_pump_started = false
31
- @send_pump_idle = true
35
+ @conn_queues = {} # connection => send queue
36
+ @conn_send_tasks = {} # connection => send pump task
32
37
  @direct_pipe = nil
33
- @written = Set.new
38
+ @staging_queue = Routing.build_queue(@engine.options.send_hwm, :block)
39
+ end
40
+
41
+
42
+ # Creates a per-connection send queue and starts its send pump.
43
+ # Call from #connection_added after appending to @connections.
44
+ #
45
+ # @param conn [Connection]
46
+ #
47
+ def add_round_robin_send_connection(conn)
48
+ update_direct_pipe
49
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
50
+ @conn_queues[conn] = q
51
+ drain_staging_to(q)
52
+ start_conn_send_pump(conn, q)
53
+ signal_connection_available
54
+ end
55
+
56
+
57
+ # Stops the per-connection send pump and removes the queue.
58
+ # Call from #connection_removed.
59
+ #
60
+ # @param conn [Connection]
61
+ #
62
+ def remove_round_robin_send_connection(conn)
63
+ update_direct_pipe
64
+ @conn_queues.delete(conn)
65
+ @conn_send_tasks.delete(conn)&.stop
34
66
  end
35
67
 
36
68
 
@@ -56,15 +88,33 @@ module OMQ
56
88
  end
57
89
 
58
90
 
59
- # Enqueues directly to the inproc peer's recv queue if possible,
60
- # otherwise falls back to the send queue for the send pump.
91
+ # Enqueues directly to the inproc peer's recv queue if possible.
92
+ # When peers are connected, picks the next one round-robin and
93
+ # enqueues into its per-connection send queue (blocking if full).
94
+ # When no peers are connected yet, buffers in a staging queue
95
+ # (bounded by send_hwm) — drained into the first peer's queue
96
+ # when it connects.
61
97
  #
62
98
  def enqueue_round_robin(parts)
63
99
  pipe = @direct_pipe
64
100
  if pipe&.direct_recv_queue
65
101
  pipe.send_message(transform_send(parts))
102
+ elsif @connections.empty?
103
+ @staging_queue.enqueue(parts)
66
104
  else
67
- @send_queue.enqueue(parts)
105
+ conn = next_connection
106
+ @conn_queues[conn].enqueue(parts)
107
+ end
108
+ end
109
+
110
+
111
+ # Drains the staging queue into the given per-connection queue.
112
+ # Called when the first peer connects, to deliver messages that
113
+ # were enqueued before any connection existed.
114
+ #
115
+ def drain_staging_to(q)
116
+ while (msg = @staging_queue.dequeue(timeout: 0))
117
+ q.enqueue(msg)
68
118
  end
69
119
  end
70
120
 
@@ -93,66 +143,35 @@ module OMQ
93
143
  def transform_send(parts) = parts
94
144
 
95
145
 
96
- def start_send_pump
97
- @send_pump_started = true
98
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
146
+ # Starts a dedicated send pump for one connection.
147
+ # Batches messages for throughput; flushes after each batch.
148
+ # Calls Engine#connection_lost on disconnect so reconnect fires.
149
+ #
150
+ # @param conn [Connection]
151
+ # @param q [Async::LimitedQueue] the connection's send queue
152
+ #
153
+ def start_conn_send_pump(conn, q)
154
+ task = @engine.spawn_pump_task(annotation: "send pump") do
99
155
  loop do
100
- @send_pump_idle = true
101
- batch = [@send_queue.dequeue]
102
- @send_pump_idle = false
103
- Routing.drain_send_queue(@send_queue, batch)
104
-
105
- if batch.size == 1
106
- send_with_retry(batch[0])
107
- else
108
- send_batch(batch)
109
- end
156
+ batch = [q.dequeue]
157
+ Routing.drain_send_queue(q, batch)
158
+ write_batch(conn, batch)
159
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
160
+ @engine.connection_lost(conn)
161
+ break
110
162
  end
111
163
  end
164
+ @conn_send_tasks[conn] = task
165
+ @tasks << task
112
166
  end
113
167
 
114
168
 
115
- # Sends a single message, retrying on a new connection if
116
- # the current one is lost.
117
- #
118
- # @param parts [Array<String>]
119
- #
120
- def send_with_retry(parts)
121
- conn = next_connection
122
- conn.send_message(transform_send(parts))
123
- rescue *CONNECTION_LOST
124
- @engine.connection_lost(conn)
125
- retry
126
- end
127
-
128
-
129
- # Sends a batch of messages, writing without flushing for
130
- # throughput. Falls back to #send_with_retry on failure.
131
- #
132
- # @param batch [Array<Array<String>>]
133
- #
134
- def send_batch(batch)
135
- @written.clear
136
- batch.each_with_index do |parts, i|
137
- conn = next_connection
138
- begin
139
- conn.write_message(transform_send(parts))
140
- @written << conn
141
- rescue *CONNECTION_LOST
142
- @engine.connection_lost(conn)
143
- @written.each do |c|
144
- c.flush
145
- rescue *CONNECTION_LOST
146
- end
147
- @written.clear
148
- send_with_retry(parts)
149
- batch[(i + 1)..].each { |p| send_with_retry(p) }
150
- return
151
- end
152
- end
153
- @written.each do |conn|
169
+ def write_batch(conn, batch)
170
+ if batch.size == 1
171
+ conn.send_message(transform_send(batch[0]))
172
+ else
173
+ batch.each { |parts| conn.write_message(transform_send(parts)) }
154
174
  conn.flush
155
- rescue *CONNECTION_LOST
156
175
  end
157
176
  end
158
177
  end
@@ -11,23 +11,23 @@ module OMQ
11
11
  # routing identity on send.
12
12
  #
13
13
  class Router
14
+ include FairRecv
14
15
  # @param engine [Engine]
15
16
  #
16
17
  def initialize(engine)
17
18
  @engine = engine
18
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
19
+ @recv_queue = FairQueue.new
20
20
  @connections_by_identity = {}
21
21
  @identity_by_connection = {}
22
+ @conn_queues = {} # connection => per-connection send queue
23
+ @conn_send_tasks = {} # connection => send pump task
22
24
  @tasks = []
23
- @send_pump_started = false
24
- @send_pump_idle = true
25
- @written = Set.new
26
25
  end
27
26
 
28
- # @return [Async::LimitedQueue]
27
+
28
+ # @return [FairQueue]
29
29
  #
30
- attr_reader :recv_queue, :send_queue
30
+ attr_reader :recv_queue
31
31
 
32
32
  # @param connection [Connection]
33
33
  #
@@ -37,74 +37,58 @@ module OMQ
37
37
  @connections_by_identity[identity] = connection
38
38
  @identity_by_connection[connection] = identity
39
39
 
40
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
41
- [identity, *msg]
42
- end
43
- @tasks << task if task
40
+ add_fair_recv_connection(connection) { |msg| [identity, *msg] }
44
41
 
45
- start_send_pump unless @send_pump_started
42
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
43
+ @conn_queues[connection] = q
44
+ @conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
46
45
  end
47
46
 
47
+
48
48
  # @param connection [Connection]
49
49
  #
50
50
  def connection_removed(connection)
51
51
  identity = @identity_by_connection.delete(connection)
52
52
  @connections_by_identity.delete(identity) if identity
53
+ @recv_queue.remove_queue(connection)
54
+ @conn_queues.delete(connection)
55
+ @conn_send_tasks.delete(connection)&.stop
53
56
  end
54
57
 
55
- # Enqueues a message for sending.
58
+
59
+ # Enqueues a message for sending. The first frame is the routing identity.
56
60
  #
57
61
  # @param parts [Array<String>]
58
62
  #
59
63
  def enqueue(parts)
64
+ identity = parts.first
60
65
  if @engine.options.router_mandatory?
61
- identity = parts.first
62
66
  unless @connections_by_identity[identity]
63
67
  raise SocketError, "no route to identity #{identity.inspect}"
64
68
  end
65
69
  end
66
- @send_queue.enqueue(parts)
70
+ conn = @connections_by_identity[identity]
71
+ return unless conn # silently drop if peer disconnected
72
+ @conn_queues[conn]&.enqueue(parts[1..])
67
73
  end
68
74
 
75
+
76
+ # Stops all background tasks.
77
+ #
78
+ # @return [void]
79
+ #
69
80
  def stop
70
81
  @tasks.each(&:stop)
71
82
  @tasks.clear
72
83
  end
73
84
 
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
85
+
86
+ # @return [Boolean] true when all per-connection send queues are empty
87
+ #
88
+ def send_queues_drained?
89
+ @conn_queues.values.all?(&:empty?)
107
90
  end
91
+
108
92
  end
109
93
  end
110
94
  end
@@ -12,13 +12,14 @@ module OMQ
12
12
  #
13
13
  def initialize(engine)
14
14
  @engine = engine
15
- @connections = []
16
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, engine.options.on_mute)
15
+ @connections = Set.new
16
+ @recv_queue = FairQueue.new
17
17
  @subscriptions = Set.new
18
18
  @tasks = []
19
19
  end
20
20
 
21
- # @return [Async::LimitedQueue]
21
+
22
+ # @return [FairQueue]
22
23
  #
23
24
  attr_reader :recv_queue
24
25
 
@@ -29,22 +30,29 @@ module OMQ
29
30
  @subscriptions.each do |prefix|
30
31
  connection.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
31
32
  end
32
- task = @engine.start_recv_pump(connection, @recv_queue)
33
+ conn_q = Routing.build_queue(@engine.options.recv_hwm, @engine.options.on_mute)
34
+ signaling = SignalingQueue.new(conn_q, @recv_queue)
35
+ @recv_queue.add_queue(connection, conn_q)
36
+ task = @engine.start_recv_pump(connection, signaling)
33
37
  @tasks << task if task
34
38
  end
35
39
 
40
+
36
41
  # @param connection [Connection]
37
42
  #
38
43
  def connection_removed(connection)
39
44
  @connections.delete(connection)
45
+ @recv_queue.remove_queue(connection)
40
46
  end
41
47
 
48
+
42
49
  # SUB is read-only.
43
50
  #
44
51
  def enqueue(_parts)
45
52
  raise "SUB sockets cannot send"
46
53
  end
47
54
 
55
+
48
56
  # Subscribes to a topic prefix.
49
57
  #
50
58
  # @param prefix [String]
@@ -56,6 +64,7 @@ module OMQ
56
64
  end
57
65
  end
58
66
 
67
+
59
68
  # Unsubscribes from a topic prefix.
60
69
  #
61
70
  # @param prefix [String]
@@ -67,11 +76,15 @@ module OMQ
67
76
  end
68
77
  end
69
78
 
79
+
80
+ # Stops all background tasks.
81
+ #
82
+ # @return [void]
83
+ #
70
84
  def stop
71
85
  @tasks.each(&:stop)
72
86
  @tasks.clear
73
87
  end
74
-
75
88
  end
76
89
  end
77
90
  end
@@ -8,6 +8,9 @@ module OMQ
8
8
  # the application as data frames: \x01 + prefix for subscribe,
9
9
  # \x00 + prefix for unsubscribe.
10
10
  #
11
+ # The recv_queue is a simple bounded queue (not a FairQueue) because
12
+ # messages come from subscription commands, not from peer data pumps.
13
+ #
11
14
  class XPub
12
15
  include FanOut
13
16
 
@@ -20,9 +23,10 @@ module OMQ
20
23
  init_fan_out(engine)
21
24
  end
22
25
 
26
+
23
27
  # @return [Async::LimitedQueue]
24
28
  #
25
- attr_reader :recv_queue, :send_queue
29
+ attr_reader :recv_queue
26
30
 
27
31
  # @param connection [Connection]
28
32
  #
@@ -30,22 +34,29 @@ module OMQ
30
34
  @connections << connection
31
35
  @subscriptions[connection] = Set.new
32
36
  start_subscription_listener(connection)
33
- start_send_pump unless @send_pump_started
37
+ add_fan_out_send_connection(connection)
34
38
  end
35
39
 
40
+
36
41
  # @param connection [Connection]
37
42
  #
38
43
  def connection_removed(connection)
39
44
  @connections.delete(connection)
40
45
  @subscriptions.delete(connection)
46
+ remove_fan_out_send_connection(connection)
41
47
  end
42
48
 
49
+
43
50
  # @param parts [Array<String>]
44
51
  #
45
52
  def enqueue(parts)
46
- @send_queue.enqueue(parts)
53
+ fan_out_enqueue(parts)
47
54
  end
48
55
 
56
+
57
+ # Stops all background tasks.
58
+ #
59
+ # @return [void]
49
60
  #
50
61
  def stop
51
62
  @tasks.each(&:stop)
@@ -61,6 +72,7 @@ module OMQ
61
72
  @recv_queue.enqueue(["\x01#{prefix}".b])
62
73
  end
63
74
 
75
+
64
76
  # Expose unsubscription to application as data message.
65
77
  #
66
78
  def on_cancel(conn, prefix)
@@ -5,78 +5,104 @@ module OMQ
5
5
  # XSUB socket routing: like SUB but subscriptions sent as data messages.
6
6
  #
7
7
  # Subscriptions are sent as data frames: \x01 + prefix for subscribe,
8
- # \x00 + prefix for unsubscribe.
8
+ # \x00 + prefix for unsubscribe. Each connected PUB gets its own send
9
+ # queue so subscription commands are delivered independently per peer.
9
10
  #
10
11
  class XSub
11
12
 
12
13
  # @param engine [Engine]
13
14
  #
14
15
  def initialize(engine)
15
- @engine = engine
16
- @connections = []
17
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, engine.options.on_mute)
18
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
19
- @tasks = []
20
- @send_pump_started = false
21
- @send_pump_idle = true
16
+ @engine = engine
17
+ @connections = Set.new
18
+ @recv_queue = FairQueue.new
19
+ @conn_queues = {} # connection => per-connection send queue
20
+ @conn_send_tasks = {} # connection => send pump task
21
+ @tasks = []
22
22
  end
23
23
 
24
- # @return [Async::LimitedQueue]
24
+
25
+ # @return [FairQueue]
25
26
  #
26
- attr_reader :recv_queue, :send_queue
27
+ attr_reader :recv_queue
27
28
 
28
29
  # @param connection [Connection]
29
30
  #
30
31
  def connection_added(connection)
31
32
  @connections << connection
32
- task = @engine.start_recv_pump(connection, @recv_queue)
33
+
34
+ conn_q = Routing.build_queue(@engine.options.recv_hwm, @engine.options.on_mute)
35
+ signaling = SignalingQueue.new(conn_q, @recv_queue)
36
+ @recv_queue.add_queue(connection, conn_q)
37
+ task = @engine.start_recv_pump(connection, signaling)
33
38
  @tasks << task if task
34
- start_send_pump unless @send_pump_started
39
+
40
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
41
+ @conn_queues[connection] = q
42
+ start_conn_send_pump(connection, q)
35
43
  end
36
44
 
45
+
37
46
  # @param connection [Connection]
38
47
  #
39
48
  def connection_removed(connection)
40
49
  @connections.delete(connection)
50
+ @recv_queue.remove_queue(connection)
51
+ @conn_queues.delete(connection)
52
+ @conn_send_tasks.delete(connection)&.stop
41
53
  end
42
54
 
55
+
56
+ # Enqueues a subscription command (fan-out to all connected PUBs).
57
+ #
43
58
  # @param parts [Array<String>]
44
59
  #
45
60
  def enqueue(parts)
46
- @send_queue.enqueue(parts)
61
+ @connections.each { |conn| @conn_queues[conn]&.enqueue(parts) }
47
62
  end
48
63
 
64
+
65
+ # Stops all background tasks.
66
+ #
67
+ # @return [void]
49
68
  #
50
69
  def stop
51
70
  @tasks.each(&:stop)
52
71
  @tasks.clear
53
72
  end
54
73
 
55
- def send_pump_idle? = @send_pump_idle
74
+
75
+ # @return [Boolean] true when all per-connection send queues are empty
76
+ #
77
+ def send_queues_drained?
78
+ @conn_queues.values.all?(&:empty?)
79
+ end
56
80
 
57
81
  private
58
82
 
59
- def start_send_pump
60
- @send_pump_started = true
61
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
83
+ def start_conn_send_pump(conn, q)
84
+ task = @engine.spawn_pump_task(annotation: "send pump") do
62
85
  loop do
63
- @send_pump_idle = true
64
- parts = @send_queue.dequeue
65
- @send_pump_idle = false
86
+ parts = q.dequeue
66
87
  frame = parts.first&.b
67
88
  next if frame.nil? || frame.empty?
68
-
69
89
  flag = frame.getbyte(0)
70
90
  prefix = frame.byteslice(1..) || "".b
71
-
72
- case flag
73
- when 0x01
74
- @connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix)) }
75
- when 0x00
76
- @connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix)) }
91
+ begin
92
+ case flag
93
+ when 0x01
94
+ conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
95
+ when 0x00
96
+ conn.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix))
97
+ end
98
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
99
+ @engine.connection_lost(conn)
100
+ break
77
101
  end
78
102
  end
79
103
  end
104
+ @conn_send_tasks[conn] = task
105
+ @tasks << task
80
106
  end
81
107
  end
82
108
  end