rainbows 0.2.0 → 0.3.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 (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