about summary refs log tree commit homepage
path: root/lib/mongrel.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/mongrel.rb')
-rw-r--r--lib/mongrel.rb110
1 files changed, 46 insertions, 64 deletions
diff --git a/lib/mongrel.rb b/lib/mongrel.rb
index 990a8eb..424b7f0 100644
--- a/lib/mongrel.rb
+++ b/lib/mongrel.rb
@@ -8,6 +8,7 @@ require 'etc'
 require 'uri'
 require 'stringio'
 require 'fcntl'
+require 'logger'
 
 # Compiled Mongrel extension
 require 'http11'
@@ -25,11 +26,21 @@ require 'mongrel/const'
 require 'mongrel/http_request'
 require 'mongrel/header_out'
 require 'mongrel/http_response'
+require 'mongrel/semaphore'
 
 # Mongrel module containing all of the classes (include C extensions) for running
 # a Mongrel web server.  It contains a minimalist HTTP server with just enough
 # functionality to service web application requests fast as possible.
 module Mongrel
+  class << self
+    # A logger instance that conforms to the API of stdlib's Logger.
+    attr_accessor :logger
+    
+    # By default, will return an instance of stdlib's Logger logging to STDERR
+    def logger
+      @logger ||= Logger.new STDERR
+    end
+  end
 
   # Used to stop the HttpServer via Thread.raise.
   class StopServer < Exception; end
@@ -69,13 +80,20 @@ module Mongrel
     attr_reader :port
     attr_reader :throttle
     attr_reader :timeout
-    attr_reader :num_processors
+    attr_reader :max_queued_threads
+    
+    DEFAULTS = {
+      :max_queued_threads => 20,
+      :max_concurrent_threads => 20,
+      :throttle => 0,
+      :timeout => 60
+    }
 
     # Creates a working server on host:port (strange things happen if port isn't a Number).
     # Use HttpServer::run to start the server and HttpServer.acceptor.join to
     # join the thread that's processing incoming requests on the socket.
     #
-    # The num_processors optional argument is the maximum number of concurrent
+    # The max_queued_threads optional argument is the maximum number of concurrent
     # processors to accept, anything over this is closed immediately to maintain
     # server processing performance.  This may seem mean but it is the most efficient
     # way to deal with overload.  Other schemes involve still parsing the client's request
@@ -84,20 +102,21 @@ module Mongrel
     # The throttle parameter is a sleep timeout (in hundredths of a second) that is placed between
     # socket.accept calls in order to give the server a cheap throttle time.  It defaults to 0 and
     # actually if it is 0 then the sleep is not done at all.
-    def initialize(host, port, app, opts = {})
+    def initialize(host, port, app, options = {})
+      options = DEFAULTS.merge(options)
+
       tries = 0
       @socket = TCPServer.new(host, port)
       if defined?(Fcntl::FD_CLOEXEC)
         @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
       end
-      @host = host
-      @port = port
+      @host, @port, @app = host, port, app
       @workers = ThreadGroup.new
-      # Set default opts
-      @app = app
-      @num_processors = opts.delete(:num_processors) || 950
-      @throttle       = (opts.delete(:throttle) || 0) / 100
-      @timeout        = opts.delete(:timeout) || 60
+
+      @throttle = options[:throttle] / 100.0
+      @timeout = options[:timeout]
+      @max_queued_threads = options[:max_queued_threads]
+      @max_concurrent_threads = options[:max_concurrent_threads]
     end
 
     # Does the majority of the IO processing.  It has been written in Ruby using
@@ -162,21 +181,21 @@ module Mongrel
       rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
         client.close rescue nil
       rescue HttpParserError => e
-        STDERR.puts "#{Time.now}: HTTP parse error, malformed request (#{params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #{e.inspect}"
-        STDERR.puts "#{Time.now}: REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n"
+        Mongrel.logger.error "#{Time.now}: HTTP parse error, malformed request (#{params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #{e.inspect}"
+        Mongrel.logger.error "#{Time.now}: REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n"
       rescue Errno::EMFILE
         reap_dead_workers('too many files')
       rescue Object => e
-        STDERR.puts "#{Time.now}: Read error: #{e.inspect}"
-        STDERR.puts e.backtrace.join("\n")
+        Mongrel.logger.error "#{Time.now}: Read error: #{e.inspect}"
+        Mongrel.logger.error e.backtrace.join("\n")
       ensure
         begin
           client.close
         rescue IOError
           # Already closed
         rescue Object => e
-          STDERR.puts "#{Time.now}: Client error: #{e.inspect}"
-          STDERR.puts e.backtrace.join("\n")
+          Mongrel.logger.error "#{Time.now}: Client error: #{e.inspect}"
+          Mongrel.logger.error e.backtrace.join("\n")
         end
         request.body.close! if request and request.body.class == Tempfile
       end
@@ -188,14 +207,14 @@ module Mongrel
     # after the reap is done.  It only runs if there are workers to reap.
     def reap_dead_workers(reason='unknown')
       if @workers.list.length > 0
-        STDERR.puts "#{Time.now}: Reaping #{@workers.list.length} threads for slow workers because of '#{reason}'"
+        Mongrel.logger.info "#{Time.now}: Reaping #{@workers.list.length} threads for slow workers because of '#{reason}'"
         error_msg = "Mongrel timed out this thread: #{reason}"
         mark = Time.now
         @workers.list.each do |worker|
           worker[:started_on] = Time.now if not worker[:started_on]
 
           if mark - worker[:started_on] > @timeout + @throttle
-            STDERR.puts "Thread #{worker.inspect} is too old, killing."
+            Mongrel.logger.info "Thread #{worker.inspect} is too old, killing."
             worker.raise(TimeoutError.new(error_msg))
           end
         end
@@ -210,7 +229,7 @@ module Mongrel
     # that much longer.
     def graceful_shutdown
       while reap_dead_workers("shutdown") > 0
-        STDERR.puts "Waiting for #{@workers.list.length} requests to finish, could take #{@timeout + @throttle} seconds."
+        Mongrel.logger.info "Waiting for #{@workers.list.length} requests to finish, could take #{@timeout + @throttle} seconds."
         sleep @timeout / 10
       end
     end
@@ -235,6 +254,7 @@ module Mongrel
     # Runs the thing.  It returns the thread used so you can "join" it.  You can also
     # access the HttpServer::acceptor attribute to get the thread later.
     def start!
+      semaphore = Semaphore.new(@max_concurrent_threads)
       BasicSocket.do_not_reverse_lookup=true
 
       configure_socket_options
@@ -254,12 +274,12 @@ module Mongrel
               end
   
               worker_list = @workers.list
-              if worker_list.length >= @num_processors
-                STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max). Dropping connection."
+              if worker_list.length >= @max_queued_threads
+                Mongrel.logger.error "Server overloaded with #{worker_list.length} processors (#@max_queued_threads max). Dropping connection."
                 client.close rescue nil
                 reap_dead_workers("max processors")
               else
-                thread = Thread.new(client) {|c| process_client(c) }
+                thread = Thread.new(client) {|c| semaphore.synchronize { process_client(c) } }
                 thread[:started_on] = Time.now
                 @workers.add(thread)
   
@@ -274,63 +294,25 @@ module Mongrel
               # client closed the socket even before accept
               client.close rescue nil
             rescue Object => e
-              STDERR.puts "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
-              STDERR.puts e.backtrace.join("\n")
+              Mongrel.logger.error "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
+              Mongrel.logger.error e.backtrace.join("\n")
             end
           end
           graceful_shutdown
         ensure
           @socket.close
-          # STDERR.puts "#{Time.now}: Closed socket."
+          # Mongrel.logger.info "#{Time.now}: Closed socket."
         end
       end
 
       return @acceptor
     end
 
-    # Simply registers a handler with the internal URIClassifier.  When the URI is
-    # found in the prefix of a request then your handler's HttpHandler::process method
-    # is called.  See Mongrel::URIClassifier#register for more information.
-    #
-    # If you set in_front=true then the passed in handler will be put in the front of the list
-    # for that particular URI. Otherwise it's placed at the end of the list.
-    def register(uri, handler, in_front=false)
-      begin
-        @classifier.register(uri, [handler])
-      rescue URIClassifier::RegistrationError => e
-        handlers = @classifier.resolve(uri)[2]
-        if handlers
-          # Already registered
-          method_name = in_front ? 'unshift' : 'push'
-          handlers.send(method_name, handler)
-        else
-          raise
-        end
-      end
-      handler.listener = self
-    end
-
-    # Removes any handlers registered at the given URI.  See Mongrel::URIClassifier#unregister
-    # for more information.  Remember this removes them *all* so the entire
-    # processing chain goes away.
-    def unregister(uri)
-      @classifier.unregister(uri)
-    end
-
     # Stops the acceptor thread and then causes the worker threads to finish
     # off the request queue before finally exiting.
     def stop(synchronous=false)
       @acceptor.raise(StopServer.new)
-
-      if synchronous
-        sleep(0.5) while @acceptor.alive?
-      end
+      (sleep(0.5) while @acceptor.alive?) if synchronous
     end
-
   end
 end
-
-# Load experimental library, if present. We put it here so it can override anything
-# in regular Mongrel.
-
-$LOAD_PATH.unshift 'projects/mongrel_experimental/lib/'