diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/mongrel.rb | 97 | ||||
-rw-r--r-- | lib/mongrel/debug.rb | 144 | ||||
-rw-r--r-- | lib/mongrel/handlers.rb | 23 | ||||
-rw-r--r-- | lib/mongrel/rails.rb | 74 |
4 files changed, 315 insertions, 23 deletions
diff --git a/lib/mongrel.rb b/lib/mongrel.rb index b501c26..cd94b9b 100644 --- a/lib/mongrel.rb +++ b/lib/mongrel.rb @@ -238,12 +238,19 @@ module Mongrel attr_reader :header attr_reader :status attr_writer :status + attr_reader :body_sent + attr_reader :header_sent + attr_reader :status_sent - def initialize(socket) + def initialize(socket, filter = nil) @socket = socket @body = StringIO.new @status = 404 @header = HeaderOut.new(StringIO.new) + @filter = filter + @body_sent = false + @header_sent = false + @status_sent = false end # Receives a block passing it the header and body for you to work with. @@ -257,27 +264,37 @@ module Mongrel end # Primarily used in exception handling to reset the response output in order to write - # an alternative response. + # an alternative response. It will abort with an exception if you have already + # sent the header or the body. This is pretty catastrophic actually. def reset - @header.out.rewind - @body.rewind + if @body_sent + raise "You ahve already sent the request body." + elsif @header_sent + raise "You have already sent the request headers." + else + @header.out.rewind + @body.rewind + end end def send_status status = "HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status]}\r\nContent-Length: #{@body.length}\r\nConnection: close\r\n" @socket.write(status) + @status_sent = true end def send_header @header.out.rewind @socket.write(@header.out.read) @socket.write("\r\n") + @header_sent = true end def send_body @body.rewind # connection: close is also added to ensure that the client does not pipeline. @socket.write(@body.read) + @body_sent = true end # This takes whatever has been done to header and body and then writes it in the @@ -287,6 +304,10 @@ module Mongrel send_header send_body end + + def done + @status_sent && @header_sent && @body_sent + end end @@ -317,10 +338,13 @@ module Mongrel # 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. The server will return a "503 Service Unavailable" - # message when this happens so that they know to come back. The timeout - # parameter is a sleep timeout that is placed between socket.accept calls - # in order to give the server a cheap throttle time. It defaults to 0 and + # 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. def initialize(host, port, num_processors=(2**30-1), timeout=0) @socket = TCPServer.new(host, port) @@ -349,15 +373,20 @@ module Mongrel nread = parser.execute(params, data) if parser.finished? - script_name, path_info, handler = @classifier.resolve(params[Const::REQUEST_URI]) + script_name, path_info, handlers = @classifier.resolve(params[Const::REQUEST_URI]) - if handler + if handlers params[Const::PATH_INFO] = path_info params[Const::SCRIPT_NAME] = script_name - request = HttpRequest.new(params, data[nread ... data.length], client) + request = HttpRequest.new(params, data[nread ... data.length], client) response = HttpResponse.new(client) - handler.process(request, response) + + handlers.each do |handler| + handler.process(request, response) + break if response.done + end + else client.write(Const::ERROR_404_RESPONSE) end @@ -384,30 +413,44 @@ module Mongrel end end + # 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. + def reap_dead_workers(worker_list) + mark = Time.now + worker_list.each do |w| + if mark - w[:started_on] > 10 * @timeout + STDERR.puts "Thread #{w.inspect} is too old, killing." + w.raise(StopServer.new("Timed out thread.")) + end + end + end + + # 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 run - BasicSocket.do_not_reverse_lookup=true - + @acceptor = Thread.new do while true begin client = @socket.accept - - if @workers.list.length >= @num_processors - STDERR.puts "Server overloaded with #{@workers.list.length} active processors." - client.write(Const::ERROR_503_RESPONSE) + worker_list = @workers.list + if worker_list.length >= @num_processors + STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max)." 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 if @timeout > 0 + sleep @timeout/100 if @timeout > 0 end rescue StopServer STDERR.puts "Server stopped. Exiting." @@ -437,11 +480,19 @@ module Mongrel # found in the prefix of a request then your handler's HttpHandler::process method # is called. See Mongrel::URIClassifier#register for more information. def register(uri, handler) - @classifier.register(uri, handler) + script_name, path_info, handlers = @classifier.resolve(uri) + + if not handlers or (path_info and path_info != "/" and path_info.length == 0) + # new uri that is just longer + @classifier.register(uri, [handler]) + else + handlers << handler + end end - # Removes any handler registered at the given URI. See Mongrel::URIClassifier#unregister - # for more information. + # 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 @@ -636,7 +687,7 @@ module Mongrel # are merged with the defaults prior to passing them in. def plugin(name, options={}) ops = resolve_defaults(options) - GemPlugin::Manager.instance.create(plugin, ops) + GemPlugin::Manager.instance.create(name, ops) end diff --git a/lib/mongrel/debug.rb b/lib/mongrel/debug.rb new file mode 100644 index 0000000..8b2dd5c --- /dev/null +++ b/lib/mongrel/debug.rb @@ -0,0 +1,144 @@ +require 'logger' +require 'set' + + +$mongrel_debugging=true + +module MongrelDbg + SETTINGS = { :tracing => {}} + LOGGING = { } + + def MongrelDbg::configure(log_dir = "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] + LOGGING[target].log(Logger::DEBUG, message) + end + end + + def MongrelDbg::begin_trace(target) + SETTINGS[:tracing][target] = true + if not LOGGING[target] + LOGGING[target] = Logger.new(File.join(@log_dir, "#{target.to_s}.log")) + end + MongrelDbg::trace(target, "TRACING ON #{Time.now}") + end + + def MongrelDbg::end_trace(target) + SETTINGS[:tracing][target] = false + MongrelDbg::trace(target, "TRACING OFF #{Time.now}") + LOGGING[target].close + LOGGING[target] = nil + end +end + + +module ObjectTracker + @active_objects = nil + @live_object_tracking = false + + 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 + @live_object_tracking = false + end + + def ObjectTracker.sample + ospace = Set.new + ObjectSpace.each_object do |obj| + ospace << obj.object_id + 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}" + + @active_objects = live_objects + new_objects + end + +end + +class Class + alias_method :orig_new, :new + + @@count = 0 + @@stoppit = false + @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} + + def new(*arg,&blk) + unless @@stoppit + @@stoppit = true + @@count += 1 + @@class_caller_count[self][caller[0]] += 1 + @@stoppit = 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 + 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." + end + puts + end + end + + def Class.reset_object_creations + @@stopit = true + @@count = 0 + @@class_caller_count = Hash.new{|hash,key| hash[key] = Hash.new(0)} + @@stoppit = false + end +end + + diff --git a/lib/mongrel/handlers.rb b/lib/mongrel/handlers.rb index 609a252..7d817bb 100644 --- a/lib/mongrel/handlers.rb +++ b/lib/mongrel/handlers.rb @@ -5,9 +5,32 @@ module Mongrel # just the minimum necessary for you to handle a request and shoot back # a response. Look at the HttpRequest and HttpResponse objects for how # to use them. + # + # This is used for very simple handlers that don't require much to operate. + # More extensive plugins or those you intend to distribute as GemPlugins + # should be implemented using the HttpHandlerPlugin mixin. + # class HttpHandler + + def process(request, response) + end + end + + + # This is used when your handler is implemented as a GemPlugin. + # The plugin always takes an options hash which you can modify + # and then access later. They are stored by default for + # the process method later. + module HttpHandlerPlugin + attr_reader :options + + def initialize(options={}) + @options = options + end + def process(request, response) end + end diff --git a/lib/mongrel/rails.rb b/lib/mongrel/rails.rb index fb4e172..808d7d3 100644 --- a/lib/mongrel/rails.rb +++ b/lib/mongrel/rails.rb @@ -1,6 +1,62 @@ require 'mongrel' require 'cgi' +# Creates Rails specific configuration options for people to use +# instead of the base Configurator. +class RailsConfigurator < Mongrel::Configurator + + # 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 @@ -83,3 +139,21 @@ class RailsHandler < Mongrel::HttpHandler 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) + + MongrelDbg::trace(:rails, "REQUEST #{Time.now}\n" + request.params.to_yaml) + end + end + +end |