rainbows 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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