about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-06-28 08:06:32 +0000
committerEric Wong <normalperson@yhbt.net>2010-06-28 08:57:26 +0000
commit8175a52c67fb9dfc9c04a7b0597b680699f43deb (patch)
tree9c50e69b27c271929082aad63d0f1f7e9a4d28fa
parent86e9c7013308d77def5fe41b52a35dea60c7361c (diff)
downloadrainbows-8175a52c67fb9dfc9c04a7b0597b680699f43deb.tar.gz
This still needs work and lots of cleanup, but the basics are
there.  The sendfile 1.0.0 RubyGem is now safe to use under MRI
1.8, and is superior to current (1.9.2-preview3) versions of
IO.copy_stream for static files in that it supports more
platforms and doesn't truncate large files on 32-bit platforms.
-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'