rainbows 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/GIT-VERSION-GEN +1 -1
  2. data/TODO +15 -0
  3. data/TUNING +14 -0
  4. data/lib/rainbows.rb +7 -0
  5. data/lib/rainbows/app_pool.rb +4 -3
  6. data/lib/rainbows/base.rb +17 -11
  7. data/lib/rainbows/const.rb +5 -1
  8. data/lib/rainbows/dev_fd_response.rb +69 -0
  9. data/lib/rainbows/http_response.rb +1 -0
  10. data/lib/rainbows/http_server.rb +2 -0
  11. data/lib/rainbows/rev.rb +136 -43
  12. data/lib/rainbows/revactor.rb +6 -14
  13. data/lib/rainbows/thread_pool.rb +11 -13
  14. data/lib/rainbows/thread_spawn.rb +4 -4
  15. data/local.mk.sample +9 -1
  16. data/t/GNUmakefile +4 -4
  17. data/t/README +1 -1
  18. data/t/async-response-no-autochunk.ru +24 -0
  19. data/t/async-response.ru +13 -0
  20. data/t/bin/content-md5-put +1 -1
  21. data/t/bin/utee +12 -0
  22. data/t/env.ru +3 -0
  23. data/t/large-file-response.ru +13 -0
  24. data/t/lib-async-response-no-autochunk.sh +6 -0
  25. data/t/lib-async-response.sh +45 -0
  26. data/t/lib-graceful.sh +40 -0
  27. data/t/lib-input-trailer.sh +63 -0
  28. data/t/lib-large-file-response.sh +45 -0
  29. data/t/lib-parser-error.sh +29 -0
  30. data/t/{t3100-revactor-tee-input.sh → lib-rack-input-hammer.sh} +3 -9
  31. data/t/lib-reopen-logs.sh +60 -0
  32. data/t/lib-simple-http.sh +92 -0
  33. data/t/sleep.ru +13 -6
  34. data/t/t0000-basic.sh +1 -36
  35. data/t/t1000-thread-pool-basic.sh +1 -41
  36. data/t/t1002-thread-pool-graceful.sh +1 -36
  37. data/t/t1003-thread-pool-reopen-logs.sh +2 -0
  38. data/t/t1004-thread-pool-async-response.sh +45 -0
  39. data/t/t1005-thread-pool-large-file-response.sh +45 -0
  40. data/t/t1006-thread-pool-async-response-no-autochunk.sh +6 -0
  41. data/t/t1100-thread-pool-rack-input.sh +2 -0
  42. data/t/t1101-thread-pool-input-trailer.sh +2 -0
  43. data/t/t2000-thread-spawn-basic.sh +1 -37
  44. data/t/t2002-thread-spawn-graceful.sh +1 -36
  45. data/t/t2003-thread-spawn-reopen-logs.sh +2 -0
  46. data/t/t2004-thread-spawn-async-response.sh +45 -0
  47. data/t/t2005-thread-spawn-large-file-response.sh +45 -0
  48. data/t/t2006-thread-spawn-async-response-no-autochunk.sh +6 -0
  49. data/t/t2100-thread-spawn-rack-input.sh +2 -0
  50. data/t/t2101-thread-spawn-input-trailer.sh +2 -0
  51. data/t/t3000-revactor-basic.sh +1 -39
  52. data/t/t3002-revactor-graceful.sh +1 -37
  53. data/t/t3003-revactor-reopen-logs.sh +1 -53
  54. data/t/t3004-revactor-async-response.sh +45 -0
  55. data/t/t3005-revactor-large-file-response.sh +2 -0
  56. data/t/t3006-revactor-async-response-no-autochunk.sh +6 -0
  57. data/t/t3100-revactor-rack-input.sh +2 -0
  58. data/t/t3101-revactor-rack-input-trailer.sh +2 -0
  59. data/t/t4000-rev-basic.sh +1 -50
  60. data/t/t4002-rev-graceful.sh +1 -51
  61. data/t/t4003-rev-parser-error.sh +1 -33
  62. data/t/t4003-rev-reopen-logs.sh +2 -0
  63. data/t/t4004-rev-async-response.sh +45 -0
  64. data/t/t4005-rev-large-file-response.sh +2 -0
  65. data/t/t4006-rev-async-response-no-autochunk.sh +6 -0
  66. data/t/t4100-rev-rack-input.sh +1 -43
  67. data/t/t4101-rev-rack-input-trailer.sh +1 -50
  68. data/t/t9000-rack-app-pool.sh +2 -3
  69. data/t/test-lib.sh +80 -18
  70. metadata +39 -4
  71. data/t/t3001-revactor-pipeline.sh +0 -46
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.2.0.GIT
4
+ DEF_VER=v0.3.0.GIT
5
5
 
6
6
  LF='
7
7
  '
data/TODO CHANGED
@@ -3,6 +3,13 @@
3
3
  We're lazy and pick the easy items to do first, then the ones people
4
4
  care about.
5
5
 
6
+ * Improve test suite coverage. We won't waste cycles with puny
7
+ unit tests, only integration tests that exercise externally
8
+ visible parts.
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
+
6
13
  * Rev + Thread - current Rev model with threading, which will give
7
14
  us a streaming (but rewindable) "rack.input".
8
15
 
@@ -10,6 +17,12 @@ care about.
10
17
  (our benevolent dictator doesn't like C++). If we can figure out how
11
18
  to do Rev without Revactor, then this should be pretty easy.
12
19
 
20
+ * EventMachine + catch/throw :async API used in Thin
21
+
22
+ * EventMachine.spawn - should be like Revactor, maybe?
23
+
24
+ * Rev + callcc - current Rev model with callcc (should work with MBARI)
25
+
13
26
  * Fiber support - Revactor already uses these with Ruby 1.9, also not
14
27
  sure how TeeInput can be done with this.
15
28
 
@@ -17,3 +30,5 @@ care about.
17
30
 
18
31
  * Rubinius Actors - should be like Revactor and easily doable once
19
32
  Rubinius gets more mature.
33
+
34
+ * test and improve performance (throughput/latency/memory usage)
data/TUNING CHANGED
@@ -7,6 +7,13 @@ of the time waiting on network activity. Thus memory usage and memory
7
7
  bandwidth for keeping connections open are often limiting factors as
8
8
  well.
9
9
 
10
+ As of October 2009, absolutely ZERO work has been done for performance
11
+ validation and tuning. Furthermore, \Rainbows! is NOT expected to do well on
12
+ traditional benchmarks. Remember that \Rainbows! is only designed for
13
+ applications that sleep and/or trickle network traffic. In the future,
14
+ *may* do well in traditional benchmarks as a side effect, but that will
15
+ never be the primary goal of the project.
16
+
10
17
  == \Rainbows! configuration
11
18
 
12
19
  * Don't set +worker_connections+ too high. It is often better to start
@@ -18,6 +25,13 @@ well.
18
25
  robust against crashes and are more likely to be fairly scheduled by
19
26
  the kernel.
20
27
 
28
+ * If your workers do not seem to be releasing memory to the OS after
29
+ traffic spikes, consider the {mall}[http://bogomips.org/mall/] library
30
+ which allows access to the mallopt(3) function from Ruby. As of
31
+ October 2009 tcmalloc (the default allocator for Ruby Enterprise
32
+ Edition) does not release memory back to the kernel, the best it can
33
+ do is use madvise(2) in an effort to swap out unused pages.
34
+
21
35
  == nginx configuration
22
36
 
23
37
  If you intend to use nginx as a reverse-proxy in front of \Rainbows! to
data/lib/rainbows.rb CHANGED
@@ -3,11 +3,18 @@ require 'unicorn'
3
3
 
4
4
  module Rainbows
5
5
 
6
+ # global vars because class/instance variables are confusing me :<
7
+ # this struct is only accessed inside workers and thus private to each
8
+ G = Struct.new(:cur, :max, :logger, :alive, :app).new
9
+ # G.cur may not be used the network concurrency model
10
+ G.alive = true
11
+
6
12
  require 'rainbows/const'
7
13
  require 'rainbows/http_server'
8
14
  require 'rainbows/http_response'
9
15
  require 'rainbows/base'
10
16
  autoload :AppPool, 'rainbows/app_pool'
17
+ autoload :DevFdResponse, 'rainbows/dev_fd_response'
11
18
 
12
19
  class << self
13
20
 
@@ -39,10 +39,11 @@ 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 Rainbows::Rev concurrency model as that is
42
+ # AppPool has no effect on the Rev concurrency model as that is
43
43
  # single-threaded/single-instance as far as application concurrency goes.
44
- # In other words, +P+ is always +one+ when using \Rev (but not
45
- # \Revactor) regardless of (or even if) this middleware is loaded.
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.
46
47
  #
47
48
  # Since this is Rack middleware, you may load this in your Rack
48
49
  # config.ru file and even use it in servers other than \Rainbows!
data/lib/rainbows/base.rb CHANGED
@@ -8,6 +8,7 @@ module Rainbows
8
8
 
9
9
  include Unicorn
10
10
  include Rainbows::Const
11
+ G = Rainbows::G
11
12
 
12
13
  # write a response without caring if it went out or not for error
13
14
  # messages.
@@ -17,21 +18,28 @@ module Rainbows
17
18
  client.close rescue nil
18
19
  end
19
20
 
20
- # TODO: migrate into Unicorn::HttpServer
21
21
  def listen_loop_error(e)
22
+ G.alive or return
22
23
  logger.error "Unhandled listen loop exception #{e.inspect}."
23
24
  logger.error e.backtrace.join("\n")
24
25
  end
25
26
 
26
27
  def init_worker_process(worker)
27
28
  super(worker)
29
+ G.cur = 0
30
+ G.max = worker_connections
31
+ G.logger = logger
32
+ G.app = app
28
33
 
29
34
  # we're don't use the self-pipe mechanism in the Rainbows! worker
30
35
  # since we don't defer reopening logs
31
36
  HttpServer::SELF_PIPE.each { |x| x.close }.clear
32
37
  trap(:USR1) { reopen_worker_logs(worker.nr) rescue nil }
33
- # closing anything we IO.select on will raise EBADF
34
- trap(:QUIT) { HttpServer::LISTENERS.map! { |s| s.close rescue nil } }
38
+ trap(:QUIT) do
39
+ G.alive = false
40
+ # closing anything we IO.select on will raise EBADF
41
+ HttpServer::LISTENERS.map! { |s| s.close rescue nil }
42
+ end
35
43
  [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
36
44
  logger.info "Rainbows! #@use worker_connections=#@worker_connections"
37
45
  end
@@ -62,7 +70,7 @@ module Rainbows
62
70
  response = app.call(env)
63
71
  end
64
72
 
65
- alive = hp.keepalive? && ! Thread.current[:quit]
73
+ alive = hp.keepalive? && G.alive
66
74
  out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if hp.headers?
67
75
  HttpResponse.write(client, response, out)
68
76
  end while alive and hp.reset.nil? and env.clear
@@ -82,23 +90,21 @@ module Rainbows
82
90
  end
83
91
 
84
92
  def join_threads(threads, worker)
85
- logger.info "Joining threads..."
86
- threads.each { |thr| thr[:quit] = true }
87
- t0 = Time.now
88
- timeleft = timeout * 2.0
93
+ Rainbows::G.alive = false
94
+ expire = Time.now + (timeout * 2.0)
89
95
  m = 0
90
- while (nr = threads.count { |thr| thr.alive? }) > 0 && timeleft > 0
96
+ while (nr = threads.count { |thr| thr.alive? }) > 0
91
97
  threads.each { |thr|
92
98
  worker.tmp.chmod(m = 0 == m ? 1 : 0)
93
99
  thr.join(1)
94
- break if (timeleft -= (Time.now - t0)) < 0
100
+ break if Time.now >= expire
95
101
  }
96
102
  end
97
- logger.info "Done joining threads. #{nr} left running"
98
103
  end
99
104
 
100
105
  def self.included(klass)
101
106
  klass.const_set :LISTENERS, HttpServer::LISTENERS
107
+ klass.const_set :G, Rainbows::G
102
108
  end
103
109
 
104
110
  end
@@ -3,12 +3,16 @@
3
3
  module Rainbows
4
4
 
5
5
  module Const
6
- RAINBOWS_VERSION = '0.2.0'
6
+ RAINBOWS_VERSION = '0.3.0'
7
7
 
8
8
  include Unicorn::Const
9
9
 
10
10
  RACK_DEFAULTS = ::Unicorn::HttpRequest::DEFAULTS.merge({
11
11
  "SERVER_SOFTWARE" => "Rainbows! #{RAINBOWS_VERSION}",
12
+
13
+ # using the Rev model, we'll automatically chunk pipe and socket objects
14
+ # if they're the response body
15
+ 'rainbows.autochunk' => false,
12
16
  })
13
17
 
14
18
  CONN_CLOSE = "Connection: close\r\n"
@@ -0,0 +1,69 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module Rainbows
4
+
5
+ # Rack response middleware wrapping any IO-like object with an
6
+ # OS-level file descriptor associated with it. May also be used to
7
+ # create responses from integer file descriptors or existing +IO+
8
+ # objects. This may be used in conjunction with the #to_path method
9
+ # on servers that support it to pass arbitrary file descriptors into
10
+ # the HTTP response without additional open(2) syscalls
11
+
12
+ class DevFdResponse < Struct.new(:app, :to_io, :to_path)
13
+ include Rack::Utils
14
+
15
+ # Rack middleware entry point, we'll just pass through responses
16
+ # unless they respond to +to_io+ or +to_path+
17
+ def call(env)
18
+ status, headers, body = response = app.call(env)
19
+
20
+ # totally uninteresting to us if there's no body
21
+ return response if STATUS_WITH_NO_ENTITY_BODY.include?(status)
22
+
23
+ io = body.to_io if body.respond_to?(:to_io)
24
+ io ||= File.open(body.to_path, 'rb') if body.respond_to?(:to_path)
25
+ return response if io.nil?
26
+
27
+ headers = HeaderHash.new(headers)
28
+ st = io.stat
29
+ if st.file?
30
+ headers['Content-Length'] ||= st.size.to_s
31
+ headers.delete('Transfer-Encoding')
32
+ elsif st.pipe? || st.socket? # epoll-able things
33
+ if env['rainbows.autochunk']
34
+ headers['Transfer-Encoding'] = 'chunked'
35
+ headers.delete('Content-Length')
36
+ else
37
+ headers['X-Rainbows-Autochunk'] = 'no'
38
+ end
39
+ else # unlikely, char/block device file, directory, ...
40
+ return response
41
+ end
42
+ resp = dup # be reentrant here
43
+ resp.to_path = "/dev/fd/#{io.fileno}"
44
+ resp.to_io = io
45
+ [ status, headers.to_hash, resp ]
46
+ end
47
+
48
+ # called by the webserver or other middlewares if they can't
49
+ # handle #to_path
50
+ def each(&block)
51
+ to_io.each(&block)
52
+ end
53
+
54
+ # remain Rack::Lint-compatible for people with wonky systems :P
55
+ unless File.exist?("/dev/fd/0")
56
+ alias to_path_orig to_path
57
+ undef_method :to_path
58
+ end
59
+
60
+ # called by the web server after #each
61
+ def close
62
+ begin
63
+ to_io.close if to_io.respond_to?(:close)
64
+ rescue IOError # could've been IO::new()'ed and closed
65
+ end
66
+ end
67
+
68
+ end # class
69
+ end
@@ -13,6 +13,7 @@ module Rainbows
13
13
  status = CODES[status.to_i] || status
14
14
 
15
15
  headers.each do |key, value|
16
+ next if %r{\AX-Rainbows-}i =~ key
16
17
  next if SKIP.include?(key.downcase)
17
18
  if value =~ /\n/
18
19
  out.concat(value.split(/\n/).map! { |v| "#{key}: #{v}\r\n" })
@@ -32,6 +32,8 @@ module Rainbows
32
32
  raise ArgumentError, "concurrency model #{model.inspect} not supported"
33
33
  extend(mod)
34
34
  Const::RACK_DEFAULTS['rainbows.model'] = @use = model
35
+ Const::RACK_DEFAULTS['rack.multithread'] = !!(/Thread/ =~ model.to_s)
36
+ Const::RACK_DEFAULTS['rainbows.autochunk'] = (model.to_s == "Rev")
35
37
  end
36
38
 
37
39
  def worker_connections(*args)
data/lib/rainbows/rev.rb CHANGED
@@ -12,8 +12,10 @@ module Rainbows
12
12
  # thousands of simultaneous client connections, but with only a
13
13
  # single-threaded app dispatch. It is suited for slow clients and
14
14
  # fast applications (applications that do not have slow network
15
- # dependencies). It does not require your Rack application to
16
- # be reentrant or thread-safe.
15
+ # dependencies) or applications that use DevFdResponse for deferrable
16
+ # response bodies. It does not require your Rack application to be
17
+ # thread-safe, reentrancy is only required for the DevFdResponse body
18
+ # generator.
17
19
  #
18
20
  # Compatibility: Whatever \Rev itself supports, currently Ruby
19
21
  # 1.8/1.9.
@@ -22,39 +24,41 @@ module Rainbows
22
24
  # allows the Rack application to process data as it arrives. This
23
25
  # means "rack.input" will be fully buffered in memory or to a
24
26
  # temporary file before the application is entered.
25
- #
26
- # Caveats: this model can buffer all output for slow clients in
27
- # memory. This can be a problem if your application generates large
28
- # responses (including static files served with Rack) as it will cause
29
- # the memory footprint of your process to explode. If your workers
30
- # seem to be eating a lot of memory from this, consider the
31
- # {mall}[http://bogomips.org/mall/] library which allows access to the
32
- # mallopt(3) function from Ruby.
33
27
 
34
28
  module Rev
35
29
 
36
- # global vars because class/instance variables are confusing me :<
37
- # this struct is only accessed inside workers and thus private to each
38
- G = Struct.new(:nr, :max, :logger, :alive, :app).new
39
-
40
30
  include Base
41
31
 
42
32
  class Client < ::Rev::IO
43
33
  include Unicorn
44
34
  include Rainbows::Const
45
- G = Rainbows::Rev::G
35
+ G = Rainbows::G
36
+
37
+ # queued, optional response bodies, it should only be unpollable "fast"
38
+ # devices where read(2) is uninterruptable. Unfortunately, NFS and ilk
39
+ # are also part of this. We'll also stick DeferredResponse bodies in
40
+ # here to prevent connections from being closed on us.
41
+ attr_reader :deferred_bodies
46
42
 
47
43
  def initialize(io)
48
- G.nr += 1
44
+ G.cur += 1
49
45
  super(io)
50
46
  @remote_addr = ::TCPSocket === io ? io.peeraddr.last : LOCALHOST
51
47
  @env = {}
52
48
  @hp = HttpParser.new
53
49
  @state = :headers # [ :body [ :trailers ] ] :app_call :close
54
50
  @buf = ""
51
+ @deferred_bodies = [] # for (fast) regular files only
52
+ end
53
+
54
+ # graceful exit, like SIGQUIT
55
+ def quit
56
+ @deferred_bodies.clear
57
+ @state = :close
55
58
  end
56
59
 
57
60
  def handle_error(e)
61
+ quit
58
62
  msg = case e
59
63
  when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
60
64
  ERROR_500_RESPONSE
@@ -66,33 +70,51 @@ module Rainbows
66
70
  ERROR_500_RESPONSE
67
71
  end
68
72
  write(msg)
69
- ensure
70
- @state = :close
71
73
  end
72
74
 
73
75
  def app_call
74
- @input.rewind
75
- @env[RACK_INPUT] = @input
76
- @env[REMOTE_ADDR] = @remote_addr
77
- response = G.app.call(@env.update(RACK_DEFAULTS))
78
- alive = @hp.keepalive? && G.alive
79
- out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
80
- HttpResponse.write(self, response, out)
81
- if alive
82
- @env.clear
83
- @hp.reset
84
- @state = :headers
85
- else
86
- @state = :close
87
- end
76
+ begin
77
+ (@env[RACK_INPUT] = @input).rewind
78
+ alive = @hp.keepalive?
79
+ @env[REMOTE_ADDR] = @remote_addr
80
+ response = G.app.call(@env.update(RACK_DEFAULTS))
81
+ alive &&= G.alive
82
+ out = [ alive ? CONN_ALIVE : CONN_CLOSE ] if @hp.headers?
83
+
84
+ DeferredResponse.write(self, response, out)
85
+ if alive
86
+ @env.clear
87
+ @hp.reset
88
+ @state = :headers
89
+ # keepalive requests are always body-less, so @input is unchanged
90
+ @hp.headers(@env, @buf) and next
91
+ else
92
+ @state = :close
93
+ end
94
+ return
95
+ end while true
88
96
  end
89
97
 
90
98
  def on_write_complete
91
- :close == @state and close
99
+ if body = @deferred_bodies.first
100
+ return if DeferredResponse === body
101
+ begin
102
+ begin
103
+ write(body.sysread(CHUNK_SIZE))
104
+ rescue EOFError # expected at file EOF
105
+ @deferred_bodies.shift
106
+ body.close
107
+ end
108
+ rescue Object => e
109
+ handle_error(e)
110
+ end
111
+ else
112
+ close if :close == @state
113
+ end
92
114
  end
93
115
 
94
116
  def on_close
95
- G.nr -= 1
117
+ G.cur -= 1
96
118
  end
97
119
 
98
120
  def tmpio
@@ -144,10 +166,10 @@ module Rainbows
144
166
  end
145
167
 
146
168
  class Server < ::Rev::IO
147
- G = Rainbows::Rev::G
169
+ G = Rainbows::G
148
170
 
149
171
  def on_readable
150
- return if G.nr >= G.max
172
+ return if G.cur >= G.max
151
173
  begin
152
174
  Client.new(@_io.accept_nonblock).attach(::Rev::Loop.default)
153
175
  rescue Errno::EAGAIN, Errno::ECONNBORTED
@@ -156,18 +178,89 @@ module Rainbows
156
178
 
157
179
  end
158
180
 
181
+ class DeferredResponse < ::Rev::IO
182
+ include Unicorn
183
+ include Rainbows::Const
184
+ G = Rainbows::G
185
+
186
+ def self.defer!(client, response, out)
187
+ body = response.last
188
+ headers = Rack::Utils::HeaderHash.new(response[1])
189
+
190
+ # to_io is not part of the Rack spec, but make an exception
191
+ # here since we can't get here without checking to_path first
192
+ io = body.to_io if body.respond_to?(:to_io)
193
+ io ||= ::IO.new($1.to_i) if body.to_path =~ %r{\A/dev/fd/(\d+)\z}
194
+ io ||= File.open(File.expand_path(body.to_path), 'rb')
195
+ st = io.stat
196
+
197
+ if st.socket? || st.pipe?
198
+ do_chunk = !!(headers['Transfer-Encoding'] =~ %r{\Achunked\z}i)
199
+ do_chunk = false if headers.delete('X-Rainbows-Autochunk') == 'no'
200
+ # too tricky to support keepalive/pipelining when a response can
201
+ # take an indeterminate amount of time here.
202
+ out[0] = CONN_CLOSE
203
+
204
+ io = new(io, client, do_chunk, body).attach(::Rev::Loop.default)
205
+ elsif st.file?
206
+ headers.delete('Transfer-Encoding')
207
+ headers['Content-Length'] ||= st.size.to_s
208
+ else # char/block device, directory, whatever... nobody cares
209
+ return response
210
+ end
211
+ client.deferred_bodies << io
212
+ [ response.first, headers.to_hash, [] ]
213
+ end
214
+
215
+ def self.write(client, response, out)
216
+ response.last.respond_to?(:to_path) and
217
+ response = defer!(client, response, out)
218
+ HttpResponse.write(client, response, out)
219
+ end
220
+
221
+ def initialize(io, client, do_chunk, body)
222
+ super(io)
223
+ @client, @do_chunk, @body = client, do_chunk, body
224
+ end
225
+
226
+ def on_read(data)
227
+ @do_chunk and @client.write(sprintf("%x\r\n", data.size))
228
+ @client.write(data)
229
+ @do_chunk and @client.write("\r\n")
230
+ end
231
+
232
+ def on_close
233
+ @do_chunk and @client.write("0\r\n\r\n")
234
+ @client.quit
235
+ @body.respond_to?(:close) and @body.close
236
+ end
237
+ end
238
+
239
+ # This timer handles the fchmod heartbeat to prevent our master
240
+ # from killing us.
241
+ class Heartbeat < ::Rev::TimerWatcher
242
+ G = Rainbows::G
243
+
244
+ def initialize(tmp)
245
+ @m, @tmp = 0, tmp
246
+ super(1, true)
247
+ end
248
+
249
+ def on_timer
250
+ @tmp.chmod(@m = 0 == @m ? 1 : 0)
251
+ exit if (! G.alive && G.cur <= 0)
252
+ end
253
+ end
254
+
159
255
  # runs inside each forked worker, this sits around and waits
160
256
  # for connections and doesn't die until the parent dies (or is
161
257
  # given a INT, QUIT, or TERM signal)
162
258
  def worker_loop(worker)
163
259
  init_worker_process(worker)
164
- G.nr = 0
165
- G.max = worker_connections
166
- G.alive = true
167
- G.logger = logger
168
- G.app = app
169
- LISTENERS.map! { |s| Server.new(s).attach(::Rev::Loop.default) }
170
- ::Rev::Loop.default.run
260
+ rloop = ::Rev::Loop.default
261
+ Heartbeat.new(worker.tmp).attach(rloop)
262
+ LISTENERS.map! { |s| Server.new(s).attach(rloop) }
263
+ rloop.run
171
264
  end
172
265
 
173
266
  end