From 8175a52c67fb9dfc9c04a7b0597b680699f43deb Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 28 Jun 2010 08:06:32 +0000 Subject: add preliminary sendfile (1.0.0) gem support 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. --- TODO | 8 +++++ lib/rainbows/base.rb | 51 +++++++++++++++++++++------ lib/rainbows/fiber/base.rb | 29 ++++++++++++--- t/t0020-large-sendfile-response.sh | 72 ++++++++++++++++++++++++++++++++++++++ t/test_isolate.rb | 2 ++ 5 files changed, 148 insertions(+), 14 deletions(-) create mode 100755 t/t0020-large-sendfile-response.sh 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' -- cgit v1.2.3-24-ge0c7