rainbows 0.3.0 → 0.4.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 (86) hide show
  1. data/GIT-VERSION-GEN +1 -1
  2. data/GNUmakefile +12 -6
  3. data/README +3 -2
  4. data/Rakefile +3 -3
  5. data/TODO +2 -9
  6. data/lib/rainbows.rb +1 -0
  7. data/lib/rainbows/app_pool.rb +10 -6
  8. data/lib/rainbows/base.rb +1 -1
  9. data/lib/rainbows/const.rb +1 -1
  10. data/lib/rainbows/ev_core.rb +88 -0
  11. data/lib/rainbows/event_machine.rb +218 -0
  12. data/lib/rainbows/http_server.rb +4 -1
  13. data/lib/rainbows/rev.rb +21 -89
  14. data/lib/rainbows/revactor.rb +4 -10
  15. data/local.mk.sample +13 -7
  16. data/rainbows.gemspec +17 -2
  17. data/t/.gitignore +1 -0
  18. data/t/GNUmakefile +63 -40
  19. data/t/README +6 -2
  20. data/t/async_sinatra.ru +13 -0
  21. data/t/bin/unused_listen +1 -1
  22. data/t/large-file-response.ru +1 -0
  23. data/t/my-tap-lib.sh +200 -0
  24. data/t/simple-http_Base.ru +3 -0
  25. data/t/simple-http_EventMachine.ru +9 -0
  26. data/t/simple-http_Rev.ru +9 -0
  27. data/t/simple-http_Revactor.ru +10 -0
  28. data/t/simple-http_ThreadPool.ru +10 -0
  29. data/t/simple-http_ThreadSpawn.ru +10 -0
  30. data/t/t0000-simple-http.sh +142 -0
  31. data/t/t0001-unix-http.sh +103 -0
  32. data/t/t0002-graceful.sh +32 -0
  33. data/t/t0002-parser-error.sh +31 -0
  34. data/t/t0003-reopen-logs.sh +97 -0
  35. data/t/t0005-large-file-response.sh +83 -0
  36. data/t/t0100-rack-input-hammer.sh +45 -0
  37. data/t/t0101-rack-input-trailer.sh +68 -0
  38. data/t/t0200-async-response.sh +66 -0
  39. data/t/t0201-async-response-no-autochunk.sh +3 -0
  40. data/t/t0300-async_sinatra.sh +65 -0
  41. data/t/t9000-rack-app-pool.sh +45 -33
  42. data/t/test-lib.sh +67 -56
  43. metadata +26 -56
  44. data/t/lib-async-response-no-autochunk.sh +0 -6
  45. data/t/lib-async-response.sh +0 -45
  46. data/t/lib-graceful.sh +0 -40
  47. data/t/lib-input-trailer.sh +0 -63
  48. data/t/lib-large-file-response.sh +0 -45
  49. data/t/lib-parser-error.sh +0 -29
  50. data/t/lib-rack-input-hammer.sh +0 -38
  51. data/t/lib-reopen-logs.sh +0 -60
  52. data/t/lib-simple-http.sh +0 -92
  53. data/t/t0000-basic.sh +0 -2
  54. data/t/t1000-thread-pool-basic.sh +0 -2
  55. data/t/t1002-thread-pool-graceful.sh +0 -2
  56. data/t/t1003-thread-pool-reopen-logs.sh +0 -2
  57. data/t/t1004-thread-pool-async-response.sh +0 -45
  58. data/t/t1005-thread-pool-large-file-response.sh +0 -45
  59. data/t/t1006-thread-pool-async-response-no-autochunk.sh +0 -6
  60. data/t/t1100-thread-pool-rack-input.sh +0 -2
  61. data/t/t1101-thread-pool-input-trailer.sh +0 -2
  62. data/t/t2000-thread-spawn-basic.sh +0 -2
  63. data/t/t2002-thread-spawn-graceful.sh +0 -2
  64. data/t/t2003-thread-spawn-reopen-logs.sh +0 -2
  65. data/t/t2004-thread-spawn-async-response.sh +0 -45
  66. data/t/t2005-thread-spawn-large-file-response.sh +0 -45
  67. data/t/t2006-thread-spawn-async-response-no-autochunk.sh +0 -6
  68. data/t/t2100-thread-spawn-rack-input.sh +0 -2
  69. data/t/t2101-thread-spawn-input-trailer.sh +0 -2
  70. data/t/t3000-revactor-basic.sh +0 -2
  71. data/t/t3002-revactor-graceful.sh +0 -2
  72. data/t/t3003-revactor-reopen-logs.sh +0 -2
  73. data/t/t3004-revactor-async-response.sh +0 -45
  74. data/t/t3005-revactor-large-file-response.sh +0 -2
  75. data/t/t3006-revactor-async-response-no-autochunk.sh +0 -6
  76. data/t/t3100-revactor-rack-input.sh +0 -2
  77. data/t/t3101-revactor-rack-input-trailer.sh +0 -2
  78. data/t/t4000-rev-basic.sh +0 -2
  79. data/t/t4002-rev-graceful.sh +0 -2
  80. data/t/t4003-rev-parser-error.sh +0 -2
  81. data/t/t4003-rev-reopen-logs.sh +0 -2
  82. data/t/t4004-rev-async-response.sh +0 -45
  83. data/t/t4005-rev-large-file-response.sh +0 -2
  84. data/t/t4006-rev-async-response-no-autochunk.sh +0 -6
  85. data/t/t4100-rev-rack-input.sh +0 -2
  86. data/t/t4101-rev-rack-input-trailer.sh +0 -2
data/GIT-VERSION-GEN CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
3
  GVF=GIT-VERSION-FILE
4
- DEF_VER=v0.3.0.GIT
4
+ DEF_VER=v0.4.0.GIT
5
5
 
6
6
  LF='
7
7
  '
data/GNUmakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  # use GNU Make to run tests in parallel, and without depending on Rubygems
2
2
  all::
3
- ruby = ruby
3
+ RUBY = ruby
4
4
  rake = rake
5
5
  GIT_URL = git://git.bogomips.org/rainbows.git
6
6
 
@@ -8,11 +8,17 @@ GIT-VERSION-FILE: .FORCE-GIT-VERSION-FILE
8
8
  @./GIT-VERSION-GEN
9
9
  -include GIT-VERSION-FILE
10
10
  -include local.mk
11
+ ifdef ruby
12
+ ifeq ($(RUBY),ruby)
13
+ $(warning ruby variable is deprecated, use RUBY instead)
14
+ RUBY = $(ruby)
15
+ endif
16
+ endif
11
17
  ifeq ($(DLEXT),) # "so" for Linux
12
- DLEXT := $(shell $(ruby) -rrbconfig -e 'puts Config::CONFIG["DLEXT"]')
18
+ DLEXT := $(shell $(RUBY) -rrbconfig -e 'puts Config::CONFIG["DLEXT"]')
13
19
  endif
14
20
  ifeq ($(RUBY_VERSION),)
15
- RUBY_VERSION := $(shell $(ruby) -e 'puts RUBY_VERSION')
21
+ RUBY_VERSION := $(shell $(RUBY) -e 'puts RUBY_VERSION')
16
22
  endif
17
23
 
18
24
  base_bins := rainbows
@@ -25,7 +31,7 @@ install: $(bins)
25
31
  $(RM) -r .install-tmp
26
32
  mkdir .install-tmp
27
33
  cp -p bin/* .install-tmp
28
- $(ruby) setup.rb all
34
+ $(RUBY) setup.rb all
29
35
  $(RM) $^
30
36
  mv .install-tmp/* bin/
31
37
  $(RM) -r .install-tmp
@@ -82,10 +88,10 @@ doc: .document NEWS ChangeLog
82
88
  cd doc && for i in $(base_bins); do \
83
89
  sed -e '/"documentation">/r man1/'$$i'.1.html' \
84
90
  < $${i}_1.html > tmp && mv tmp $${i}_1.html; done
85
- $(ruby) -i -p -e \
91
+ $(RUBY) -i -p -e \
86
92
  '$$_.gsub!("</title>",%q{\&$(call atom,$(cgit_atom))})' \
87
93
  doc/ChangeLog.html
88
- $(ruby) -i -p -e \
94
+ $(RUBY) -i -p -e \
89
95
  '$$_.gsub!("</title>",%q{\&$(call atom,$(news_atom))})' \
90
96
  doc/NEWS.html doc/README.html
91
97
  $(rake) -s news_atom > doc/NEWS.atom.xml
data/README CHANGED
@@ -13,10 +13,11 @@ suck; differently.
13
13
 
14
14
  For network concurrency, models we currently support are:
15
15
 
16
- * {:ThreadSpawn}[link:Rainbows/ThreadSpawn.html]
17
- * {:ThreadPool}[link:Rainbows/ThreadPool.html]
18
16
  * {:Revactor}[link:Rainbows/Revactor.html]
17
+ * {:ThreadPool}[link:Rainbows/ThreadPool.html]
19
18
  * {:Rev}[link:Rainbows/Rev.html]*
19
+ * {:ThreadSpawn}[link:Rainbows/ThreadSpawn.html]
20
+ * {:EventMachine}[link:Rainbows/EventMachine.html]
20
21
 
21
22
  We have {more on the way}[link:TODO.html] for handling network concurrency.
22
23
  Additionally, we also use multiple processes (managed by Unicorn) for
data/Rakefile CHANGED
@@ -12,8 +12,8 @@ def tags
12
12
  body ||= "initial"
13
13
  {
14
14
  :time => Time.at(tagger.split(/ /)[-2].to_i).utc.strftime(timefmt),
15
- :tagger_name => %r{^tagger ([^<]+)}.match(tagger)[1],
16
- :tagger_email => %r{<([^>]+)>}.match(tagger)[1],
15
+ :tagger_name => %r{^tagger ([^<]+)}.match(tagger)[1].strip,
16
+ :tagger_email => %r{<([^>]+)>}.match(tagger)[1].strip,
17
17
  :id => `git rev-parse refs/tags/#{tag}`.chomp!,
18
18
  :tag => tag,
19
19
  :subject => subject,
@@ -49,7 +49,7 @@ task :news_atom do
49
49
  url = "#{cgit_url}/tag/?id=#{tag[:tag]}"
50
50
  link! :rel => "alternate", :type => "text/html", :href =>url
51
51
  id! url
52
- content(:type => 'text') { tag[:body] }
52
+ content({:type => 'text'}, tag[:body])
53
53
  end
54
54
  end
55
55
  end
data/TODO CHANGED
@@ -7,18 +7,9 @@ care about.
7
7
  unit tests, only integration tests that exercise externally
8
8
  visible parts.
9
9
 
10
- * (maybe) make our tests TAP-compatible. Unfortunately the the
11
- only shell library we've seen for TAP seems to use bashisms.
12
-
13
10
  * Rev + Thread - current Rev model with threading, which will give
14
11
  us a streaming (but rewindable) "rack.input".
15
12
 
16
- * EventMachine - much like Rev, but we haven't looked at this one much
17
- (our benevolent dictator doesn't like C++). If we can figure out how
18
- to do Rev without Revactor, then this should be pretty easy.
19
-
20
- * EventMachine + catch/throw :async API used in Thin
21
-
22
13
  * EventMachine.spawn - should be like Revactor, maybe?
23
14
 
24
15
  * Rev + callcc - current Rev model with callcc (should work with MBARI)
@@ -28,6 +19,8 @@ care about.
28
19
 
29
20
  * Omnibus - haven't looked into it, probably like Revactor with 1.8?
30
21
 
22
+ * Packet - pure Ruby, EventMachine-like library
23
+
31
24
  * Rubinius Actors - should be like Revactor and easily doable once
32
25
  Rubinius gets more mature.
33
26
 
data/lib/rainbows.rb CHANGED
@@ -56,6 +56,7 @@ module Rainbows
56
56
  :ThreadSpawn => 30,
57
57
  :ThreadPool => 10,
58
58
  :Rev => 50,
59
+ :EventMachine => 50,
59
60
  }.each do |model, _|
60
61
  u = model.to_s.gsub(/([a-z0-9])([A-Z0-9])/) { "#{$1}_#{$2.downcase!}" }
61
62
  autoload model, "rainbows/#{u.downcase!}"
@@ -39,14 +39,18 @@ module Rainbows
39
39
  # AppPool should be used if you want to enforce a lower value of +P+
40
40
  # than +N+.
41
41
  #
42
- # AppPool has no effect on the Rev concurrency model as that is
43
- # single-threaded/single-instance as far as application concurrency goes.
44
- # In other words, +P+ is always +one+ when using Rev. AppPool currently
45
- # only works with the ThreadSpawn and ThreadPool models. It does not
46
- # yet work reliably with the Revactor model, yet.
42
+ # AppPool has no effect on the Rev or EventMachine concurrency models
43
+ # as those are single-threaded/single-instance as far as application
44
+ # concurrency goes. In other words, +P+ is always +one+ when using
45
+ # Rev or EventMachine. AppPool currently only works with the
46
+ # ThreadSpawn and ThreadPool models. It does not yet work reliably
47
+ # with the Revactor model, but actors are far more lightweight and
48
+ # probably better suited for lightweight applications that would
49
+ # not benefit from AppPool.
47
50
  #
48
51
  # Since this is Rack middleware, you may load this in your Rack
49
- # config.ru file and even use it in servers other than \Rainbows!
52
+ # config.ru file and even use it in threaded servers other than
53
+ # \Rainbows!
50
54
  #
51
55
  # use Rainbows::AppPool, :size => 30
52
56
  # map "/lobster" do
data/lib/rainbows/base.rb CHANGED
@@ -93,7 +93,7 @@ module Rainbows
93
93
  Rainbows::G.alive = false
94
94
  expire = Time.now + (timeout * 2.0)
95
95
  m = 0
96
- while (nr = threads.count { |thr| thr.alive? }) > 0
96
+ until (threads.delete_if { |thr| ! thr.alive? }).empty?
97
97
  threads.each { |thr|
98
98
  worker.tmp.chmod(m = 0 == m ? 1 : 0)
99
99
  thr.join(1)
@@ -3,7 +3,7 @@
3
3
  module Rainbows
4
4
 
5
5
  module Const
6
- RAINBOWS_VERSION = '0.3.0'
6
+ RAINBOWS_VERSION = '0.4.0'
7
7
 
8
8
  include Unicorn::Const
9
9
 
@@ -0,0 +1,88 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module Rainbows
4
+
5
+ # base module for evented models like Rev and EventMachine
6
+ module EvCore
7
+ include Unicorn
8
+ include Rainbows::Const
9
+ G = Rainbows::G
10
+
11
+ def post_init
12
+ @remote_addr = ::TCPSocket === @_io ? @_io.peeraddr.last : LOCALHOST
13
+ @env = {}
14
+ @hp = HttpParser.new
15
+ @state = :headers # [ :body [ :trailers ] ] :app_call :close
16
+ @buf = ""
17
+ @deferred_bodies = [] # for (fast) regular files only
18
+ end
19
+
20
+ # graceful exit, like SIGQUIT
21
+ def quit
22
+ @state = :close
23
+ end
24
+
25
+ def handle_error(e)
26
+ msg = case e
27
+ when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
28
+ ERROR_500_RESPONSE
29
+ when HttpParserError # try to tell the client they're bad
30
+ ERROR_400_RESPONSE
31
+ else
32
+ G.logger.error "Read error: #{e.inspect}"
33
+ G.logger.error e.backtrace.join("\n")
34
+ ERROR_500_RESPONSE
35
+ end
36
+ write(msg)
37
+ quit
38
+ end
39
+
40
+ def tmpio
41
+ io = Util.tmpio
42
+ def io.size
43
+ # already sync=true at creation, so no need to flush before stat
44
+ stat.size
45
+ end
46
+ io
47
+ end
48
+
49
+ # TeeInput doesn't map too well to this right now...
50
+ def on_read(data)
51
+ case @state
52
+ when :headers
53
+ @hp.headers(@env, @buf << data) or return
54
+ @state = :body
55
+ len = @hp.content_length
56
+ if len == 0
57
+ @input = HttpRequest::NULL_IO
58
+ app_call # common case
59
+ else # nil or len > 0
60
+ # since we don't do streaming input, we have no choice but
61
+ # to take over 100-continue handling from the Rack application
62
+ if @env[HTTP_EXPECT] =~ /\A100-continue\z/i
63
+ write(EXPECT_100_RESPONSE)
64
+ @env.delete(HTTP_EXPECT)
65
+ end
66
+ @input = len && len <= MAX_BODY ? StringIO.new("") : tmpio
67
+ @hp.filter_body(@buf2 = @buf.dup, @buf)
68
+ @input << @buf2
69
+ on_read("")
70
+ end
71
+ when :body
72
+ if @hp.body_eof?
73
+ @state = :trailers
74
+ on_read(data)
75
+ elsif data.size > 0
76
+ @hp.filter_body(@buf2, @buf << data)
77
+ @input << @buf2
78
+ on_read("")
79
+ end
80
+ when :trailers
81
+ @hp.trailers(@env, @buf << data) and app_call
82
+ end
83
+ rescue Object => e
84
+ handle_error(e)
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,218 @@
1
+ # -*- encoding: binary -*-
2
+ require 'eventmachine'
3
+ EM::VERSION >= '0.12.10' or abort 'eventmachine 0.12.10 is required'
4
+ require 'rainbows/ev_core'
5
+
6
+ module Rainbows
7
+
8
+ # Implements a basic single-threaded event model with
9
+ # {EventMachine}[http://rubyeventmachine.com/]. It is capable of
10
+ # handling thousands of simultaneous client connections, but with only
11
+ # a single-threaded app dispatch. It is suited for slow clients and
12
+ # fast applications (applications that do not have slow network
13
+ # dependencies) or applications that use DevFdResponse for deferrable
14
+ # response bodies. It does not require your Rack application to be
15
+ # thread-safe, reentrancy is only required for the DevFdResponse body
16
+ # generator.
17
+ #
18
+ # Compatibility: Whatever \EventMachine ~> 0.12.10 and Unicorn both
19
+ # support, currently Ruby 1.8/1.9.
20
+ #
21
+ # This model is compatible with users of "async.callback" in the Rack
22
+ # environment such as
23
+ # {async_sinatra}[http://github.com/raggi/async_sinatra].
24
+ #
25
+ # This model does not implement as streaming "rack.input" which allows
26
+ # the Rack application to process data as it arrives. This means
27
+ # "rack.input" will be fully buffered in memory or to a temporary file
28
+ # before the application is entered.
29
+
30
+ module EventMachine
31
+
32
+ include Base
33
+
34
+ class Client < EM::Connection
35
+ include Rainbows::EvCore
36
+ G = Rainbows::G
37
+
38
+ # Apps may return this Rack response: AsyncResponse = [ -1, {}, [] ]
39
+ ASYNC_CALLBACK = 'async.callback'.freeze
40
+
41
+ def initialize(io)
42
+ @_io = io
43
+ end
44
+
45
+ alias write send_data
46
+ alias receive_data on_read
47
+
48
+ def quit
49
+ super
50
+ close_connection_after_writing
51
+ end
52
+
53
+ def app_call
54
+ begin
55
+ (@env[RACK_INPUT] = @input).rewind
56
+ alive = @hp.keepalive?
57
+ @env[REMOTE_ADDR] = @remote_addr
58
+ @env[ASYNC_CALLBACK] = method(:response_write)
59
+
60
+ response = catch(:async) { G.app.call(@env.update(RACK_DEFAULTS)) }
61
+
62
+ # too tricky to support pipelining with :async since the
63
+ # second (pipelined) request could be a stuck behind a
64
+ # long-running async response
65
+ (response.nil? || -1 == response.first) and return @state = :close
66
+
67
+ alive &&= G.alive
68
+ out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
69
+ response_write(response, out, alive)
70
+
71
+ if alive
72
+ @env.clear
73
+ @hp.reset
74
+ @state = :headers
75
+ # keepalive requests are always body-less, so @input is unchanged
76
+ @hp.headers(@env, @buf) and next
77
+ end
78
+ return
79
+ end while true
80
+ end
81
+
82
+ def response_write(response, out = [], alive = false)
83
+ body = response.last
84
+ unless body.respond_to?(:to_path)
85
+ HttpResponse.write(self, response, out)
86
+ quit unless alive
87
+ return
88
+ end
89
+
90
+ headers = Rack::Utils::HeaderHash.new(response[1])
91
+ path = body.to_path
92
+ io = body.to_io if body.respond_to?(:to_io)
93
+ io ||= IO.new($1.to_i) if path =~ %r{\A/dev/fd/(\d+)\z}
94
+ io ||= File.open(path, 'rb') # could be a named pipe
95
+
96
+ st = io.stat
97
+ if st.file?
98
+ headers.delete('Transfer-Encoding')
99
+ headers['Content-Length'] ||= st.size.to_s
100
+ response = [ response.first, headers.to_hash, [] ]
101
+ HttpResponse.write(self, response, out)
102
+ stream = stream_file_data(path)
103
+ stream.callback { quit } unless alive
104
+ elsif st.socket? || st.pipe?
105
+ do_chunk = !!(headers['Transfer-Encoding'] =~ %r{\Achunked\z}i)
106
+ do_chunk = false if headers.delete('X-Rainbows-Autochunk') == 'no'
107
+ if out.nil?
108
+ do_chunk = false
109
+ else
110
+ out[0] = CONN_CLOSE
111
+ end
112
+ response = [ response.first, headers.to_hash, [] ]
113
+ HttpResponse.write(self, response, out)
114
+ if do_chunk
115
+ EM.watch(io, ResponseChunkPipe, self).notify_readable = true
116
+ else
117
+ EM.enable_proxy(EM.attach(io, ResponsePipe, self), self)
118
+ end
119
+ else
120
+ HttpResponse.write(self, response, out)
121
+ end
122
+ end
123
+
124
+ def unbind
125
+ @_io.close
126
+ end
127
+ end
128
+
129
+ module ResponsePipe
130
+ def initialize(client)
131
+ @client = client
132
+ end
133
+
134
+ def unbind
135
+ @io.close
136
+ @client.quit
137
+ end
138
+ end
139
+
140
+ module ResponseChunkPipe
141
+ include ResponsePipe
142
+
143
+ def unbind
144
+ @client.write("0\r\n\r\n")
145
+ super
146
+ end
147
+
148
+ def notify_readable
149
+ begin
150
+ data = begin
151
+ @io.read_nonblock(16384)
152
+ rescue Errno::EINTR
153
+ retry
154
+ rescue Errno::EAGAIN
155
+ return
156
+ rescue EOFError
157
+ detach
158
+ return
159
+ end
160
+ @client.send_data(sprintf("%x\r\n", data.size))
161
+ @client.send_data(data)
162
+ @client.send_data("\r\n")
163
+ end while true
164
+ end
165
+ end
166
+
167
+ module Server
168
+
169
+ def initialize(conns)
170
+ @limit = Rainbows::G.max + HttpServer::LISTENERS.size
171
+ @em_conns = conns
172
+ end
173
+
174
+ def close
175
+ detach
176
+ @io.close
177
+ end
178
+
179
+ def notify_readable
180
+ return if @em_conns.size >= @limit
181
+ begin
182
+ io = @io.accept_nonblock
183
+ sig = EM.attach_fd(io.fileno, false)
184
+ @em_conns[sig] = Client.new(sig, io)
185
+ rescue Errno::EAGAIN, Errno::ECONNABORTED
186
+ end
187
+ end
188
+ end
189
+
190
+ # runs inside each forked worker, this sits around and waits
191
+ # for connections and doesn't die until the parent dies (or is
192
+ # given a INT, QUIT, or TERM signal)
193
+ def worker_loop(worker)
194
+ init_worker_process(worker)
195
+ m = 0
196
+
197
+ # enable them both, should be non-fatal if not supported
198
+ EM.epoll
199
+ EM.kqueue
200
+ logger.info "EventMachine: epoll=#{EM.epoll?} kqueue=#{EM.kqueue?}"
201
+ EM.run {
202
+ conns = EM.instance_variable_get(:@conns) or
203
+ raise RuntimeError, "EM @conns instance variable not accessible!"
204
+ EM.add_periodic_timer(1) do
205
+ worker.tmp.chmod(m = 0 == m ? 1 : 0)
206
+ unless G.alive
207
+ conns.each_value { |client| Client === client and client.quit }
208
+ EM.stop if conns.empty? && EM.reactor_running?
209
+ end
210
+ end
211
+ LISTENERS.map! do |s|
212
+ EM.watch(s, Server, conns) { |c| c.notify_readable = true }
213
+ end
214
+ }
215
+ end
216
+
217
+ end
218
+ end