about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--TODO8
-rw-r--r--lib/rainbows/base.rb51
-rw-r--r--lib/rainbows/fiber/base.rb29
-rwxr-xr-xt/t0020-large-sendfile-response.sh72
-rw-r--r--t/test_isolate.rb2
5 files changed, 148 insertions, 14 deletions
diff --git a/TODO b/TODO
index 00dce85..a6ae16f 100644
--- a/TODO
+++ b/TODO
@@ -6,6 +6,14 @@ care about.
 * Split out NeverBlock into NeverBlockEventMachine and NeverBlockReactor
   NeverBlock will default to one of them (depending on NB upstream).
 
+* allow _OPTIONAL_ splice(2) with DevFdResponse under Linux
+  (splice is very broken under some older kernels)
+
+* use IO#sendfile_nonblock for EventMachine/Rev/Revactor/NeverBlock
+
+* Open file cache (idea from nginx), since sendfile (and IO.copy_stream)
+  allows pread(2)-style offsets
+
 * Improve test suite coverage.  We won't waste cycles with puny
   unit tests, only integration tests that exercise externally
   visible parts.
diff --git a/lib/rainbows/base.rb b/lib/rainbows/base.rb
index 2627719..24924cb 100644
--- a/lib/rainbows/base.rb
+++ b/lib/rainbows/base.rb
@@ -39,25 +39,56 @@ module Rainbows::Base
     logger.info "Rainbows! #@use worker_connections=#@worker_connections"
   end
 
+  # TODO: move write_body_* stuff out of Base
+  def write_body_each(client, body)
+    body.each { |chunk| client.write(chunk) }
+    ensure
+      body.respond_to?(:close) and body.close
+  end
+
+  # The sendfile 1.0.0 RubyGem includes IO#sendfile and
+  # IO#sendfile_nonblock, previous versions didn't have
+  # IO#sendfile_nonblock, and IO#sendfile in previous versions
+  # could other threads under 1.8 with large files
+  #
+  # IO#sendfile currently (June 2010) beats 1.9 IO.copy_stream with
+  # non-Linux support and large files on 32-bit.  We still fall back to
+  # IO.copy_stream (if available) if we're dealing with DevFdResponse
+  # objects, though.
+  if IO.method_defined?(:sendfile_nonblock)
+    def write_body_path(client, body)
+      file = Rainbows.body_to_io(body)
+      file.stat.file? ? client.sendfile(file, 0) :
+                        write_body_stream(client, file)
+    end
+  end
+
   if IO.respond_to?(:copy_stream)
-    def write_body(client, body)
-      if body.respond_to?(:to_path)
+    unless method_defined?(:write_body_path)
+      def write_body_path(client, body)
         IO.copy_stream(Rainbows.body_to_io(body), client)
-      else
-        body.each { |chunk| client.write(chunk) }
       end
-      ensure
-        body.respond_to?(:close) and body.close
+    end
+
+    def write_body_stream(client, body)
+      IO.copy_stream(body, client)
     end
   else
+    alias write_body_stream write_body_each
+  end
+
+  if method_defined?(:write_body_path)
     def write_body(client, body)
-      body.each { |chunk| client.write(chunk) }
-      ensure
-        body.respond_to?(:close) and body.close
+      body.respond_to?(:to_path) ?
+        write_body_path(client, body) :
+        write_body_each(client, body)
     end
+  else
+    alias write_body write_body_each
   end
 
-  module_function :write_body
+  module_function :write_body, :write_body_each, :write_body_stream
+  method_defined?(:write_body_path) and module_function(:write_body_path)
 
   def wait_headers_readable(client)
     IO.select([client], nil, nil, G.kato)
diff --git a/lib/rainbows/fiber/base.rb b/lib/rainbows/fiber/base.rb
index 0298948..7e39441 100644
--- a/lib/rainbows/fiber/base.rb
+++ b/lib/rainbows/fiber/base.rb
@@ -72,10 +72,31 @@ module Rainbows
         max.nil? || max > (now + 1) ? 1 : max - now
       end
 
-      def write_body(client, body)
-        body.each { |chunk| client.write(chunk) }
-        ensure
-          body.respond_to?(:close) and body.close
+      # TODO: IO.splice under Linux
+      alias write_body_stream write_body_each
+
+      # the sendfile 1.0.0+ gem includes IO#sendfile_nonblock
+      if ::IO.method_defined?(:sendfile_nonblock)
+        def write_body_path(client, body)
+          file = Rainbows.body_to_io(body)
+          if file.stat.file?
+            sock, off = client.to_io, 0
+            begin
+              off += sock.sendfile_nonblock(file, off, 0x10000)
+            rescue Errno::EAGAIN
+              client.wait_writable
+            rescue EOFError
+              break
+            rescue => e
+              Rainbows::Error.app(e)
+              break
+            end while true
+          else
+            write_body_stream(client, body)
+          end
+        end
+      else
+        alias write_body write_body_each
       end
 
       def wait_headers_readable(client)
diff --git a/t/t0020-large-sendfile-response.sh b/t/t0020-large-sendfile-response.sh
new file mode 100755
index 0000000..822a23f
--- /dev/null
+++ b/t/t0020-large-sendfile-response.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+. ./test-lib.sh
+test -r random_blob || die "random_blob required, run with 'make $0'"
+case $RUBY_ENGINE in
+ruby) ;;
+*)
+        t_info "skipping $T since it can't load the sendfile gem, yet"
+        exit 0
+        ;;
+esac
+
+t_plan 7 "large sendfile response for $model"
+
+t_begin "setup and startup" && {
+        rtmpfiles curl_out a b c
+        rainbows_setup $model 2
+
+        # FIXME: allow "require 'sendfile'" to work in $unicorn_config
+        RUBYOPT="-rsendfile"
+        export RUBYOPT
+
+        # can't load Rack::Lint here since it clobbers body#to_path
+        rainbows -E none -D large-file-response.ru -c $unicorn_config
+        rainbows_wait_start
+}
+
+t_begin "read random blob sha1" && {
+        random_blob_sha1=$(rsha1 < random_blob)
+}
+
+t_begin "send a series of HTTP/1.1 requests in parallel" && {
+        for i in $a $b $c
+        do
+                (
+                        curl -sSf http://$listen/random_blob | rsha1 > $i
+                ) &
+        done
+        wait
+        for i in $a $b $c
+        do
+                test x$(cat $i) = x$random_blob_sha1
+        done
+}
+
+# this was a problem during development
+t_begin "HTTP/1.0 test" && {
+        sha1=$( (curl -0 -sSfv http://$listen/random_blob &&
+                 echo ok >$ok) | rsha1)
+        test $sha1 = $random_blob_sha1
+        test xok = x$(cat $ok)
+}
+
+t_begin "HTTP/0.9 test" && {
+        (
+                printf 'GET /random_blob\r\n'
+                rsha1 < $fifo > $tmp &
+                wait
+                echo ok > $ok
+        ) | socat - TCP:$listen > $fifo
+        test $(cat $tmp) = $random_blob_sha1
+        test xok = x$(cat $ok)
+}
+
+t_begin "shutdown server" && {
+        kill -QUIT $rainbows_pid
+}
+
+dbgcat r_err
+
+t_begin "check stderr" && check_stderr
+
+t_done
diff --git a/t/test_isolate.rb b/t/test_isolate.rb
index 00d57bf..1b6c46d 100644
--- a/t/test_isolate.rb
+++ b/t/test_isolate.rb
@@ -18,6 +18,8 @@ Isolate.now!(opts) do
   gem 'unicorn', '1.0.0'
 
   if engine == "ruby"
+    gem 'sendfile', '1.0.0' # next Rubinius should support this
+
     gem 'iobuffer', '0.1.3'
     gem 'rev', '0.3.2'