omq 0.5.0 → 0.6.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +195 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +444 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +59 -12
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +25 -36
  28. data/lib/omq/zmtp/routing/channel.rb +14 -3
  29. data/lib/omq/zmtp/routing/fan_out.rb +52 -10
  30. data/lib/omq/zmtp/routing/pair.rb +14 -3
  31. data/lib/omq/zmtp/routing/peer.rb +28 -6
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +45 -12
  34. data/lib/omq/zmtp/routing/rep.rb +32 -13
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +72 -3
  37. data/lib/omq/zmtp/routing/router.rb +30 -10
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +28 -6
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/routing.rb +19 -0
  42. data/lib/omq/zmtp/transport/inproc.rb +48 -5
  43. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  44. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  45. data/lib/omq/zmtp/writable.rb +21 -4
  46. data/lib/omq.rb +7 -0
  47. metadata +18 -3
  48. data/exe/omqcat +0 -532
@@ -14,7 +14,8 @@ module OMQ
14
14
  @connection = nil
15
15
  @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
16
16
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
17
- @tasks = []
17
+ @tasks = []
18
+ @send_pump_idle = true
18
19
  end
19
20
 
20
21
  # @return [Async::LimitedQueue]
@@ -56,9 +57,19 @@ module OMQ
56
57
 
57
58
  private
58
59
 
60
+ def send_pump_idle? = @send_pump_idle
61
+
62
+
59
63
  def start_send_pump(conn)
60
- @send_pump = Reactor.spawn_pump do
61
- loop { conn.send_message(@send_queue.dequeue) }
64
+ @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
65
+ loop do
66
+ @send_pump_idle = true
67
+ batch = [@send_queue.dequeue]
68
+ @send_pump_idle = false
69
+ Routing.drain_send_queue(@send_queue, batch)
70
+ batch.each { |parts| conn.write_message(parts) }
71
+ conn.flush
72
+ end
62
73
  rescue *ZMTP::CONNECTION_LOST
63
74
  @engine.connection_lost(conn)
64
75
  end
@@ -12,6 +12,8 @@ module OMQ
12
12
  # their #initialize.
13
13
  #
14
14
  module FanOut
15
+ attr_reader :subscriber_joined
16
+
15
17
  private
16
18
 
17
19
  def init_fan_out(engine)
@@ -19,6 +21,9 @@ module OMQ
19
21
  @subscriptions = {} # connection => Set of prefixes
20
22
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
21
23
  @send_pump_started = false
24
+ @send_pump_idle = true
25
+ @conflate = engine.options.conflate
26
+ @subscriber_joined = Async::Promise.new
22
27
  end
23
28
 
24
29
  # @return [Boolean] whether the connection is subscribed to the topic
@@ -38,6 +43,7 @@ module OMQ
38
43
  #
39
44
  def on_subscribe(conn, prefix)
40
45
  @subscriptions[conn] << prefix
46
+ @subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
41
47
  end
42
48
 
43
49
  # Called when a cancel command is received from a peer.
@@ -50,26 +56,62 @@ module OMQ
50
56
  @subscriptions[conn]&.delete(prefix)
51
57
  end
52
58
 
59
+ # @return [Boolean] true when the send pump is idle (not sending a batch)
60
+ def send_pump_idle? = @send_pump_idle
61
+
62
+
53
63
  def start_send_pump
54
64
  @send_pump_started = true
55
- @tasks << Reactor.spawn_pump do
65
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
56
66
  loop do
57
- parts = @send_queue.dequeue
58
- topic = parts.first || "".b
59
- @connections.each do |conn|
60
- next unless subscribed?(conn, topic)
61
- begin
62
- conn.send_message(parts)
63
- rescue *ZMTP::CONNECTION_LOST
64
- # connection dead — will be cleaned up
67
+ @send_pump_idle = true
68
+ batch = [@send_queue.dequeue]
69
+ @send_pump_idle = false
70
+ Routing.drain_send_queue(@send_queue, batch)
71
+
72
+ written = Set.new
73
+
74
+ if @conflate
75
+ # Keep only the last matching message per connection.
76
+ latest = {} # conn => parts
77
+ batch.each do |parts|
78
+ topic = parts.first || "".b
79
+ @connections.each do |conn|
80
+ next unless subscribed?(conn, topic)
81
+ latest[conn] = parts
82
+ end
65
83
  end
84
+ latest.each do |conn, parts|
85
+ begin
86
+ conn.write_message(parts)
87
+ written << conn
88
+ rescue *ZMTP::CONNECTION_LOST
89
+ end
90
+ end
91
+ else
92
+ batch.each do |parts|
93
+ topic = parts.first || "".b
94
+ @connections.each do |conn|
95
+ next unless subscribed?(conn, topic)
96
+ begin
97
+ conn.write_message(parts)
98
+ written << conn
99
+ rescue *ZMTP::CONNECTION_LOST
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ written.each do |conn|
106
+ conn.flush
107
+ rescue *ZMTP::CONNECTION_LOST
66
108
  end
67
109
  end
68
110
  end
69
111
  end
70
112
 
71
113
  def start_subscription_listener(conn)
72
- @tasks << Reactor.spawn_pump do
114
+ @tasks << Reactor.spawn_pump(annotation: "recv pump") do
73
115
  loop do
74
116
  frame = conn.read_frame
75
117
  next unless frame.command?
@@ -17,7 +17,8 @@ module OMQ
17
17
  @connection = nil
18
18
  @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
19
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
20
- @tasks = []
20
+ @tasks = []
21
+ @send_pump_idle = true
21
22
  end
22
23
 
23
24
  # @return [Async::LimitedQueue]
@@ -59,9 +60,19 @@ module OMQ
59
60
 
60
61
  private
61
62
 
63
+ def send_pump_idle? = @send_pump_idle
64
+
65
+
62
66
  def start_send_pump(conn)
63
- @send_pump = Reactor.spawn_pump do
64
- loop { conn.send_message(@send_queue.dequeue) }
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
65
76
  rescue *ZMTP::CONNECTION_LOST
66
77
  @engine.connection_lost(conn)
67
78
  end
@@ -21,6 +21,7 @@ module OMQ
21
21
  @connections_by_routing_id = {}
22
22
  @tasks = []
23
23
  @send_pump_started = false
24
+ @send_pump_idle = true
24
25
  end
25
26
 
26
27
  # @return [Async::LimitedQueue]
@@ -59,15 +60,36 @@ module OMQ
59
60
 
60
61
  private
61
62
 
63
+ def send_pump_idle? = @send_pump_idle
64
+
65
+
62
66
  def start_send_pump
63
67
  @send_pump_started = true
64
- @tasks << Reactor.spawn_pump do
68
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
65
69
  loop do
66
- parts = @send_queue.dequeue
67
- routing_id = parts.first
68
- conn = @connections_by_routing_id[routing_id]
69
- next unless conn # silently drop if peer gone
70
- conn.send_message(parts[1..])
70
+ @send_pump_idle = true
71
+ batch = [@send_queue.dequeue]
72
+ @send_pump_idle = false
73
+ Routing.drain_send_queue(@send_queue, batch)
74
+
75
+ written = Set.new
76
+ batch.each do |parts|
77
+ routing_id = parts.first
78
+ conn = @connections_by_routing_id[routing_id]
79
+ next unless conn # silently drop if peer gone
80
+ begin
81
+ conn.write_message(parts[1..])
82
+ written << conn
83
+ rescue *ZMTP::CONNECTION_LOST
84
+ # will be cleaned up
85
+ end
86
+ end
87
+
88
+ written.each do |conn|
89
+ conn.flush
90
+ rescue *ZMTP::CONNECTION_LOST
91
+ # will be cleaned up
92
+ end
71
93
  end
72
94
  end
73
95
  end
@@ -16,37 +16,44 @@ module OMQ
16
16
  init_round_robin(engine)
17
17
  end
18
18
 
19
+
19
20
  # @return [Async::LimitedQueue]
20
21
  #
21
22
  attr_reader :send_queue
22
23
 
24
+
23
25
  # PUSH is write-only.
24
26
  #
25
27
  def recv_queue
26
28
  raise "PUSH sockets cannot receive"
27
29
  end
28
30
 
31
+
29
32
  # @param connection [Connection]
30
33
  #
31
34
  def connection_added(connection)
32
35
  @connections << connection
33
36
  signal_connection_available
34
37
  start_send_pump unless @send_pump_started
35
- start_monitor(connection)
38
+ start_reaper(connection)
36
39
  end
37
40
 
41
+
38
42
  # @param connection [Connection]
39
43
  #
40
44
  def connection_removed(connection)
41
45
  @connections.delete(connection)
42
46
  end
43
47
 
48
+
44
49
  # @param parts [Array<String>]
45
50
  #
46
51
  def enqueue(parts)
47
52
  @send_queue.enqueue(parts)
48
53
  end
49
54
 
55
+
56
+ # Stops all background tasks (send pump, reapers).
50
57
  #
51
58
  def stop
52
59
  @tasks.each(&:stop)
@@ -55,13 +62,13 @@ module OMQ
55
62
 
56
63
  private
57
64
 
58
- # Monitors a connection for disconnection.
59
- # Write-only sockets have no recv pump, so without this monitor
60
- # a dead peer is only detected on the next send — which may
61
- # succeed if the kernel send buffer absorbs the data.
65
+
66
+ # Detects peer disconnection on write-only sockets. Without
67
+ # this, a dead peer is only noticed on the next send — which
68
+ # may succeed if the kernel send buffer absorbs the data.
62
69
  #
63
- def start_monitor(conn)
64
- @tasks << Reactor.spawn_pump do
70
+ def start_reaper(conn)
71
+ @tasks << Reactor.spawn_pump(annotation: "reaper") do
65
72
  conn.receive_message # blocks until peer disconnects
66
73
  rescue *ZMTP::CONNECTION_LOST
67
74
  @engine.connection_lost(conn)
@@ -21,6 +21,7 @@ module OMQ
21
21
  @groups = {} # connection => Set of joined groups
22
22
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
23
23
  @send_pump_started = false
24
+ @conflate = engine.options.conflate
24
25
  @tasks = []
25
26
  end
26
27
 
@@ -67,26 +68,58 @@ module OMQ
67
68
 
68
69
  def start_send_pump
69
70
  @send_pump_started = true
70
- @tasks << Reactor.spawn_pump do
71
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
71
72
  loop do
72
- parts = @send_queue.dequeue
73
- group = parts[0]
74
- body = parts[1] || "".b
75
- @connections.each do |conn|
76
- next unless @groups[conn]&.include?(group)
77
- begin
78
- # Wire format: group frame (MORE) + body frame
79
- conn.send_message([group, body])
80
- rescue *ZMTP::CONNECTION_LOST
81
- # connection dead will be cleaned up
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 = Set.new
79
+
80
+ if @conflate
81
+ # Keep only the last matching message per connection.
82
+ latest = {} # conn => [group, body]
83
+ batch.each do |parts|
84
+ group = parts[0]
85
+ body = parts[1] || "".b
86
+ @connections.each do |conn|
87
+ next unless @groups[conn]&.include?(group)
88
+ latest[conn] = [group, body]
89
+ end
90
+ end
91
+ latest.each do |conn, msg|
92
+ begin
93
+ conn.write_message(msg)
94
+ written << conn
95
+ rescue *ZMTP::CONNECTION_LOST
96
+ end
82
97
  end
98
+ else
99
+ batch.each do |parts|
100
+ group = parts[0]
101
+ body = parts[1] || "".b
102
+ @connections.each do |conn|
103
+ next unless @groups[conn]&.include?(group)
104
+ begin
105
+ conn.write_message([group, body])
106
+ written << conn
107
+ rescue *ZMTP::CONNECTION_LOST
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ written.each do |conn|
114
+ conn.flush
115
+ rescue *ZMTP::CONNECTION_LOST
83
116
  end
84
117
  end
85
118
  end
86
119
  end
87
120
 
88
121
  def start_group_listener(conn)
89
- @tasks << Reactor.spawn_pump do
122
+ @tasks << Reactor.spawn_pump(annotation: "recv pump") do
90
123
  loop do
91
124
  frame = conn.read_frame
92
125
  next unless frame.command?
@@ -19,6 +19,7 @@ module OMQ
19
19
  @pending_replies = []
20
20
  @tasks = []
21
21
  @send_pump_started = false
22
+ @send_pump_idle = true
22
23
  end
23
24
 
24
25
  # @return [Async::LimitedQueue]
@@ -29,13 +30,11 @@ module OMQ
29
30
  #
30
31
  def connection_added(connection)
31
32
  transform = ->(msg) {
32
- envelope = []
33
- while msg.first && !msg.first.empty?
34
- envelope << msg.shift
35
- end
36
- msg.shift # remove empty delimiter
33
+ delimiter = msg.index(&:empty?) || msg.size
34
+ envelope = msg[0, delimiter]
35
+ body = msg[(delimiter + 1)..] || []
37
36
  @pending_replies << { conn: connection, envelope: envelope }
38
- msg
37
+ body
39
38
  }
40
39
  task = @engine.start_recv_pump(connection, @recv_queue, transform: transform)
41
40
  @tasks << task if task
@@ -64,17 +63,37 @@ module OMQ
64
63
 
65
64
  private
66
65
 
66
+ def send_pump_idle? = @send_pump_idle
67
+
68
+
67
69
  def start_send_pump
68
70
  @send_pump_started = true
69
- @tasks << Reactor.spawn_pump do
71
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
70
72
  loop do
71
- parts = @send_queue.dequeue
72
- reply_info = @pending_replies.shift
73
- next unless reply_info
74
- reply_info[:conn].send_message([*reply_info[:envelope], "".b, *parts])
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 = Set.new
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], "".b, *parts])
85
+ written << conn
86
+ rescue *ZMTP::CONNECTION_LOST
87
+ # connection lost mid-write
88
+ end
89
+ end
90
+
91
+ written.each do |conn|
92
+ conn.flush
93
+ rescue *ZMTP::CONNECTION_LOST
94
+ # connection lost mid-flush
95
+ end
75
96
  end
76
- rescue *ZMTP::CONNECTION_LOST
77
- # connection lost mid-write
78
97
  end
79
98
  end
80
99
  end
@@ -61,8 +61,7 @@ module OMQ
61
61
  # REQ strips the leading empty delimiter frame on receive.
62
62
  #
63
63
  def transform_recv(msg)
64
- msg.shift if msg.first&.empty?
65
- msg
64
+ msg.first&.empty? ? msg[1..] : msg
66
65
  end
67
66
  end
68
67
  end
@@ -15,20 +15,31 @@ module OMQ
15
15
  module RoundRobin
16
16
  private
17
17
 
18
+
19
+ # Initializes round-robin state for the including class.
20
+ #
21
+ # @param engine [Engine]
22
+ #
18
23
  def init_round_robin(engine)
19
24
  @connections = []
20
25
  @cycle = @connections.cycle
21
26
  @connection_available = Async::Promise.new
22
27
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
23
28
  @send_pump_started = false
29
+ @send_pump_idle = true
24
30
  end
25
31
 
32
+
33
+ # Resolves the connection-available promise so blocked
34
+ # senders can proceed.
35
+ #
26
36
  def signal_connection_available
27
37
  unless @connection_available.resolved?
28
38
  @connection_available.resolve(true)
29
39
  end
30
40
  end
31
41
 
42
+
32
43
  # Blocks until a connection is available, then returns
33
44
  # the next one in round-robin order.
34
45
  #
@@ -43,6 +54,7 @@ module OMQ
43
54
  retry
44
55
  end
45
56
 
57
+
46
58
  # Transforms parts before sending. Override in subclasses
47
59
  # (e.g. REQ prepends an empty delimiter frame).
48
60
  #
@@ -51,16 +63,38 @@ module OMQ
51
63
  #
52
64
  def transform_send(parts) = parts
53
65
 
66
+
67
+ # Starts the background send pump that dequeues messages
68
+ # and dispatches them round-robin across connections.
69
+ #
70
+ # @return [Boolean] true when the send pump is idle (not sending a batch)
71
+ def send_pump_idle? = @send_pump_idle
72
+
73
+
54
74
  def start_send_pump
55
75
  @send_pump_started = true
56
- @tasks << Reactor.spawn_pump do
76
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
57
77
  loop do
58
- parts = @send_queue.dequeue
59
- send_with_retry(parts)
78
+ @send_pump_idle = true
79
+ batch = [@send_queue.dequeue]
80
+ @send_pump_idle = false
81
+ Routing.drain_send_queue(@send_queue, batch)
82
+
83
+ if batch.size == 1
84
+ send_with_retry(batch[0])
85
+ else
86
+ send_batch(batch)
87
+ end
60
88
  end
61
89
  end
62
90
  end
63
91
 
92
+
93
+ # Sends a single message, retrying on a new connection if
94
+ # the current one is lost.
95
+ #
96
+ # @param parts [Array<String>]
97
+ #
64
98
  def send_with_retry(parts)
65
99
  conn = next_connection
66
100
  conn.send_message(transform_send(parts))
@@ -68,6 +102,41 @@ module OMQ
68
102
  @engine.connection_lost(conn)
69
103
  retry
70
104
  end
105
+
106
+
107
+ # Sends a batch of messages, writing without flushing for
108
+ # throughput. Falls back to #send_with_retry on failure.
109
+ #
110
+ # @param batch [Array<Array<String>>]
111
+ #
112
+ def send_batch(batch)
113
+ written = Set.new
114
+ batch.each_with_index do |parts, i|
115
+ conn = next_connection
116
+ begin
117
+ conn.write_message(transform_send(parts))
118
+ written << conn
119
+ rescue *ZMTP::CONNECTION_LOST
120
+ @engine.connection_lost(conn)
121
+ # Flush what we've written so far
122
+ written.each do |c|
123
+ c.flush
124
+ rescue *ZMTP::CONNECTION_LOST
125
+ # will be cleaned up
126
+ end
127
+ written.clear
128
+ # Fall back to send_with_retry for this and remaining
129
+ send_with_retry(parts)
130
+ batch[(i + 1)..].each { |p| send_with_retry(p) }
131
+ return
132
+ end
133
+ end
134
+ written.each do |conn|
135
+ conn.flush
136
+ rescue *ZMTP::CONNECTION_LOST
137
+ # will be cleaned up
138
+ end
139
+ end
71
140
  end
72
141
  end
73
142
  end
@@ -21,6 +21,7 @@ module OMQ
21
21
  @connections_by_identity = {}
22
22
  @tasks = []
23
23
  @send_pump_started = false
24
+ @send_pump_idle = true
24
25
  end
25
26
 
26
27
  # @return [Async::LimitedQueue]
@@ -52,6 +53,12 @@ module OMQ
52
53
  # @param parts [Array<String>]
53
54
  #
54
55
  def enqueue(parts)
56
+ if @engine.options.router_mandatory?
57
+ identity = parts.first
58
+ unless @connections_by_identity[identity]
59
+ raise SocketError, "no route to identity #{identity.inspect}"
60
+ end
61
+ end
55
62
  @send_queue.enqueue(parts)
56
63
  end
57
64
 
@@ -62,23 +69,36 @@ module OMQ
62
69
 
63
70
  private
64
71
 
72
+ def send_pump_idle? = @send_pump_idle
73
+
74
+
65
75
  def start_send_pump
66
76
  @send_pump_started = true
67
- @tasks << Reactor.spawn_pump do
77
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
68
78
  loop do
69
- parts = @send_queue.dequeue
70
- identity = parts.first
71
- conn = @connections_by_identity[identity]
79
+ @send_pump_idle = true
80
+ batch = [@send_queue.dequeue]
81
+ @send_pump_idle = false
82
+ Routing.drain_send_queue(@send_queue, batch)
72
83
 
73
- unless conn
74
- if @engine.options.router_mandatory?
75
- raise SocketError, "no route to identity #{identity.inspect}"
84
+ written = Set.new
85
+ batch.each do |parts|
86
+ identity = parts.first
87
+ conn = @connections_by_identity[identity]
88
+ next unless conn # silently drop (peer may have disconnected)
89
+ begin
90
+ conn.write_message(parts[1..])
91
+ written << conn
92
+ rescue *ZMTP::CONNECTION_LOST
93
+ # will be cleaned up
76
94
  end
77
- next # silently drop
78
95
  end
79
96
 
80
- # Send everything after the identity frame
81
- conn.send_message(parts[1..])
97
+ written.each do |conn|
98
+ conn.flush
99
+ rescue *ZMTP::CONNECTION_LOST
100
+ # will be cleaned up
101
+ end
82
102
  end
83
103
  end
84
104
  end