1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

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
This commit is contained in:
zedshaw 2006-03-27 06:10:07 +00:00
parent 4d9966ee00
commit 110e92752b
4 changed files with 253 additions and 216 deletions

View file

@ -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"
dbg_th "runner_thread resumed"
dbg_th "runner_thread acceptor.join"
@server.acceptor.join
@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 "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

View file

@ -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

View file

@ -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

View file

@ -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