about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--ext/http11/http11.c12
-rw-r--r--lib/mongrel.rb85
-rw-r--r--test/test_http11.rb51
3 files changed, 95 insertions, 53 deletions
diff --git a/ext/http11/http11.c b/ext/http11/http11.c
index 3755d65..cbfb55e 100644
--- a/ext/http11/http11.c
+++ b/ext/http11/http11.c
@@ -152,6 +152,10 @@ VALUE HttpParser_finish(VALUE self)
  * returning an Integer to indicate how much of the data has been read.  No matter
  * what the return value, you should call HttpParser#finished? and HttpParser#error?
  * to figure out if it's done parsing or there was an error.
+ *
+ * This function now throws an exception when there is a parsing error.  This makes
+ * the logic for working with the parser much easier.  You can still test for an
+ * error, but now you need to wrap the parser with an exception handling block.
  */
 VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data)
 {
@@ -160,8 +164,12 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data)
 
   http->data = (void *)req_hash;
   http_parser_execute(http, RSTRING(data)->ptr, RSTRING(data)->len);
-  
-  return INT2FIX(http_parser_nread(http));
+
+  if(http_parser_has_error(http)) {
+    rb_raise(rb_eStandardError, "HTTP Parsing failure");
+  } else {
+    return INT2FIX(http_parser_nread(http));
+  }
 }
 
 
diff --git a/lib/mongrel.rb b/lib/mongrel.rb
index 5c5bafb..28b88a2 100644
--- a/lib/mongrel.rb
+++ b/lib/mongrel.rb
@@ -121,8 +121,9 @@ module Mongrel
     def finished
       @header.out.rewind
       @body.rewind
-
-      @socket.write("HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status]}\r\nContent-Length: #{@body.length}\r\n")
+      
+      # connection: close is also added to ensure that the client does not pipeline.
+      @socket.write("HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status]}\r\nContent-Length: #{@body.length}\r\nConnection: close\r\n")
       @socket.write(@header.out.read)
       @socket.write("\r\n")
       @socket.write(@body.read)
@@ -159,16 +160,13 @@ module Mongrel
     end
 
   end
-  
+
 
   # This is the main driver of Mongrel, while the Mognrel::HttpParser and Mongrel::URIClassifier
   # make up the majority of how the server functions.  It's a very simple class that just
   # has a thread accepting connections and a simple HttpServer.process_client function
   # to do the heavy lifting with the IO and Ruby.  
   #
-  # *NOTE:* The process_client function used threads at one time but that proved to have
-  # stability issues on Mac OSX.  Actually, Ruby in general has stability issues on Mac OSX.
-  #
   # You use it by doing the following:
   #
   #   server = HttpServer.new("0.0.0.0", 3000)
@@ -177,64 +175,93 @@ module Mongrel
   #
   # The last line can be just server.run if you don't want to join the thread used.
   # If you don't though Ruby will mysteriously just exit on you.
+  #
+  # Ruby's thread implementation is "interesting" to say the least.  Experiments with
+  # *many* different types of IO processing simply cannot make a dent in it.  Future
+  # releases of Mongrel will find other creative ways to make threads faster, but don't
+  # hold your breath until Ruby 1.9 is actually finally useful.
   class HttpServer
     attr_reader :acceptor
 
     # The standard empty 404 response for bad requests.  Use Error4040Handler for custom stuff.
-    ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Type: text/plain\r\nServer: Mongrel/0.1\r\n\r\n"
+    ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Mongrel/0.2\r\n\r\nNOT FOUND"
+    ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY"
 
-    # For now we just read 2k chunks.  Not optimal at all.
-    CHUNK_SIZE=2048
+    # The basic max request size we'll try to read.
+    CHUNK_SIZE=(16 * 1024)
     
     # Creates a working server on host:port (strange things happen if port isn't a Number).
     # Use HttpServer::run to start the server.
-    def initialize(host, port)
+    #
+    # The num_processors variable has varying affects on how requests are processed.  You'd
+    # think adding more processing threads (processors) would make the server faster, but
+    # that's just not true.  There's actually an effect of how Ruby does threads such that
+    # the more processors waiting on the request queue, the slower the system is to handle
+    # each request.  But, the lower the number of processors the fewer concurrent responses
+    # the server can make.
+    #
+    # 20 is the default number of processors and is based on experimentation on a few
+    # systems.  If you find that you overload Mongrel too much
+    # try changing it higher.  If you find that responses are way too slow
+    # try lowering it (after you've tuned your stuff of course).
+    # Future versions of Mongrel will make this more dynamic (hopefully).
+    def initialize(host, port, num_processors=20)
       @socket = TCPServer.new(host, port)
       @classifier = URIClassifier.new
+      @req_queue = Queue.new
+      num_processors.times {|i| Thread.new do
+          while client = @req_queue.deq
+            process_client(client)
+          end
+        end
+      }
     end
     
-    # Used internally to process an accepted client.  It uses HttpParser and URIClassifier
-    # (in ext/http11/http11.c) to do the heavy work, and mostly just does a hack job
-    # at some simple IO.  Future releases will target this area mostly.
+
+    # Does the majority of the IO processing.  It has been written in Ruby using
+    # about 7 different IO processing strategies and no matter how it's done
+    # the performance just does not improve.  Ruby's use of select to implement
+    # threads means that it will most likely never improve, so the only remaining
+    # approach is to write all or some of this function in C.  That will be the
+    # focus of future releases.
     def process_client(client)
       begin
         parser = HttpParser.new
         params = {}
-        data = ""
-        
+        data = client.readpartial(CHUNK_SIZE)
+
         while true
-          data << client.readpartial(CHUNK_SIZE)
-          
           nread = parser.execute(params, data)
-          
-          if parser.error?
-            STDERR.puts "parser error:"
-            STDERR.puts data
-            break
-          elsif parser.finished?
+          if parser.finished?
             script_name, path_info, handler = @classifier.resolve(params["PATH_INFO"])
-            
+
             if handler
               params['PATH_INFO'] = path_info
               params['SCRIPT_NAME'] = script_name
-              
+
               request = HttpRequest.new(params, data[nread ... data.length], client)
               response = HttpResponse.new(client)
-              
               handler.process(request, response)
             else
               client.write(ERROR_404_RESPONSE)
             end
-            
+
             break
           else
             # gotta stream and read again until we can get the parser to be character safe
             # TODO: make this more efficient since this means we're parsing a lot repeatedly
             parser.reset
+            data << client.readpartial(CHUNK_SIZE)
           end
         end
+      rescue EOFError
+        # ignored
+      rescue Errno::ECONNRESET
+        # ignored
+      rescue Errno::EPIPE
+        # ignored
       rescue => details
-        STDERR.puts "ERROR: #{details}"
+        STDERR.puts "ERROR(#{details.class}): #{details}"
         STDERR.puts details.backtrace.join("\n")
       ensure
         client.close
@@ -246,7 +273,7 @@ module Mongrel
     def run
       @acceptor = Thread.new do
         while true
-          process_client(@socket.accept)
+          @req_queue << @socket.accept
         end
       end
     end
diff --git a/test/test_http11.rb b/test/test_http11.rb
index 8f07900..60d020b 100644
--- a/test/test_http11.rb
+++ b/test/test_http11.rb
@@ -4,28 +4,35 @@ require 'http11'
 
 class HttpParserTest < Test::Unit::TestCase
     
-    def test_parse_simple
-            parser = HttpParser.new
-            req = {}
-            http = "GET / HTTP/1.1\r\n\r\n"
-            nread = parser.execute(req, http);
-            assert nread == http.length, "Failed to parse the full HTTP request"
-            assert parser.finished?, "Parser didn't finish"
-            assert !parser.error?, "Parser had error"
-            assert nread == parser.nread, "Number read returned from execute does not match"
-            parser.reset
-            assert parser.nread == 0, "Number read after reset should be 0"
-    end
-    
-    
-    def test_parse_error
-        parser = HttpParser.new
-        req = {}
-        bad_http = "GET / SsUTF/1.1"
-        nread = parser.execute(req, bad_http)
-        assert nread < bad_http.length, "Number read should be less than total on error"
-        assert !parser.finished?, "Parser shouldn't be finished"
-        assert parser.error?, "Parser SHOULD have error"
+  def test_parse_simple
+    parser = HttpParser.new
+    req = {}
+    http = "GET / HTTP/1.1\r\n\r\n"
+    nread = parser.execute(req, http);
+    assert nread == http.length, "Failed to parse the full HTTP request"
+    assert parser.finished?, "Parser didn't finish"
+    assert !parser.error?, "Parser had error"
+    assert nread == parser.nread, "Number read returned from execute does not match"
+    parser.reset
+    assert parser.nread == 0, "Number read after reset should be 0"
+  end
+  
+  
+  def test_parse_error
+    parser = HttpParser.new
+    req = {}
+    bad_http = "GET / SsUTF/1.1"
+
+    error = false
+    begin
+      nread = parser.execute(req, bad_http)
+    rescue => details
+      error = true
     end
+
+    assert error, "failed to throw exception"
+    assert !parser.finished?, "Parser shouldn't be finished"
+    assert parser.error?, "Parser SHOULD have error"
+  end
 end