diff options
-rw-r--r-- | Rakefile | 1 | ||||
-rw-r--r-- | lib/unicorn.rb | 106 | ||||
-rw-r--r-- | lib/unicorn/app/exec_cgi.rb | 12 | ||||
-rw-r--r-- | lib/unicorn/app/old_rails/static.rb | 10 | ||||
-rw-r--r-- | lib/unicorn/const.rb | 55 | ||||
-rw-r--r-- | lib/unicorn/http_request.rb | 78 | ||||
-rw-r--r-- | lib/unicorn/http_response.rb | 28 | ||||
-rw-r--r-- | test/benchmark/big_request.rb | 6 | ||||
-rw-r--r-- | test/benchmark/request.rb | 3 | ||||
-rw-r--r-- | test/benchmark/response.rb | 1 | ||||
-rw-r--r-- | test/tools/trickletest.rb | 45 | ||||
-rw-r--r-- | test/unit/test_request.rb | 5 | ||||
-rw-r--r-- | test/unit/test_signals.rb | 83 |
13 files changed, 203 insertions, 230 deletions
@@ -16,6 +16,7 @@ Echoe.new("unicorn") do |p| p.ignore_pattern = /^(pkg|site|projects|doc|log)|CVS|\.log/ p.need_tar_gz = false p.need_tgz = true + p.dependencies = [ 'rack' ] p.extension_pattern = ["ext/**/extconf.rb"] diff --git a/lib/unicorn.rb b/lib/unicorn.rb index f5c1c8c..4a4e2e1 100644 --- a/lib/unicorn.rb +++ b/lib/unicorn.rb @@ -377,7 +377,9 @@ module Unicorn # worker. def murder_lazy_workers WORKERS.each_pair do |pid, worker| - Time.now - worker.tempfile.ctime <= @timeout and next + stat = worker.tempfile.stat + stat.mode == 0100000 and next + Time.now - stat.ctime <= @timeout and next logger.error "worker=#{worker.nr} PID:#{pid} is too old, killing" kill_worker(:KILL, pid) # take no prisoners for @timeout violations worker.tempfile.close rescue nil @@ -414,8 +416,6 @@ module Unicorn # once a client is accepted, it is processed in its entirety here # in 3 easy steps: read request, call app, write app response def process_client(client) - # one syscall less than "client.nonblock = false": - client.fcntl(Fcntl::F_SETFL, File::RDWR) HttpResponse.write(client, @app.call(@request.read(client))) # if we get any error, try to write something back to the client # assuming we haven't closed the socket, but don't get hung up @@ -423,20 +423,15 @@ module Unicorn # the socket is closed at the end of this function rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil + client.close rescue nil rescue HttpParserError # try to tell the client they're bad client.write_nonblock(Const::ERROR_400_RESPONSE) rescue nil + client.close rescue nil rescue Object => e client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil + client.close rescue nil logger.error "Read error: #{e.inspect}" logger.error e.backtrace.join("\n") - ensure - begin - client.closed? or client.close - rescue Object => e - logger.error "Client error: #{e.inspect}" - logger.error e.backtrace.join("\n") - end - @request.reset end # gets rid of stuff the worker has no business keeping track of @@ -475,16 +470,18 @@ module Unicorn nr = 0 # this becomes negative if we need to reopen logs alive = worker.tempfile # tempfile is our lifeline to the master process ready = LISTENERS - client = nil + t = ti = 0 # closing anything we IO.select on will raise EBADF trap(:USR1) { nr = -65536; SELF_PIPE.first.close rescue nil } trap(:QUIT) { alive = nil; LISTENERS.each { |s| s.close rescue nil } } - [:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown + [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown @logger.info "worker=#{worker.nr} ready" - while alive - reopen_worker_logs(worker.nr) if nr < 0 + begin + nr < 0 and reopen_worker_logs(worker.nr) + nr = 0 + # we're a goner in @timeout seconds anyways if alive.chmod # breaks, so don't trap the exception. Using fchmod() since # futimes() is not available in base Ruby and I very strongly @@ -493,55 +490,41 @@ module Unicorn # changes with chmod doesn't update ctime on all filesystems; so # we change our counter each and every time (after process_client # and before IO.select). - alive.chmod(nr = 0) + t == (ti = Time.now.to_i) or alive.chmod(t = ti) + + ready.each do |sock| + begin + process_client(sock.accept_nonblock) + nr += 1 + t == (ti = Time.now.to_i) or alive.chmod(t = ti) + rescue Errno::EAGAIN, Errno::ECONNABORTED + end + break if nr < 0 + end + + # make the following bet: if we accepted clients this round, + # we're probably reasonably busy, so avoid calling select() + # and do a speculative accept_nonblock on every listener + # before we sleep again in select(). + redo unless nr == 0 # (nr < 0) => reopen logs + master_pid == Process.ppid or return + alive.chmod(t = 0) begin - ready.each do |sock| - begin - client = begin - sock.accept_nonblock - rescue Errno::EAGAIN - next - end - process_client(client) - rescue Errno::ECONNABORTED - # client closed the socket even before accept - client.close rescue nil - ensure - alive.chmod(nr += 1) if client - break if nr < 0 - end - end - client = nil - - # make the following bet: if we accepted clients this round, - # we're probably reasonably busy, so avoid calling select() - # and do a speculative accept_nonblock on every listener - # before we sleep again in select(). - if nr != 0 # (nr < 0) => reopen logs - ready = LISTENERS - else - master_pid == Process.ppid or exit(0) - alive.chmod(nr += 1) - begin - # timeout used so we can detect parent death: - ret = IO.select(LISTENERS, nil, SELF_PIPE, @timeout) or next - ready = ret.first - rescue Errno::EINTR - ready = LISTENERS - rescue Errno::EBADF => e - nr < 0 or exit(alive ? 1 : 0) - end - end - rescue SignalException, SystemExit => e - raise e - rescue Object => e - if alive - logger.error "Unhandled listen loop exception #{e.inspect}." - logger.error e.backtrace.join("\n") - end + # timeout used so we can detect parent death: + ret = IO.select(LISTENERS, nil, SELF_PIPE, @timeout) or redo + ready = ret.first + rescue Errno::EINTR + ready = LISTENERS + rescue Errno::EBADF + nr < 0 or return end - end + rescue Object => e + if alive + logger.error "Unhandled listen loop exception #{e.inspect}." + logger.error e.backtrace.join("\n") + end + end while alive end # delivers a signal to a worker and fails gracefully if the worker @@ -585,6 +568,7 @@ module Unicorn @config.reload @config.commit!(self) kill_each_worker(:QUIT) + Unicorn::Util.reopen_logs logger.info "done reloading config_file=#{@config.config_file}" rescue Object => e logger.error "error reloading config_file=#{@config.config_file}: " \ diff --git a/lib/unicorn/app/exec_cgi.rb b/lib/unicorn/app/exec_cgi.rb index d98b3e4..8f81d78 100644 --- a/lib/unicorn/app/exec_cgi.rb +++ b/lib/unicorn/app/exec_cgi.rb @@ -95,10 +95,15 @@ module Unicorn::App # Allows +out+ to be used as a Rack body. def out.each sysseek(@unicorn_app_exec_cgi_offset) + + # don't use a preallocated buffer for sysread since we can't + # guarantee an actual socket is consuming the yielded string + # (or if somebody is pushing to an array for eventual concatenation begin - loop { yield(sysread(CHUNK_SIZE)) } + yield(sysread(CHUNK_SIZE)) rescue EOFError - end + return + end while true end prev = nil @@ -126,7 +131,8 @@ module Unicorn::App tmp.binmode # Rack::Lint::InputWrapper doesn't allow sysread :( - while buf = inp.read(CHUNK_SIZE) + buf = '' + while inp.read(CHUNK_SIZE, buf) tmp.syswrite(buf) end tmp.sysseek(0) diff --git a/lib/unicorn/app/old_rails/static.rb b/lib/unicorn/app/old_rails/static.rb index 7ec6b6d..17c007c 100644 --- a/lib/unicorn/app/old_rails/static.rb +++ b/lib/unicorn/app/old_rails/static.rb @@ -22,6 +22,8 @@ require 'rack/file' class Unicorn::App::OldRails::Static FILE_METHODS = { 'GET' => true, 'HEAD' => true }.freeze REQUEST_METHOD = 'REQUEST_METHOD'.freeze + REQUEST_URI = 'REQUEST_URI'.freeze + PATH_INFO = 'PATH_INFO'.freeze def initialize(app) @app = app @@ -34,10 +36,10 @@ class Unicorn::App::OldRails::Static FILE_METHODS.include?(env[REQUEST_METHOD]) or return @app.call(env) # first try the path as-is - path_info = env[Unicorn::Const::PATH_INFO].chomp("/") + path_info = env[PATH_INFO].chomp("/") if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}") # File exists as-is so serve it up - env[Unicorn::Const::PATH_INFO] = path_info + env[PATH_INFO] = path_info return @file_server.call(env) end @@ -45,11 +47,11 @@ class Unicorn::App::OldRails::Static # grab the semi-colon REST operator used by old versions of Rails # this is the reason we didn't just copy the new Rails::Rack::Static - env[Unicorn::Const::REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/ + env[REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/ path_info << "#$1#{ActionController::Base.page_cache_extension}" if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}") - env[Unicorn::Const::PATH_INFO] = path_info + env[PATH_INFO] = path_info return @file_server.call(env) end diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index 3bd4808..241c52e 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -1,64 +1,13 @@ -module Unicorn +require 'rack/utils' - # Every standard HTTP code mapped to the appropriate message. These are - # used so frequently that they are placed directly in Unicorn for easy - # access rather than Unicorn::Const itself. - HTTP_STATUS_CODES = { - 100 => 'Continue', - 101 => 'Switching Protocols', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Moved Temporarily', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', - 415 => 'Unsupported Media Type', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported' - }.inject({}) { |hash,(code,msg)| - hash[code] = "#{code} #{msg}" - hash - } +module Unicorn # Frequently used constants when constructing requests or responses. Many times # the constant just refers to a string with the same contents. Using these constants # gave about a 3% to 10% performance improvement over using the strings directly. # Symbols did not really improve things much compared to constants. module Const - # This is the part of the path after the SCRIPT_NAME. - PATH_INFO="PATH_INFO".freeze - - # The original URI requested by the client. - REQUEST_URI='REQUEST_URI'.freeze - REQUEST_PATH='REQUEST_PATH'.freeze - UNICORN_VERSION="0.7.0".freeze DEFAULT_HOST = "0.0.0.0".freeze # default TCP listen host address diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index 424a54f..368305f 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -12,19 +12,22 @@ module Unicorn # class HttpRequest - # default parameters we merge into the request env for Rack handlers - DEF_PARAMS = { - "rack.errors" => $stderr, - "rack.multiprocess" => true, - "rack.multithread" => false, - "rack.run_once" => false, - "rack.version" => [1, 0].freeze, - "SCRIPT_NAME" => "".freeze, - - # this is not in the Rack spec, but some apps may rely on it - "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze - }.freeze - + # default parameters we merge into the request env for Rack handlers + DEFAULTS = { + "rack.errors" => $stderr, + "rack.multiprocess" => true, + "rack.multithread" => false, + "rack.run_once" => false, + "rack.version" => [1, 0].freeze, + "SCRIPT_NAME" => "".freeze, + + # this is not in the Rack spec, but some apps may rely on it + "SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze + } + + # Optimize for the common case where there's no request body + # (GET/HEAD) requests. + NULL_IO = StringIO.new LOCALHOST = '127.0.0.1'.freeze # Being explicitly single-threaded, we have certain advantages in @@ -35,14 +38,6 @@ module Unicorn def initialize(logger) @logger = logger - reset - end - - def reset - PARAMS[Const::RACK_INPUT].close rescue nil - PARAMS[Const::RACK_INPUT].close! rescue nil - PARSER.reset - PARAMS.clear end # Does the majority of the IO processing. It has been written in @@ -59,6 +54,14 @@ module Unicorn # This does minimal exception trapping and it is up to the caller # to handle any socket errors (e.g. user aborted upload). def read(socket) + # reset the parser + unless NULL_IO == (input = PARAMS[Const::RACK_INPUT]) # unlikely + input.close rescue nil + input.close! rescue nil + end + PARAMS.clear + PARSER.reset + # From http://www.ietf.org/rfc/rfc3875: # "Script authors should be aware that the REMOTE_ADDR and # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9) @@ -70,15 +73,15 @@ module Unicorn TCPSocket === socket ? socket.peeraddr.last : LOCALHOST # short circuit the common case with small GET requests first - PARSER.execute(PARAMS, read_socket(socket)) and + PARSER.execute(PARAMS, socket.readpartial(Const::CHUNK_SIZE, BUFFER)) and return handle_body(socket) - data = BUFFER.dup # read_socket will clobber BUFFER + data = BUFFER.dup # socket.readpartial will clobber BUFFER # Parser is not done, queue up more data to read and continue parsing # an Exception thrown from the PARSER will throw us out of the loop begin - data << read_socket(socket) + data << socket.readpartial(Const::CHUNK_SIZE, BUFFER) PARSER.execute(PARAMS, data) and return handle_body(socket) end while true rescue HttpParserError => e @@ -99,8 +102,8 @@ module Unicorn content_length = PARAMS[Const::CONTENT_LENGTH].to_i if content_length == 0 # short circuit the common case - PARAMS[Const::RACK_INPUT] = StringIO.new - return PARAMS.update(DEF_PARAMS) + PARAMS[Const::RACK_INPUT] = NULL_IO.closed? ? NULL_IO.reopen : NULL_IO + return PARAMS.update(DEFAULTS) end # must read more data to complete body @@ -110,21 +113,19 @@ module Unicorn StringIO.new : Tempfile.new('unicorn') body.binmode - body.sync = true - body.syswrite(http_body) + body.write(http_body) # Some clients (like FF1.0) report 0 for body and then send a body. # This will probably truncate them but at least the request goes through # usually. read_body(socket, remain, body) if remain > 0 body.rewind - body.sysseek(0) if body.respond_to?(:sysseek) # in case read_body overread because the client tried to pipeline # another request, we'll truncate it. Again, we don't do pipelining # or keepalive body.truncate(content_length) - PARAMS.update(DEF_PARAMS) + PARAMS.update(DEFAULTS) end # Does the heavy lifting of properly reading the larger body @@ -133,10 +134,10 @@ module Unicorn # of the body that has been read to be in the PARAMS['rack.input'] # already. It will return true if successful and false if not. def read_body(socket, remain, body) - while remain > 0 - # writes always write the requested amount on a POSIX filesystem - remain -= body.syswrite(read_socket(socket)) - end + begin + # write always writes the requested amount on a POSIX filesystem + remain -= body.write(socket.readpartial(Const::CHUNK_SIZE, BUFFER)) + end while remain > 0 rescue Object => e @logger.error "Error reading HTTP body: #{e.inspect}" @@ -147,14 +148,5 @@ module Unicorn raise e end - # read(2) on "slow" devices like sockets can be interrupted by signals - def read_socket(socket) - begin - socket.sysread(Const::CHUNK_SIZE, BUFFER) - rescue Errno::EINTR - retry - end - end - end end diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index f79e856..15df3f6 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -21,6 +21,12 @@ module Unicorn class HttpResponse + # Every standard HTTP code mapped to the appropriate message. + CODES = Rack::Utils::HTTP_STATUS_CODES.inject({}) { |hash,(code,msg)| + hash[code] = "#{code} #{msg}" + hash + } + # Rack does not set/require a Date: header. We always override the # Connection: and Date: headers no matter what (if anything) our # Rack application sent us. @@ -31,7 +37,7 @@ module Unicorn # writes the rack_response to socket as an HTTP response def self.write(socket, rack_response) status, headers, body = rack_response - status = HTTP_STATUS_CODES[status.to_i] + status = CODES[status.to_i] OUT.clear # Don't bother enforcing duplicate supression, it's a Hash most of @@ -49,30 +55,16 @@ module Unicorn # so don't worry or care about them. # Date is required by HTTP/1.1 as long as our clock can be trusted. # Some broken clients require a "Status" header so we accomodate them - socket_write(socket, - "HTTP/1.1 #{status}\r\n" \ + socket.write("HTTP/1.1 #{status}\r\n" \ "Date: #{Time.now.httpdate}\r\n" \ "Status: #{status}\r\n" \ "Connection: close\r\n" \ "#{OUT.join(EMPTY)}\r\n") - body.each { |chunk| socket_write(socket, chunk) } - socket.close # uncorks the socket immediately + body.each { |chunk| socket.write(chunk) } + socket.close # flushes and uncorks the socket immediately ensure body.respond_to?(:close) and body.close rescue nil end - private - - # write(2) can return short on slow devices like sockets as well - # as fail with EINTR if a signal was caught. - def self.socket_write(socket, buffer) - begin - written = socket.syswrite(buffer) - return written if written == buffer.length - buffer = buffer[written..-1] - rescue Errno::EINTR - end while true - end - end end diff --git a/test/benchmark/big_request.rb b/test/benchmark/big_request.rb index 5f2111b..ee42d41 100644 --- a/test/benchmark/big_request.rb +++ b/test/benchmark/big_request.rb @@ -8,7 +8,11 @@ length = bs * count slice = (' ' * bs).freeze big = Tempfile.new('') -def big.unicorn_peeraddr; '127.0.0.1'; end + +def big.unicorn_peeraddr # old versions of Unicorn used this + '127.0.0.1' +end + big.syswrite( "PUT /hello/world/puturl?abcd=efg&hi#anchor HTTP/1.0\r\n" \ "Host: localhost\r\n" \ diff --git a/test/benchmark/request.rb b/test/benchmark/request.rb index 67266cb..1b2d280 100644 --- a/test/benchmark/request.rb +++ b/test/benchmark/request.rb @@ -10,6 +10,9 @@ class TestClient buf.replace(@response) end + alias readpartial sysread + + # old versions of Unicorn used this def unicorn_peeraddr '127.0.0.1' end diff --git a/test/benchmark/response.rb b/test/benchmark/response.rb index 0ff0ac2..cb7397b 100644 --- a/test/benchmark/response.rb +++ b/test/benchmark/response.rb @@ -3,6 +3,7 @@ require 'unicorn' class NullWriter def syswrite(buf); buf.size; end + alias write syswrite def close; end end diff --git a/test/tools/trickletest.rb b/test/tools/trickletest.rb deleted file mode 100644 index e19ed71..0000000 --- a/test/tools/trickletest.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'socket' -require 'stringio' - -def do_test(st, chunk) - s = TCPSocket.new('127.0.0.1',ARGV[0].to_i); - req = StringIO.new(st) - nout = 0 - randstop = rand(st.length / 10) - STDERR.puts "stopping after: #{randstop}" - - begin - while data = req.read(chunk) - nout += s.write(data) - s.flush - sleep 0.1 - if nout > randstop - STDERR.puts "BANG! after #{nout} bytes." - break - end - end - rescue Object => e - STDERR.puts "ERROR: #{e}" - ensure - s.close - end -end - -content = "-" * (1024 * 240) -st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}" - -puts "length: #{content.length}" - -threads = [] -ARGV[1].to_i.times do - t = Thread.new do - size = 100 - puts ">>>> #{size} sized chunks" - do_test(st, size) - end - - t.abort_on_exception = true - threads << t -end - -threads.each {|t| t.join} diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb index 060da24..0bfff7d 100644 --- a/test/unit/test_request.rb +++ b/test/unit/test_request.rb @@ -14,7 +14,9 @@ include Unicorn class RequestTest < Test::Unit::TestCase - class MockRequest < StringIO; end + class MockRequest < StringIO + alias_method :readpartial, :sysread + end def setup @request = HttpRequest.new(Logger.new($stderr)) @@ -75,7 +77,6 @@ class RequestTest < Test::Unit::TestCase client = MockRequest.new("GET #{abs_uri} HTTP/1.1\r\n" \ "Host: foo\r\n\r\n") assert_raises(HttpParserError) { @request.read(client) } - @request.reset end end diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb index bedce01..ef66ed6 100644 --- a/test/unit/test_signals.rb +++ b/test/unit/test_signals.rb @@ -37,6 +37,87 @@ class SignalsTest < Test::Unit::TestCase @server = nil end + def test_worker_dies_on_dead_master + pid = fork { + app = lambda { |env| [ 200, {'X-Pid' => "#$$" }, [] ] } + opts = @server_opts.merge(:timeout => 3) + redirect_test_io { HttpServer.new(app, opts).start.join } + } + child = sock = buf = t0 = nil + assert_nothing_raised do + wait_workers_ready("test_stderr.#{pid}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + buf = sock.readpartial(4096) + sock.close + buf =~ /\bX-Pid: (\d+)\b/ or raise Exception + child = $1.to_i + wait_master_ready("test_stderr.#{pid}.log") + Process.kill(:KILL, pid) + Process.waitpid(pid) + t0 = Time.now + end + assert child + assert t0 + assert_raises(Errno::ESRCH) { loop { Process.kill(0, child); sleep 0.2 } } + assert((Time.now - t0) < 60) + end + + def test_sleepy_kill + rd, wr = IO.pipe + pid = fork { + rd.close + app = lambda { |env| wr.syswrite('.'); sleep; [ 200, {}, [] ] } + redirect_test_io { HttpServer.new(app, @server_opts).start.join } + } + sock = buf = nil + wr.close + assert_nothing_raised do + wait_workers_ready("test_stderr.#{pid}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + buf = rd.readpartial(1) + wait_master_ready("test_stderr.#{pid}.log") + Process.kill(:INT, pid) + Process.waitpid(pid) + end + assert_equal '.', buf + buf = nil + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) do + buf = sock.sysread(4096) + end + assert_nil buf + ensure + end + + def test_timeout_slow_response + pid = fork { + app = lambda { |env| sleep } + opts = @server_opts.merge(:timeout => 3) + redirect_test_io { HttpServer.new(app, opts).start.join } + } + t0 = Time.now + sock = nil + assert_nothing_raised do + wait_workers_ready("test_stderr.#{pid}.log", 1) + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + end + + buf = nil + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) do + buf = sock.sysread(4096) + end + diff = Time.now - t0 + assert_nil buf + assert diff > 1.0, "diff was #{diff.inspect}" + assert diff < 60.0 + ensure + Process.kill(:QUIT, pid) rescue nil + end + def test_response_write app = lambda { |env| [ 200, { 'Content-Type' => 'text/plain', 'X-Pid' => Process.pid.to_s }, @@ -45,6 +126,7 @@ class SignalsTest < Test::Unit::TestCase redirect_test_io { @server = HttpServer.new(app, @server_opts).start } sock = nil assert_nothing_raised do + wait_workers_ready("test_stderr.#{$$}.log", 1) sock = TCPSocket.new('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") end @@ -82,6 +164,7 @@ class SignalsTest < Test::Unit::TestCase pid = nil assert_nothing_raised do + wait_workers_ready("test_stderr.#{$$}.log", 1) sock = TCPSocket.new('127.0.0.1', @port) sock.syswrite("GET / HTTP/1.0\r\n\r\n") pid = sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i |