about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-03-26 20:01:50 +0000
committerzedshaw <zedshaw@19e92222-5c0b-0410-8929-a290d50e31e9>2006-03-26 20:01:50 +0000
commitf2b53a3a4b1ddacac4fc18ccbe2b016194a50777 (patch)
tree10489b9db362c71558d16d85af8bcec4139c9266
parentf4a5c938d461d9c5dc17f521c9efaaf352b931fa (diff)
downloadunicorn-f2b53a3a4b1ddacac4fc18ccbe2b016194a50777.tar.gz
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@122 19e92222-5c0b-0410-8929-a290d50e31e9
-rw-r--r--bin/mongrel_rails160
-rw-r--r--lib/mongrel.rb108
-rw-r--r--lib/mongrel/command.rb2
-rw-r--r--lib/mongrel/debug.rb233
-rw-r--r--lib/mongrel/rails.rb313
-rw-r--r--lib/mongrel/stats.rb71
-rw-r--r--test/test_debug.rb14
-rw-r--r--test/test_stats.rb28
8 files changed, 592 insertions, 337 deletions
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