about summary refs log tree commit homepage
path: root/lib/rainbows/response.rb
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-12-30 08:33:15 +0000
committerEric Wong <normalperson@yhbt.net>2011-01-04 16:37:42 -0800
commite21939d776673b2f8887adf7a5c64812b7d2e98e (patch)
tree48aa3a71201e770758bd09b325c3f2704411af7f /lib/rainbows/response.rb
parent4a76da1833922c74e147be5def9bfe04fd0c16a2 (diff)
downloadrainbows-e21939d776673b2f8887adf7a5c64812b7d2e98e.tar.gz
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.
Diffstat (limited to 'lib/rainbows/response.rb')
-rw-r--r--lib/rainbows/response.rb197
1 files changed, 158 insertions, 39 deletions
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