From e21939d776673b2f8887adf7a5c64812b7d2e98e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 30 Dec 2010 08:33:15 +0000 Subject: globally refactor Range handling for responses Rack::Utils::HeaderHash is still very expensive in Rack 1.2, especially for simple things that we want to run as fast as possible with minimal interference. HeaderHash is unnecessary for most requests that do not send Content-Range in responses. --- lib/rainbows/response.rb | 197 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 158 insertions(+), 39 deletions(-) (limited to 'lib/rainbows/response.rb') diff --git a/lib/rainbows/response.rb b/lib/rainbows/response.rb index ca381b8..c0d0740 100644 --- a/lib/rainbows/response.rb +++ b/lib/rainbows/response.rb @@ -3,60 +3,179 @@ require 'time' # for Time#httpdate module Rainbows::Response - autoload :Body, 'rainbows/response/body' - autoload :Range, 'rainbows/response/range' - + CRLF = Unicorn::HttpResponse::CRLF CODES = Unicorn::HttpResponse::CODES - CRLF = "\r\n" + Close = "close" + KeepAlive = "keep-alive" + + # private file class for IO objects opened by Rainbows! itself (and not + # the app or middleware) + class F < File; end - # freeze headers we may set as hash keys for a small speedup - CONNECTION = "Connection".freeze - CLOSE = "close" - KEEP_ALIVE = "keep-alive" - HH = Rack::Utils::HeaderHash + # called after forking + def self.setup(klass) + Kgio.accept_class = Rainbows::Client + 0 == Rainbows::G.kato and Rainbows::HttpParser.keepalive_requests = 0 + end - def response_header(status, headers) + def write_headers(status, headers, alive) + @hp.headers? or return status = CODES[status.to_i] || status - rv = "HTTP/1.1 #{status}\r\n" \ - "Date: #{Time.now.httpdate}\r\n" \ - "Status: #{status}\r\n" + buf = "HTTP/1.1 #{status}\r\n" \ + "Date: #{Time.now.httpdate}\r\n" \ + "Status: #{status}\r\n" \ + "Connection: #{alive ? KeepAlive : Close}\r\n" headers.each do |key, value| - next if %r{\A(?:X-Rainbows-|Date\z|Status\z)}i =~ key + next if %r{\A(?:X-Rainbows-|Date\z|Status\z\|Connection\z)}i =~ key if value =~ /\n/ # avoiding blank, key-only cookies with /\n+/ - rv << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join('') + buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join else - rv << "#{key}: #{value}\r\n" + buf << "#{key}: #{value}\r\n" end end - rv << CRLF + write(buf << CRLF) end - # called after forking - def self.setup(klass) - if 0 == Rainbows::G.kato - KEEP_ALIVE.replace(CLOSE) - Rainbows::HttpParser.keepalive_requests = 0 - end - range_class = body_class = klass - case Rainbows::Const::RACK_DEFAULTS['rainbows.model'] - when :WriterThreadSpawn - body_class = Rainbows::WriterThreadSpawn::Client - range_class = Rainbows::HttpServer - when :EventMachine, :NeverBlock - range_class = nil # :< - end - return if body_class.included_modules.include?(Body) - body_class.__send__(:include, Body) - sf = IO.respond_to?(:copy_stream) || IO.method_defined?(:sendfile_nonblock) - if range_class - range_class.__send__(:include, sf ? Range : NoRange) + def close_if_private(io) + io.close if F === io + end + + def io_for_fd(fd) + Rainbows::FD_MAP.delete(fd) || F.for_fd(fd) + end + + # to_io is not part of the Rack spec, but make an exception here + # since we can conserve path lookups and file descriptors. + # \Rainbows! will never get here without checking for the existence + # of body.to_path first. + def body_to_io(body) + if body.respond_to?(:to_io) + body.to_io + else + # try to take advantage of Rainbows::DevFdResponse, calling F.open + # is a last resort + path = body.to_path + %r{\A/dev/fd/(\d+)\z} =~ path ? io_for_fd($1.to_i) : F.open(path) end end - module NoRange - # dummy method if we can't send range responses - def make_range!(env, status, headers) + module Each + # generic body writer, used for most dynamically-generated responses + def write_body_each(body) + body.each { |chunk| write(chunk) } + end + + # generic response writer, used for most dynamically-generated responses + # and also when IO.copy_stream and/or IO#sendfile_nonblock is unavailable + def write_response(status, headers, body, alive) + write_headers(status, headers, alive) + write_body_each(body) + ensure + body.close if body.respond_to?(:close) end end + include Each + + if IO.method_defined?(:sendfile_nonblock) + module Sendfile + def write_body_file(body, range) + io = body_to_io(body) + range ? sendfile(io, range[0], range[1]) : sendfile(io, 0) + ensure + close_if_private(io) + end + end + include Sendfile + end + + if IO.respond_to?(:copy_stream) + unless IO.method_defined?(:sendfile_nonblock) + module CopyStream + def write_body_file(body, range) + range ? IO.copy_stream(body, self, range[1], range[0]) : + IO.copy_stream(body, self, nil, 0) + end + end + include CopyStream + end + + # write_body_stream is an alias for write_body_each if IO.copy_stream + # isn't used or available. + def write_body_stream(body) + IO.copy_stream(io = body_to_io(body), self) + ensure + close_if_private(io) + end + else # ! IO.respond_to?(:copy_stream) + alias write_body_stream write_body_each + end # ! IO.respond_to?(:copy_stream) + + if IO.method_defined?(:sendfile_nonblock) || IO.respond_to?(:copy_stream) + HTTP_RANGE = 'HTTP_RANGE' + Content_Range = 'Content-Range'.freeze + Content_Length = 'Content-Length'.freeze + + # This does not support multipart responses (does anybody actually + # use those?) + def sendfile_range(status, headers) + 200 == status.to_i && + /\Abytes=(\d+-\d*|\d*-\d+)\z/ =~ @hp.env[HTTP_RANGE] or + return + a, b = $1.split(/-/) + + headers = Rack::Utils::HeaderHash.new(headers) + clen = headers[Content_Length] or return + size = clen.to_i + + if b.nil? # bytes=M- + offset = a.to_i + count = size - offset + elsif a.empty? # bytes=-N + offset = size - b.to_i + count = size - offset + else # bytes=M-N + offset = a.to_i + count = b.to_i + 1 - offset + end + + if 0 > count || offset >= size + return 416, headers, nil + else + count = size if count > size + headers[Content_Length] = count.to_s + headers[Content_Range] = "bytes #{offset}-#{offset+count-1}/#{clen}" + return 206, headers, [ offset, count ] + end + end + + def write_response_path(status, headers, body, alive) + if File.file?(body.to_path) + if r = sendfile_range(status, headers) + status, headers, range = r + write_headers(status, headers, alive) + write_body_file(body, range) if range + else + write_headers(status, headers, alive) + write_body_file(body, nil) + end + else + write_headers(status, headers, alive) + write_body_stream(body) + end + ensure + body.close if body.respond_to?(:close) + end + + module ToPath + def write_response(status, headers, body, alive) + if body.respond_to?(:to_path) + write_response_path(status, headers, body, alive) + else + super + end + end + end + include ToPath + end # IO.respond_to?(:copy_stream) || IO.method_defined?(:sendfile_nonblock) end -- cgit v1.2.3-24-ge0c7