From 110e92752bd182459a08db3fcb8eb4a48d2f846f Mon Sep 17 00:00:00 2001 From: zedshaw Date: Mon, 27 Mar 2006 06:10:07 +0000 Subject: Final tweaks to speed up the file serving a bit using sendfile and a modified file handler. git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@124 19e92222-5c0b-0410-8929-a290d50e31e9 --- bin/mongrel_rails_svc | 246 ++++++++++++++++++++++++++---------------------- lib/mongrel.rb | 6 ++ lib/mongrel/handlers.rb | 46 +++++---- lib/mongrel/rails.rb | 171 ++++++++++++++++----------------- 4 files changed, 253 insertions(+), 216 deletions(-) diff --git a/bin/mongrel_rails_svc b/bin/mongrel_rails_svc index 5fa4caa..8056bcf 100644 --- a/bin/mongrel_rails_svc +++ b/bin/mongrel_rails_svc @@ -4,6 +4,7 @@ # This is where Win32::Daemon resides. ############################################### require 'rubygems' +require 'mongrel' require 'mongrel/rails' require 'optparse' require 'win32/service' @@ -70,14 +71,15 @@ class MongrelRails def delayed_initialize dbg "delayed_initialize entered" - + @rails = configure_rails # start up mongrel with the right configurations @server = Mongrel::HttpServer.new(@ip, @port, @num_procs.to_i, @timeout.to_i) @server.register("/", @rails) - + dbg "delayed_initialize left" + end def load_mime_map @@ -104,11 +106,12 @@ class MongrelRails Dir.chdir(@rails_root) + ENV['RAILS_ENV'] = @environment - require File.join(@rails_root, 'config/environment') + require 'config/environment' # configure the rails handler - rails = RailsHandler.new(@docroot, load_mime_map) + rails = Mongrel::Rails::RailsHandler.new(@docroot, load_mime_map) dbg "configure_rails left" @@ -116,24 +119,29 @@ class MongrelRails end def start_serve - dbg "start_serve entered" - - @runner = Thread.new do - dbg_th "runner_thread suspended" - Thread.stop + begin + dbg "start_serve entered" + + @runner = Thread.new do + dbg_th "runner_thread suspended" + Thread.stop + + dbg_th "runner_thread resumed" + dbg_th "runner_thread acceptor.join" + @server.acceptor.join + end - dbg_th "runner_thread resumed" - dbg_th "runner_thread acceptor.join" - @server.acceptor.join + dbg "server.run" + @server.run + + dbg "runner.run" + @runner.run + + dbg "start_serve left" + rescue + dbg "ERROR: #$!\r\n" + dbg $!.backtrace.join("\r\n") end - - dbg "server.run" - @server.run - - dbg "runner.run" - @runner.run - - dbg "start_serve left" end def stop_serve @@ -193,99 +201,107 @@ class RailsDaemon < Win32::Daemon end -if ARGV[0] == 'service' - ARGV.shift - - # default options - OPTIONS = { - :rails_root => Dir.pwd, - :environment => 'production', - :ip => '0.0.0.0', - :port => 3000, - :mime_map => nil, - :num_procs => 1024, - :timeout => 0, - :cpu => nil - } - - ARGV.options do |opts| - opts.on('-r', '--root PATH', "Set the root path where your rails app resides.") { |OPTIONS[:rails_root]| } - opts.on('-e', '--environment ENV', "Rails environment to run as. (default: production)") { |OPTIONS[:environment]| } - opts.on('-b', '--binding ADDR', "Address to bind to") { |OPTIONS[:ip]| } - opts.on('-p', '--port PORT', "Which port to bind to") { |OPTIONS[:port]| } - opts.on('-m', '--mime PATH', "A YAML file that lists additional MIME types") { |OPTIONS[:mime_map]| } - opts.on('-P', '--num-procs INT', "Number of processor threads to use") { |OPTIONS[:num_procs]| } - opts.on('-t', '--timeout SECONDS', "Timeout all requests after SECONDS time") { |OPTIONS[:timeout]| } - opts.on('-c', '--cpu CPU', "Bind the process to specific cpu") { |OPTIONS[:cpu]| } - - opts.parse! - end - - #expand RAILS_ROOT - OPTIONS[:rails_root] = File.expand_path(OPTIONS[:rails_root]) - - OPTIONS[:docroot] = File.expand_path(OPTIONS[:rails_root] + '/public') - - # We must bind to a specific cpu? - if OPTIONS[:cpu] - Kernel32.set_affinity(Process.pid, OPTIONS[:cpu]) - end - - rails = MongrelRails.new(OPTIONS[:ip], OPTIONS[:port], OPTIONS[:rails_root], OPTIONS[:docroot], OPTIONS[:environment], OPTIONS[:mime_map], OPTIONS[:num_procs].to_i, OPTIONS[:timeout].to_i) - rails_svc = RailsDaemon.new(rails) - rails_svc.mainloop - -elsif ARGV[0] == 'debug' - ARGV.shift - - # default options - OPTIONS = { - :rails_root => Dir.pwd, - :environment => 'production', - :ip => '0.0.0.0', - :port => 3000, - :mime_map => nil, - :num_procs => 20, - :timeout => 120, - :cpu => nil - } - - ARGV.options do |opts| - opts.on('-r', '--root PATH', "Set the root path where your rails app resides.") { |OPTIONS[:rails_root]| } - opts.on('-e', '--environment ENV', "Rails environment to run as.") { |OPTIONS[:environment]| } - opts.on('-b', '--binding ADDR', "Address to bind to") { |OPTIONS[:ip]| } - opts.on('-p', '--port PORT', "Which port to bind to") { |OPTIONS[:port]| } - opts.on('-m', '--mime PATH', "A YAML file that lists additional MIME types") { |OPTIONS[:mime_map]| } - opts.on('-P', '--num-procs INT', "Number of processor threads to use") { |OPTIONS[:num_procs]| } - opts.on('-t', '--timeout SECONDS', "Timeout all requests after SECONDS time") { |OPTIONS[:timeout]| } - opts.on('-c', '--cpu CPU', "Bind the process to specific cpu") { |OPTIONS[:cpu]| } - - opts.parse! - end - - #expand RAILS_ROOT - OPTIONS[:rails_root] = File.expand_path(OPTIONS[:rails_root]) - - OPTIONS[:docroot] = File.expand_path(OPTIONS[:rails_root] + '/public') - - # We must bind to a specific cpu? - if OPTIONS[:cpu] - Kernel32.set_affinity(Process.pid, OPTIONS[:cpu]) - end - - rails = MongrelRails.new(OPTIONS[:ip], OPTIONS[:port], OPTIONS[:rails_root], OPTIONS[:docroot], OPTIONS[:environment], OPTIONS[:mime_map], OPTIONS[:num_procs].to_i, OPTIONS[:timeout].to_i) - rails.delayed_initialize - rails.start_serve - - begin - sleep - rescue Interrupt - puts "graceful shutdown?" - end - - begin - rails.stop_serve - rescue - end - +begin + if ARGV[0] == 'service' + ARGV.shift + + # default options + OPTIONS = { + :rails_root => Dir.pwd, + :environment => 'production', + :ip => '0.0.0.0', + :port => 3000, + :mime_map => nil, + :num_procs => 1024, + :timeout => 0, + :cpu => nil + } + + ARGV.options do |opts| + opts.on('-r', '--root PATH', "Set the root path where your rails app resides.") { |OPTIONS[:rails_root]| } + opts.on('-e', '--environment ENV', "Rails environment to run as. (default: production)") { |OPTIONS[:environment]| } + opts.on('-b', '--binding ADDR', "Address to bind to") { |OPTIONS[:ip]| } + opts.on('-p', '--port PORT', "Which port to bind to") { |OPTIONS[:port]| } + opts.on('-m', '--mime PATH', "A YAML file that lists additional MIME types") { |OPTIONS[:mime_map]| } + opts.on('-P', '--num-procs INT', "Number of processor threads to use") { |OPTIONS[:num_procs]| } + opts.on('-t', '--timeout SECONDS', "Timeout all requests after SECONDS time") { |OPTIONS[:timeout]| } + opts.on('-c', '--cpu CPU', "Bind the process to specific cpu") { |OPTIONS[:cpu]| } + + opts.parse! + end + + #expand RAILS_ROOT + OPTIONS[:rails_root] = File.expand_path(OPTIONS[:rails_root]) + + OPTIONS[:docroot] = File.expand_path(OPTIONS[:rails_root] + '/public') + + # We must bind to a specific cpu? + if OPTIONS[:cpu] + Kernel32.set_affinity(Process.pid, OPTIONS[:cpu]) + end + + rails = MongrelRails.new(OPTIONS[:ip], OPTIONS[:port], OPTIONS[:rails_root], OPTIONS[:docroot], OPTIONS[:environment], OPTIONS[:mime_map], OPTIONS[:num_procs].to_i, OPTIONS[:timeout].to_i) + rails_svc = RailsDaemon.new(rails) + rails_svc.mainloop + + elsif ARGV[0] == 'debug' + ARGV.shift + + # default options + OPTIONS = { + :rails_root => Dir.pwd, + :environment => 'production', + :ip => '0.0.0.0', + :port => 3000, + :mime_map => nil, + :num_procs => 20, + :timeout => 120, + :cpu => nil + } + + ARGV.options do |opts| + opts.on('-r', '--root PATH', "Set the root path where your rails app resides.") { |OPTIONS[:rails_root]| } + opts.on('-e', '--environment ENV', "Rails environment to run as.") { |OPTIONS[:environment]| } + opts.on('-b', '--binding ADDR', "Address to bind to") { |OPTIONS[:ip]| } + opts.on('-p', '--port PORT', "Which port to bind to") { |OPTIONS[:port]| } + opts.on('-m', '--mime PATH', "A YAML file that lists additional MIME types") { |OPTIONS[:mime_map]| } + opts.on('-P', '--num-procs INT', "Number of processor threads to use") { |OPTIONS[:num_procs]| } + opts.on('-t', '--timeout SECONDS', "Timeout all requests after SECONDS time") { |OPTIONS[:timeout]| } + opts.on('-c', '--cpu CPU', "Bind the process to specific cpu") { |OPTIONS[:cpu]| } + + opts.parse! + end + + #expand RAILS_ROOT + OPTIONS[:rails_root] = File.expand_path(OPTIONS[:rails_root]) + + OPTIONS[:docroot] = File.expand_path(OPTIONS[:rails_root] + '/public') + + # We must bind to a specific cpu? + if OPTIONS[:cpu] + Kernel32.set_affinity(Process.pid, OPTIONS[:cpu]) + end + + rails = MongrelRails.new(OPTIONS[:ip], OPTIONS[:port], OPTIONS[:rails_root], OPTIONS[:docroot], OPTIONS[:environment], OPTIONS[:mime_map], OPTIONS[:num_procs].to_i, OPTIONS[:timeout].to_i) + rails.delayed_initialize + rails.start_serve + + begin + sleep + rescue Interrupt + dbg "ERROR: #$!\r\n" + dbg $!.backtrace.join("\r\n") + puts "graceful shutdown?" + end + + begin + rails.stop_serve + rescue + dbg "ERROR: #$!\r\n" + dbg $!.backtrace.join("\r\n") + end + end +rescue + dbg "ERROR: #$!\r\n" + dbg $!.backtrace.join("\r\n") end diff --git a/lib/mongrel.rb b/lib/mongrel.rb index 59402cc..19edf1e 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -200,6 +200,7 @@ module Mongrel @out.write(value) @out.write("\r\n") end + end # Writes and controls your response to the client using the HTTP/1.1 specification. @@ -306,6 +307,10 @@ module Mongrel end end + def write(data) + @socket.write(data) + end + # This takes whatever has been done to header and body and then writes it in the # proper format to make an HTTP/1.1 response. def finished @@ -317,6 +322,7 @@ module Mongrel def done (@status_sent and @header_sent and @body_sent) end + end diff --git a/lib/mongrel/handlers.rb b/lib/mongrel/handlers.rb index 7d817bb..d0de0ca 100644 --- a/lib/mongrel/handlers.rb +++ b/lib/mongrel/handlers.rb @@ -1,3 +1,11 @@ +require 'rubygems' +begin + require 'sendfile' + $mongrel_has_sendfile = true + STDERR.puts "** You have sendfile installed, will use that to serve files." +rescue Object + $mongrel_has_sendfile = false +end module Mongrel @@ -150,20 +158,29 @@ module Mongrel # Sends the contents of a file back to the user. Not terribly efficient since it's # opening and closing the file for each read. def send_file(req, response) - response.start(200) do |head,out| - # set the mime type from our map based on the ending - dot_at = req.rindex(".") - if dot_at - ext = req[dot_at .. -1] - if MIME_TYPES[ext] - head['Content-Type'] = MIME_TYPES[ext] - end - end - open(req, "rb") do |f| - out.write(f.read) + # first we setup the headers and status then we do a very fast send on the socket directly + response.status = 200 + + # set the mime type from our map based on the ending + dot_at = req.rindex(".") + if dot_at + ext = req[dot_at .. -1] + if MIME_TYPES[ext] + response.header['Content-Type'] = MIME_TYPES[ext] end end + + response.header['Content-Length'] = File.size(req) + + response.send_status + response.send_header + + if $mongrel_has_sendfile + File.open(req, "rb") { |f| response.socket.sendfile(f) } + else + File.open(req, "rb") { |f| response.socket.write(f.read) } + end end @@ -184,11 +201,8 @@ module Mongrel send_file(req, response) end rescue => details - response.reset - response.start(403) do |head,out| - out << "Error accessing file: #{details}" - out << details.backtrace.join("\n") - end + STDERR.puts "Error accessing file: #{details}" + STDERR.puts details.backtrace.join("\n") end end end diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb index 8dc42a0..d2c714c 100644 --- a/lib/mongrel/rails.rb +++ b/lib/mongrel/rails.rb @@ -4,6 +4,92 @@ require 'cgi' module Mongrel module Rails + + # Implements a handler that can run Rails and serve files out of the + # Rails application's public directory. This lets you run your Rails + # application with Mongrel during development and testing, then use it + # also in production behind a server that's better at serving the + # static files. + # + # The RailsHandler takes a mime_map parameter which is a simple suffix=mimetype + # mapping that it should add to the list of valid mime types. + # + # It also supports page caching directly and will try to resolve a request + # in the following order: + # + # * If the requested exact PATH_INFO exists as a file then serve it. + # * If it exists at PATH_INFO+".html" exists then serve that. + # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. + # + # This means that if you are using page caching it will actually work with Mongrel + # and you should see a decent speed boost (but not as fast as if you use lighttpd). + # + # An additional feature you can use is + class RailsHandler < Mongrel::HttpHandler + attr_reader :files + attr_reader :guard + + def initialize(dir, mime_map = {}) + @files = Mongrel::DirHandler.new(dir,false) + @guard = Mutex.new + + # register the requested mime types + mime_map.each {|k,v| Mongrel::DirHandler::add_mime_type(k,v) } + end + + # Attempts to resolve the request as follows: + # + # + # * If the requested exact PATH_INFO exists as a file then serve it. + # * If it exists at PATH_INFO+".html" exists then serve that. + # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. + def process(request, response) + return if response.socket.closed? + + path_info = request.params[Mongrel::Const::PATH_INFO] + page_cached = request.params[Mongrel::Const::PATH_INFO] + ".html" + + if @files.can_serve(path_info) + # File exists as-is so serve it up + @files.process(request,response) + elsif @files.can_serve(page_cached) + # possible cached page, serve it up + request.params[Mongrel::Const::PATH_INFO] = page_cached + @files.process(request,response) + else + begin + cgi = Mongrel::CGIWrapper.new(request, response) + cgi.handler = self + + @guard.synchronize do + # Rails is not thread safe so must be run entirely within synchronize + Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body) + end + + # This finalizes the output using the proper HttpResponse way + cgi.out {""} + rescue Errno::EPIPE + # ignored + rescue Object => rails_error + STDERR.puts "Error calling Dispatcher.dispatch #{rails_error.inspect}" + STDERR.puts rails_error.backtrace.join("\n") + end + end + end + + + # Does the internal reload for Rails. It might work for most cases, but + # sometimes you get exceptions. In that case just do a real restart. + def reload! + @guard.synchronize do + $".replace $orig_dollar_quote + GC.start + Dispatcher.reset_application! + ActionController::Routing::Routes.reload + end + end + end + # Creates Rails specific configuration options for people to use # instead of the base Configurator. class RailsConfigurator < Mongrel::Configurator @@ -86,91 +172,6 @@ module Mongrel log "WARNING: Rails does not support signals on Win32." end end - - # Implements a handler that can run Rails and serve files out of the - # Rails application's public directory. This lets you run your Rails - # application with Mongrel during development and testing, then use it - # also in production behind a server that's better at serving the - # static files. - # - # The RailsHandler takes a mime_map parameter which is a simple suffix=mimetype - # mapping that it should add to the list of valid mime types. - # - # It also supports page caching directly and will try to resolve a request - # in the following order: - # - # * If the requested exact PATH_INFO exists as a file then serve it. - # * If it exists at PATH_INFO+".html" exists then serve that. - # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. - # - # This means that if you are using page caching it will actually work with Mongrel - # and you should see a decent speed boost (but not as fast as if you use lighttpd). - # - # An additional feature you can use is - class RailsHandler < Mongrel::HttpHandler - attr_reader :files - attr_reader :guard - - def initialize(dir, mime_map = {}) - @files = Mongrel::DirHandler.new(dir,false) - @guard = Mutex.new - - # register the requested mime types - mime_map.each {|k,v| Mongrel::DirHandler::add_mime_type(k,v) } - end - - # Attempts to resolve the request as follows: - # - # - # * If the requested exact PATH_INFO exists as a file then serve it. - # * If it exists at PATH_INFO+".html" exists then serve that. - # * Finally, construct a Mongrel::CGIWrapper and run Dispatcher.dispath to have Rails go. - def process(request, response) - return if response.socket.closed? - - path_info = request.params[Mongrel::Const::PATH_INFO] - page_cached = request.params[Mongrel::Const::PATH_INFO] + ".html" - - if @files.can_serve(path_info) - # File exists as-is so serve it up - @files.process(request,response) - elsif @files.can_serve(page_cached) - # possible cached page, serve it up - request.params[Mongrel::Const::PATH_INFO] = page_cached - @files.process(request,response) - else - begin - cgi = Mongrel::CGIWrapper.new(request, response) - cgi.handler = self - - @guard.synchronize do - # Rails is not thread safe so must be run entirely within synchronize - Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, response.body) - end - - # This finalizes the output using the proper HttpResponse way - cgi.out {""} - rescue Errno::EPIPE - # ignored - rescue Object => rails_error - log "Error calling Dispatcher.dispatch #{rails_error.inspect}" - log rails_error.backtrace.join("\n") - end - end - end - - - # Does the internal reload for Rails. It might work for most cases, but - # sometimes you get exceptions. In that case just do a real restart. - def reload! - @guard.synchronize do - $".replace $orig_dollar_quote - GC.start - Dispatcher.reset_application! - ActionController::Routing::Routes.reload - end - end - end end end end -- cgit v1.2.3-24-ge0c7