From f2b53a3a4b1ddacac4fc18ccbe2b016194a50777 Mon Sep 17 00:00:00 2001 From: zedshaw Date: Sun, 26 Mar 2006 20:01:50 +0000 Subject: mongrel_rails now uses the RailsConfigurator. All rails.rb are now in Mongrel::Rails (like Camping). Configurator has many improvements. Signals on mongrel_rails now work better. git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@122 19e92222-5c0b-0410-8929-a290d50e31e9 --- bin/mongrel_rails | 160 ++++++++----------------- lib/mongrel.rb | 108 ++++++++++++++--- lib/mongrel/command.rb | 2 +- lib/mongrel/debug.rb | 233 ++++++++++++++++++++++++++---------- lib/mongrel/rails.rb | 313 ++++++++++++++++++++++++++----------------------- lib/mongrel/stats.rb | 71 +++++++++++ test/test_debug.rb | 14 ++- test/test_stats.rb | 28 +++++ 8 files changed, 592 insertions(+), 337 deletions(-) create mode 100644 lib/mongrel/stats.rb create mode 100644 test/test_stats.rb diff --git a/bin/mongrel_rails b/bin/mongrel_rails index f6e6e59..23121d8 100644 --- a/bin/mongrel_rails +++ b/bin/mongrel_rails @@ -1,6 +1,9 @@ + require 'rubygems' require 'yaml' -require 'mongrel' +require 'mongrel/rails' + + class Start < GemPlugin::Plugin "/commands" include Mongrel::Command::Base @@ -18,6 +21,7 @@ class Start < GemPlugin::Plugin "/commands" ['-m', '--mime PATH', "A YAML file that lists additional MIME types", :@mime_map, nil], ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd], ['-r', '--root PATH', "Set the document root (default 'public')", :@docroot, "public"], + ['-B', '--debug', "Enable debugging mode", :@debug, false], ] end @@ -36,121 +40,62 @@ class Start < GemPlugin::Plugin "/commands" return @valid end + def run - def daemonize - # save this for later since daemonize will hose it - if @daemon and RUBY_PLATFORM !~ /mswin/ - require 'daemons/daemonize' - - puts "Started Mongrel server in #@environment mode at #@address:#@port" - Daemonize.daemonize(log_file=File.join(@cwd, @log_file)) - - # change back to the original starting directory - Dir.chdir(@cwd) - - open(@pid_file,"w") {|f| f.write(Process.pid) } - else - puts "Running Mongrel server in #@environment mode at #@address:#@port" - end - end - - def load_mime_map - mime = {} - - # configure any requested mime map - if @mime_map - puts "Loading additional MIME types from #@mime_map" - mime.merge!(YAML.load_file(@mime_map)) - - # check all the mime types to make sure they are the right format - mime.each {|k,v| puts "WARNING: MIME type #{k} must start with '.'" if k.index(".") != 0 } - end - - return mime - end - - def configure_rails - # need this later for safe reloading - $orig_dollar_quote = $".clone - - ENV['RAILS_ENV'] = @environment - require 'config/environment' - require 'dispatcher' - require 'mongrel/rails' + settings = { :host => @address, :port => @port, :cwd => @cwd, + :log_file => @log_file, :pid_file => @pid_file, :environment => @environment, + :docroot => @docroot, :mime_map => @mime_map, :daemon => @daemon, + :debug => @debug, :includes => ["mongrel"] + } - # configure the rails handler - rails = RailsHandler.new(@docroot, load_mime_map) - return rails - end - - def start_mongrel(rails) - @restart = false - - server = Mongrel::HttpServer.new(@address, @port, @num_procs.to_i, @timeout.to_i) - server.register("/", rails) - - # signal trapping just applies to posix systems - # TERM is a valid signal, but still doesn't gracefuly shutdown on win32. - if RUBY_PLATFORM !~ /mswin/ - # graceful shutdown - trap("TERM") { - server.stop - File.unlink @pid_file if File.exist?(@pid_file) - } - - # rails reload - trap("HUP") { - STDERR.puts "Reloading rails..." - rails.reload! - STDERR.puts "Done reloading rails." - } - - # restart - trap("USR2") { - server.stop - File.unlink @pid_file if File.exist?(@pid_file) - @restart = true - } - - trap("INT") { - server.stop - File.unlink @pid_file if File.exist?(@pid_file) - @restart = false - } + config = Mongrel::Rails::RailsConfigurator.new(settings) do + log "Starting Mongrel in #{settings[:environment]} mode at #{settings[:host]}:#{settings[:port]}" + + if defaults[:daemon] + log "Daemonizing, any open files are closed. Look at #{settings[:pid_file]} and #{settings[:log_file]} for info." + daemonize + end + + listener do + mime = {} + if defaults[:mime_map] + log "Loading additional MIME types from #{settings[:mime_map]}" + mime = load_mime_map(defaults[:mime_map], mime) + end + + if defaults[:debug] + log "Installing debugging prefixed filters. Look in log/mongrel_debug for the files." + debug "/" + end + + log "Starting Rails in environment #{settings[:environment]} ..." + uri "/", :handler => rails + log "Rails loaded." + + log "Loading any Rails specific GemPlugins" + load_plugins + + setup_rails_signals + end end - # hook up any rails specific plugins - GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE - - begin - # start mongrel processing thread - server.run + config.run + config.log "Mongrel available at #{settings[:host]}:#{settings[:port]}" + config.join + if config.needs_restart if RUBY_PLATFORM !~ /mswin/ - puts "Server Ready. Use CTRL-C to quit." + cmd = "ruby #{__FILE__} start #{original_args.join(' ')}" + config.log "Restarting with arguments: #{cmd}" + exec cmd else - puts "Server Ready. Use CTRL-Pause/Break to quit." + config.log "Win32 does not support restarts. Exiting." end - - server.acceptor.join - rescue Interrupt - STDERR.puts "Interrupted." - raise end - - # daemonize makes restart easy - run if @restart - end - - def run - daemonize - rails = configure_rails - start_mongrel(rails) end end - def send_signal(signal, pid_file) pid = open(pid_file).read.to_i print "Sending #{signal} to Mongrel at PID #{pid}..." @@ -232,12 +177,7 @@ end GemPlugin::Manager.instance.load "mongrel" => GemPlugin::INCLUDE, "rails" => GemPlugin::EXCLUDE -require 'mongrel/debug' -ObjectTracker.configure -MongrelDbg.configure - Mongrel::Command::Registry.instance.run ARGV -END { -Class.report_object_creations -} \ No newline at end of file + + diff --git a/lib/mongrel.rb b/lib/mongrel.rb index cd94b9b..9c7a804 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -438,7 +438,7 @@ module Mongrel client = @socket.accept worker_list = @workers.list if worker_list.length >= @num_processors - STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max)." + STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max). Dropping connection." client.close reap_dead_workers(worker_list) else @@ -541,11 +541,13 @@ module Mongrel class Configurator attr_reader :listeners attr_reader :defaults + attr_reader :needs_restart # You pass in initial defaults and then a block to continue configuring. def initialize(defaults={}, &blk) @listeners = {} @defaults = defaults + @needs_restart = false if blk cloaker(&blk).bind(self).call @@ -577,11 +579,15 @@ module Mongrel # # * :host => Host name to bind. # * :port => Port to bind. + # * :num_processors => The maximum number of concurrent threads allowed. (950 default) + # * :timeout => 1/100th of a second timeout between requests. (10 is 1/10th, 0 is not timeout) # def listener(options={},&blk) ops = resolve_defaults(options) - - @listener = Mongrel::HttpServer.new(ops[:host], ops[:port].to_i) + ops[:num_processors] ||= 950 + ops[:timeout] ||= 0 + + @listener = Mongrel::HttpServer.new(ops[:host], ops[:port].to_i, ops[:num_processors].to_i, ops[:timeout].to_i) @listener_name = "#{ops[:host]}:#{ops[:port]}" @listeners[@listener_name] = @listener @@ -620,17 +626,22 @@ module Mongrel # * :log_file => Where to write STDOUT and STDERR. # * :pid_file => Where to write the process ID. # + # It is safe to call this on win32 as it will only require daemons + # if NOT win32. def daemonize(options={}) + ops = resolve_defaults(options) # save this for later since daemonize will hose it if RUBY_PLATFORM !~ /mswin/ require 'daemons/daemonize' - Daemonize.daemonize(log_file=File.join(options[:cwd], options[:log_file])) + Daemonize.daemonize(log_file=File.join(ops[:cwd], ops[:log_file])) # change back to the original starting directory - Dir.chdir(options[:cwd]) + Dir.chdir(ops[:cwd]) - open(options[:pid_file],"w") {|f| f.write(Process.pid) } + open(ops[:pid_file],"w") {|f| f.write(Process.pid) } + else + log "WARNING: Win32 does not support daemon mode." end end @@ -640,16 +651,17 @@ module Mongrel # :excludes => [] setting listing the names of plugins to include # or exclude from the loading. def load_plugins(options={}) + ops = resolve_defaults(options) load_settings = {} - if options[:includes] - options[:includes].each do |plugin| + if ops[:includes] + ops[:includes].each do |plugin| load_settings[plugin] = GemPlugin::INCLUDE end end - if options[:excludes] - options[:excludes].each do |plugin| + if ops[:excludes] + ops[:excludes].each do |plugin| load_settings[plugin] = GemPlugin::EXCLUDE end end @@ -672,11 +684,11 @@ module Mongrel # is organized. def load_mime_map(file, mime={}) # configure any requested mime map - STDERR.puts "Loading additional MIME types from #{file}" + log "Loading additional MIME types from #{file}" mime = load_yaml(file, mime) # check all the mime types to make sure they are the right format - mime.each {|k,v| STDERR.puts "WARNING: MIME type #{k} must start with '.'" if k.index(".") != 0 } + mime.each {|k,v| log "WARNING: MIME type #{k} must start with '.'" if k.index(".") != 0 } return mime end @@ -696,7 +708,7 @@ module Mongrel # to prevent Ruby from exiting until each one is done. def run @listeners.each {|name,s| - STDERR.puts "Running #{name} listener." + log "Running #{name} listener." s.run } @@ -706,7 +718,7 @@ module Mongrel # stop processing requests (gracefully). def stop @listeners.each {|name,s| - STDERR.puts "Stopping #{name} listener." + log "Stopping #{name} listener." s.stop } end @@ -718,6 +730,74 @@ module Mongrel def join @listeners.values.each {|s| s.acceptor.join } end + + + # Calling this before you register your URIs to the given location + # will setup a set of handlers that log open files, objects, and the + # parameters for each request. This helps you track common problems + # found in Rails applications that are either slow or become unresponsive + # after a little while. + def debug(location) + require 'mongrel/debug' + ObjectTracker.configure + MongrelDbg.configure + MongrelDbg.begin_trace :objects + MongrelDbg.begin_trace :rails + MongrelDbg.begin_trace :files + + uri "/", :handler => plugin("/handlers/requestlog::files") + uri "/", :handler => plugin("/handlers/requestlog::objects") + uri "/", :handler => plugin("/handlers/requestlog::params") + end + + + # Sets up the standard signal handlers that are used on most Ruby + # It only configures if the platform is not win32 and doesn't do + # a HUP signal since this is typically framework specific. + # + # Requires a :pid_file option to indicate a file to delete. + # It sets the MongrelConfig.needs_restart attribute if + # the start command should reload. It's up to you to detect this + # and do whatever is needed for a "restart". + # + # This command is safely ignored if the platform is win32 (with a warning) + def setup_signals(options={}) + ops = resolve_defaults(options) + + if RUBY_PLATFORM !~ /mswin/ + # graceful shutdown + trap("TERM") { + log "TERM signal received." + stop + File.unlink ops[:pid_file] if File.exist?(ops[:pid_file]) + } + + # restart + trap("USR2") { + log "USR2 signal received." + stop + File.unlink ops[:pid_file] if File.exist?(ops[:pid_file]) + @needs_restart = true + } + + trap("INT") { + log "INT signal received." + stop + File.unlink ops[:pid_file] if File.exist?(ops[:pid_file]) + @needs_restart = false + } + + log "Signals ready. TERM => stop. USR2 => restart. INT => stop (no restart)." + else + log "WARNING: Win32 does not have signals support." + end + end + + # Logs a simple message to STDERR (or the mongrel log if in daemon mode). + def log(msg) + STDERR.print "** ", msg, "\n" + end + end end diff --git a/lib/mongrel/command.rb b/lib/mongrel/command.rb index 2b21684..0341e15 100644 --- a/lib/mongrel/command.rb +++ b/lib/mongrel/command.rb @@ -15,7 +15,7 @@ module Mongrel # user's bidding. module Base - attr_reader :valid, :done_validating + attr_reader :valid, :done_validating, :original_args # Called by the implemented command to set the options for that command. # Every option has a short and long version, a description, a variable to diff --git a/lib/mongrel/debug.rb b/lib/mongrel/debug.rb index 8b2dd5c..6178872 100644 --- a/lib/mongrel/debug.rb +++ b/lib/mongrel/debug.rb @@ -1,6 +1,6 @@ require 'logger' require 'set' - +require 'socket' $mongrel_debugging=true @@ -8,14 +8,14 @@ module MongrelDbg SETTINGS = { :tracing => {}} LOGGING = { } - def MongrelDbg::configure(log_dir = "mongrel_debug") + def MongrelDbg::configure(log_dir = "log/mongrel_debug") Dir.mkdir(log_dir) if not File.exist?(log_dir) @log_dir = log_dir end def MongrelDbg::trace(target, message) - if SETTINGS[:tracing][target] + if SETTINGS[:tracing][target] and LOGGING[target] LOGGING[target].log(Logger::DEBUG, message) end end @@ -34,32 +34,28 @@ module MongrelDbg LOGGING[target].close LOGGING[target] = nil end + + def MongrelDbg::tracing?(target) + SETTINGS[:tracing][target] + end end module ObjectTracker @active_objects = nil - @live_object_tracking = false + @live_object_tracking = true def ObjectTracker.configure @active_objects = Set.new + ObjectSpace.each_object do |obj| @active_objects << obj.object_id end - srand @active_objects.object_id - @sample_thread = Thread.new do - loop do - sleep(rand(3) + (rand(100)/100.0)) - ObjectTracker.sample - end - end - @sample_thread.priority = 20 end + def ObjectTracker.start - @stopit = true @live_object_tracking = true - @stopit = false end def ObjectTracker.stop @@ -67,78 +63,197 @@ module ObjectTracker end def ObjectTracker.sample - ospace = Set.new - ObjectSpace.each_object do |obj| - ospace << obj.object_id + Class.stopit do + ospace = Set.new + counts = {} + + # Strings can't be tracked easily and are so numerous that they drown out all else + # so we just ignore them in the counts. + ObjectSpace.each_object do |obj| + if not obj.kind_of? String + ospace << obj.object_id + counts[obj.class] ||= 0 + counts[obj.class] += 1 + end + end + + dead_objects = @active_objects - ospace + new_objects = ospace - @active_objects + live_objects = ospace & @active_objects + + MongrelDbg::trace(:objects, "COUNTS: #{dead_objects.length},#{new_objects.length},#{live_objects.length}") + + if MongrelDbg::tracing? :objects + top_20 = counts.sort{|a,b| b[1] <=> a[1]}[0..20] + MongrelDbg::trace(:objects,"TOP 20: #{top_20.inspect}") + end + + @active_objects = live_objects + new_objects + + [@active_objects, top_20] end - - dead_objects = @active_objects - ospace - new_objects = ospace - @active_objects - live_objects = ospace & @active_objects - - STDERR.puts "#{dead_objects.length},#{new_objects.length},#{live_objects.length}" + end + +end + +$open_files = {} - @active_objects = live_objects + new_objects +class IO + alias_method :orig_open, :open + alias_method :orig_close, :close + + def open(*arg, &blk) + $open_files[self] = args.inspect + orig_open(*arg,&blk) end + def close(*arg,&blk) + $open_files.delete self + orig_close(*arg,&blk) + end end + +module Kernel + alias_method :orig_open, :open + + def open(*arg, &blk) + $open_files[self] = arg[0] + orig_open(*arg,&blk) + end + + def log_open_files + Class.stopit do + open_counts = {} + $open_files.each do |f,args| + open_counts[args] ||= 0 + open_counts[args] += 1 + end + MongrelDbg::trace(:files, open_counts.to_yaml) + end + end +end + + + class Class alias_method :orig_new, :new @@count = 0 - @@stoppit = false + @@stopit = false @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} def new(*arg,&blk) - unless @@stoppit - @@stoppit = true + unless @@stopit + @@stopit = true @@count += 1 - @@class_caller_count[self][caller[0]] += 1 - @@stoppit = false + @@class_caller_count[self][caller.join("\n\t")] += 1 + @@stopit = false end orig_new(*arg,&blk) end - def Class.report_object_creations - @@stoppit = true - puts "Number of objects created = #{@@count}" - - total = Hash.new(0) - - @@class_caller_count.each_key do |klass| - caller_count = @@class_caller_count[klass] - caller_count.each_value do |count| - total[klass] += count - end - end - - klass_list = total.keys.sort{|klass_a, klass_b| - a = total[klass_a] - b = total[klass_b] - if a != b - -1* (a <=> b) - else - klass_a.to_s <=> klass_b.to_s + def Class.report_object_creations(out=$stderr, more_than=20) + Class.stopit do + out.puts "Number of objects created = #{@@count}" + + total = Hash.new(0) + + @@class_caller_count.each_key do |klass| + caller_count = @@class_caller_count[klass] + caller_count.each_value do |count| + total[klass] += count + end end - } - klass_list.each do |klass| - puts "#{total[klass]}\t#{klass} objects created." - caller_count = @@class_caller_count[ klass] - caller_count.keys.sort_by{|call| -1*caller_count[call]}.each do |call| - puts "\t#{call}\tCreated #{caller_count[call]} #{klass} objects." + + klass_list = total.keys.sort{|klass_a, klass_b| + a = total[klass_a] + b = total[klass_b] + if a != b + -1* (a <=> b) + else + klass_a.to_s <=> klass_b.to_s + end + } + + below_count = 0 + + klass_list.each do |klass| + below_calls = 0 + if total[klass] > more_than + out.puts "#{total[klass]}\t#{klass} objects created." + caller_count = @@class_caller_count[ klass] + caller_count.keys.sort_by{|call| -1*caller_count[call]}.each do |call| + if caller_count[call] > more_than + out.puts "\t** #{caller_count[call]} #{klass} objects AT:" + out.puts "\t#{call}\n\n" + else + below_calls += 1 + end + end + out.puts "\t#{below_calls} more objects had calls less that #{more_than} limit.\n\n" if below_calls > 0 + else + below_count += 1 + end end - puts + + out.puts "\t** #{below_count} More objects were created but the count was below the #{more_than} limit." if below_count > 0 end end def Class.reset_object_creations + Class.stopit do + @@count = 0 + @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} + end + end + + def Class.stopit @@stopit = true - @@count = 0 - @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} - @@stoppit = false + yield + @@stopit = false + end + +end + + +module RequestLog + class Files < GemPlugin::Plugin "/handlers" + include Mongrel::HttpHandlerPlugin + + def process(request, response) + MongrelDbg::trace(:files, "#{Time.now} FILES OPEN BEFORE REQUEST #{request.params['PATH_INFO']}") + log_open_files + end + + end + + class Objects < GemPlugin::Plugin "/handlers" + include Mongrel::HttpHandlerPlugin + + def process(request, response) + MongrelDbg::trace(:objects, "#{Time.now} OBJECT STATS BEFORE REQUEST #{request.params['PATH_INFO']}") + ObjectTracker.sample + end + + end + + + class Params < GemPlugin::Plugin "/handlers" + include Mongrel::HttpHandlerPlugin + + def process(request, response) + MongrelDbg::trace(:rails, "#{Time.now} REQUEST #{request.params['PATH_INFO']}") + MongrelDbg::trace(:rails, request.params.to_yaml) + end + end end +END { +open("log/mongrel_debug/object_tracking.log", "w") {|f| Class.report_object_creations(f) } +MongrelDbg::trace(:files, "FILES OPEN AT EXIT") +log_open_files +} diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb index 808d7d3..8dc42a0 100644 --- a/lib/mongrel/rails.rb +++ b/lib/mongrel/rails.rb @@ -1,159 +1,176 @@ require 'mongrel' require 'cgi' -# Creates Rails specific configuration options for people to use -# instead of the base Configurator. -class RailsConfigurator < Mongrel::Configurator +module Mongrel + module Rails - # Used instead of Mongrel::Configurator.uri to setup - # a rails application at a particular URI. Requires - # the following options: - # - # * :docroot => The public dir to serve from. - # * :environment => Rails environment to use. - # - # And understands the following optional settings: - # - # * :mime => A map of mime types. - # - # Because of how Rails is designed you can only have - # one installed per Ruby interpreter (talk to them - # about thread safety). This function will abort - # with an exception if called more than once. - def rails(location, options={}) - ops = resolve_defaults(options) - - # fix up some defaults - ops[:environment] ||= "development" - ops[:docroot] ||= "public" - ops[:mime] ||= {} - - if @rails_handler - raise "You can only register one RailsHandler for the whole Ruby interpreter. Complain to the ordained Rails core about thread safety." - end - - $orig_dollar_quote = $".clone - ENV['RAILS_ENV'] = ops[:environment] - require 'config/environment' - require 'dispatcher' - require 'mongrel/rails' - - @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime]) - end - - - # Reloads rails. This isn't too reliable really, but - # should work for most minimal reload purposes. Only reliable - # way it so stop then start the process. - def reload! - if not @rails_handler - raise "Rails was not configured. Read the docs for RailsConfigurator." - end - - STDERR.puts "Reloading rails..." - @rails_handler.reload! - STDERR.puts "Done reloading rails." - - 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) + # Creates Rails specific configuration options for people to use + # instead of the base Configurator. + class RailsConfigurator < Mongrel::Configurator + + # Creates a single rails handler and returns it so you + # can add it to a uri. You can actually attach it to + # as many URIs as you want, but this returns the + # same RailsHandler for each call. + # + # Requires the following options: + # + # * :docroot => The public dir to serve from. + # * :environment => Rails environment to use. + # + # And understands the following optional settings: + # + # * :mime => A map of mime types. + # + # Because of how Rails is designed you can only have + # one installed per Ruby interpreter (talk to them + # about thread safety). Because of this the first + # time you call this function it does all the config + # needed to get your rails working. After that + # it returns the one handler you've configured. + # This lets you attach Rails to any URI (and mulitple) + # you want, but still protects you from threads destroying + # your handler. + def rails(options={}) + + return @rails_handler if @rails_handler + + ops = resolve_defaults(options) + + # fix up some defaults + ops[:environment] ||= "development" + ops[:docroot] ||= "public" + ops[:mime] ||= {} + + + $orig_dollar_quote = $".clone + ENV['RAILS_ENV'] = ops[:environment] + require 'config/environment' + require 'dispatcher' + require 'mongrel/rails' + + @rails_handler = RailsHandler.new(ops[:docroot], ops[:mime]) + end + + + # Reloads rails. This isn't too reliable really, but + # should work for most minimal reload purposes. Only reliable + # way it so stop then start the process. + def reload! + if not @rails_handler + raise "Rails was not configured. Read the docs for RailsConfigurator." 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") + + log "Reloading rails..." + @rails_handler.reload! + log "Done reloading rails." + end - end - end - - - def reload! - @guard.synchronize do - $".replace $orig_dollar_quote - GC.start - Dispatcher.reset_application! - ActionController::Routing::Routes.reload - end - end -end - - -if $mongrel_debugging - - # Tweak the rails handler to allow for tracing - class RailsHandler - alias :real_process :process - - def process(request, response) - MongrelDbg::trace(:rails, "REQUEST #{Time.now}\n" + request.params.to_yaml) - real_process(request, response) + # Takes the exact same configuration as Mongrel::Configurator (and actually calls that) + # but sets up the additional HUP handler to call reload!. + def setup_rails_signals(options={}) + ops = resolve_defaults(options) + + if RUBY_PLATFORM !~ /mswin/ + setup_signals(options) + + # rails reload + trap("HUP") { + log "HUP signal received." + reload! + } + + log "Rails signals registered. HUP => reload (without restart). It might not work well." + else + log "WARNING: Rails does not support signals on Win32." + end + end - MongrelDbg::trace(:rails, "REQUEST #{Time.now}\n" + request.params.to_yaml) + # 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 diff --git a/lib/mongrel/stats.rb b/lib/mongrel/stats.rb new file mode 100644 index 0000000..a29a018 --- /dev/null +++ b/lib/mongrel/stats.rb @@ -0,0 +1,71 @@ +# A very simple little class for doing some basic fast statistics sampling. +# You feed it either samples of numeric data you want measured or you call +# Stats.tick to get it to add a time delta between the last time you called it. +# When you're done either call sum, sumsq, n, min, max, mean or sd to get +# the information. The other option is to just call dump and see everything. +# +# It does all of this very fast and doesn't take up any memory since the samples +# are not stored but instead all the values are calculated on the fly. +class Stats + attr_reader :sum, :sumsq, :n, :min, :max + + def initialize(name) + @name = name + reset + end + + # Resets the internal counters so you can start sampling again. + def reset + @sum = 0.0 + @sumsq = 0.0 + @last_time = Time.new + @n = 0.0 + @min = 0.0 + @max = 0.0 + end + + # Adds a sampling to the calculations. + def sample(s) + @sum += s + @sumsq += s * s + if @n == 0 + @min = @max = s + else + @min = s if @min > s + @max = s if @max < s + end + @n+=1 + end + + # Dump this Stats object with an optional additional message. + def dump(msg = "") + STDERR.puts "[#{@name}] #{msg} : SUM=#@sum, SUMSQ=#@sumsq, N=#@n, MEAN=#{mean}, SD=#{sd}, MIN=#@min, MAX=#@max" + end + + # Calculates and returns the mean for the data passed so far. + def mean + @sum / @n + end + + # Calculates the standard deviation of the data so far. + def sd + # (sqrt( ((s).sumsq - ( (s).sum * (s).sum / (s).n)) / ((s).n-1) )) + Math.sqrt( (@sumsq - ( @sum * @sum / @n)) / (@n-1) ) + end + + + # Adds a time delta between now and the last time you called this. This + # will give you the average time between two activities. + # + # An example is: + # + # t = Stats.new("do_stuff") + # 10000.times { do_stuff(); t.tick } + # t.dump("time") + # + def tick + now = Time.now + sample(now - @last_time) + @last_time = now + end +end diff --git a/test/test_debug.rb b/test/test_debug.rb index 7d0cb45..9c01a1c 100644 --- a/test/test_debug.rb +++ b/test/test_debug.rb @@ -1,23 +1,27 @@ +require 'fileutils' +FileUtils.mkdir_p "log/mongrel_debug" + require 'test/unit' require 'mongrel/rails' require 'mongrel/debug' -require 'fileutils' + class MongrelDbgTest < Test::Unit::TestCase def setup - FileUtils.rm_rf "mongrel_debug" + FileUtils.rm_rf "log/mongrel_debug" MongrelDbg::configure end + def test_tracing_to_log MongrelDbg::begin_trace(:rails) MongrelDbg::trace(:rails, "Good stuff") MongrelDbg::end_trace(:rails) - assert File.exist?("mongrel_debug"), "Didn't make logging directory" - assert File.exist?("mongrel_debug/rails.log"), "Didn't make the rails.log file" - assert File.size("mongrel_debug/rails.log") > 0, "Didn't write anything to the log." + assert File.exist?("log/mongrel_debug"), "Didn't make logging directory" + assert File.exist?("log/mongrel_debug/rails.log"), "Didn't make the rails.log file" + assert File.size("log/mongrel_debug/rails.log") > 0, "Didn't write anything to the log." Class.report_object_creations Class.reset_object_creations diff --git a/test/test_stats.rb b/test/test_stats.rb new file mode 100644 index 0000000..4d6bc1a --- /dev/null +++ b/test/test_stats.rb @@ -0,0 +1,28 @@ +require 'test/unit' +require 'mongrel/stats' + +class StatsTest < Test::Unit::TestCase + + def test_sampling_speed + s = Stats.new("test") + t = Stats.new("time") + + 10000.times { s.sample(rand(20)); t.tick } + + s.dump("FIRST") + t.dump("FIRST") + + old_mean = s.mean + old_sd = s.sd + + s.reset + t.reset + 10000.times { s.sample(rand(20)); t.tick } + + s.dump("SECOND") + t.dump("SECOND") + assert_not_equal old_mean, s.mean + assert_not_equal old_mean, s.sd + end + +end -- cgit v1.2.3-24-ge0c7