Class: Mongrel::HttpServer

Inherits:
Object
  • Object
show all
Defined in:
lib/mongrel.rb

Overview

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.

You use it by doing the following:

server = HttpServer.new("0.0.0.0", 3000)
server.register("/stuff", MyNifterHandler.new)
server.run.join

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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host, port, num_processors = (2**30-1), timeout = 0) ⇒ HttpServer

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 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 which defeats the point of an overload handling system.

The timeout 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.



386
387
388
389
390
391
392
393
394
395
# File 'lib/mongrel.rb', line 386

def initialize(host, port, num_processors=(2**30-1), timeout=0)
  @socket = TCPServer.new(host, port) 
  @classifier = URIClassifier.new
  @host = host
  @port = port
  @workers = ThreadGroup.new
  @timeout = timeout
  @num_processors = num_processors
  @death_time = 60
end

Instance Attribute Details

#acceptorObject (readonly)

Returns the value of attribute acceptor.



367
368
369
# File 'lib/mongrel.rb', line 367

def acceptor
  @acceptor
end

#classifierObject (readonly)

Returns the value of attribute classifier.



369
370
371
# File 'lib/mongrel.rb', line 369

def classifier
  @classifier
end

#hostObject (readonly)

Returns the value of attribute host.



370
371
372
# File 'lib/mongrel.rb', line 370

def host
  @host
end

#portObject (readonly)

Returns the value of attribute port.



371
372
373
# File 'lib/mongrel.rb', line 371

def port
  @port
end

#workersObject (readonly)

Returns the value of attribute workers.



368
369
370
# File 'lib/mongrel.rb', line 368

def workers
  @workers
end

Instance Method Details

#process_client(client) ⇒ Object

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. It is currently carefully constructed to make sure that it gets the best possible performance, but anyone who thinks they can make it faster is more than welcome to take a crack at it.



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/mongrel.rb', line 403

def process_client(client)
  begin
    parser = HttpParser.new
    params = {}

    data = client.readpartial(Const::CHUNK_SIZE)

    while true
      nread = parser.execute(params, data)

      if parser.finished?
        script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_URI])

        if handlers
          params[Const::PATH_INFO] = path_info
          params[Const::SCRIPT_NAME] = script_name
          params[Const::REMOTE_ADDR] = params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last

          request = HttpRequest.new(params, data[nread ... data.length], client)
          response = HttpResponse.new(client)
          
          handlers.each do |handler|
            handler.process(request, response)
            break if response.done
          end

          if not response.done
            response.finished
          end

        else
          client.write(Const::ERROR_404_RESPONSE)
        end
        
        break #done
      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
        if data.length >= Const::MAX_HEADER
          raise HttpParserError.new("HEADER is longer than allowed, aborting client early.")
        end

        parser.reset
        data << client.readpartial(Const::CHUNK_SIZE)
      end
    end
  rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL
    # ignored
  rescue HttpParserError
    STDERR.puts "BAD CLIENT (#{client.peeraddr.last}): #$!"
    STDERR.puts "REQUEST DATA: #{data}"
  rescue => details
    STDERR.puts "ERROR: #$!"
    STDERR.puts details.backtrace.join("\n")
  ensure
    client.close
  end
end

#reap_dead_workers(worker_list) ⇒ Object

Used internally to kill off any worker threads that have taken too long to complete processing. Only called if there are too many processors currently servicing.



465
466
467
468
469
470
471
472
473
474
475
# File 'lib/mongrel.rb', line 465

def reap_dead_workers(worker_list)
  mark = Time.now
  worker_list.each do |w|
    w[:started_on] = Time.now if not w[:started_on]

    if mark - w[:started_on] > @death_time + @timeout
      STDERR.puts "Thread #{w.inspect} is too old, killing."
      w.raise(StopServer.new("Timed out thread."))
    end
  end
end

#register(uri, handler, in_front = false) ⇒ Object

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 front in the list.



538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/mongrel.rb', line 538

def register(uri, handler, in_front=false)
  script_name, path_info, handlers = @classifier.resolve(uri)

  if not handlers
    @classifier.register(uri, [handler])
  else
    if path_info.length == 0 or (script_name == Const::SLASH and path_info == Const::SLASH)
      if in_front
        handlers.unshift(handler)
      else
        handlers << handler
      end
    else
      @classifier.register(uri, [handler])
    end
  end
end

#runObject

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.



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/mongrel.rb', line 480

def run
  BasicSocket.do_not_reverse_lookup=true

  @acceptor = Thread.new do
    while true
      begin
        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). Dropping connection."
          client.close
          reap_dead_workers(worker_list)
        else
          thread = Thread.new do
            process_client(client)
          end
          
          thread[:started_on] = Time.now
          thread.priority=1
          @workers.add(thread)
          
          sleep @timeout/100 if @timeout > 0
        end
      rescue StopServer
        STDERR.puts "Server stopped.  Exiting."
        @socket.close if not @socket.closed?
        break
      rescue Errno::EMFILE
        STDERR.puts "Too many open files.  Try increasing ulimits."
        sleep 0.5
      end
    end

    # now that processing is done we feed enough false onto the request queue to get
    # each processor to exit and stop processing.

    # finally we wait until the queue is empty (but only about 10 seconds)
    @death_time = 10
    shutdown_start = Time.now
    
    while @workers.list.length > 0
      waited_for = (Time.now - shutdown_start).ceil
      STDERR.print "Shutdown waited #{waited_for} for #{@workers.list.length} requests, could take #{@death_time + @timeout} seconds.\r" if @workers.list.length > 0
      sleep 1
      reap_dead_workers(@workers.list)
    end
  end

  return @acceptor
end

#stopObject

Stops the acceptor thread and then causes the worker threads to finish off the request queue before finally exiting.



565
566
567
568
569
570
571
# File 'lib/mongrel.rb', line 565

def stop
  stopper = Thread.new do 
    exc = StopServer.new
    @acceptor.raise(exc)
  end
  stopper.priority = 10
end

#unregister(uri) ⇒ Object

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.



559
560
561
# File 'lib/mongrel.rb', line 559

def unregister(uri)
  @classifier.unregister(uri)
end