diff options
Diffstat (limited to 'lib/unicorn')
-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 |
5 files changed, 62 insertions, 121 deletions
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 1945172..52d1775 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.1".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 |