From b18cfae4b119dca1e1bfe2d84f61e15c978db353 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Thu, 1 Dec 2011 15:23:14 -0800 Subject: [PATCH] Write a bunch of documentation --- lib/puma/cli.rb | 24 ++++++++++++ lib/puma/events.rb | 18 +++++++++ lib/puma/null_io.rb | 15 ++++++++ lib/puma/rack_patch.rb | 5 ++- lib/puma/server.rb | 83 ++++++++++++++++++++++++++++++++++++----- lib/puma/thread_pool.rb | 18 +++++++++ 6 files changed, 153 insertions(+), 10 deletions(-) diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index fe2779b6..ef1bbc9d 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -7,10 +7,18 @@ require 'puma/const' require 'rack/commonlogger' module Puma + # Handles invoke a Puma::Server in a command line style. + # class CLI DefaultTCPHost = "0.0.0.0" DefaultTCPPort = 9292 + # Create a new CLI object using +argv+ as the command line + # arguments. + # + # +stdout+ and +stderr+ can be set to IO-like objects which + # this object will report status on. + # def initialize(argv, stdout=STDOUT, stderr=STDERR) @argv = argv @stdout = stdout @@ -21,15 +29,21 @@ module Puma setup_options end + # Write +str+ to +@stdout+ + # def log(str) @stdout.puts str end + # Write +str+ to +@stderr+ + # def error(str) @stderr.puts "ERROR: #{str}" exit 1 end + # Build the OptionParser object to handle the available options. + # def setup_options @options = { :min_threads => 0, @@ -73,6 +87,9 @@ module Puma end end + # Load the specified rackup file, pull an options from + # the rackup file, and set @app. + # def load_rackup @app, options = Rack::Builder.parse_file @rackup @options.merge! options @@ -84,6 +101,9 @@ module Puma end end + # If configured, write the pid of the current process out + # to a file. + # def write_pid if path = @options[:pidfile] File.open(path, "w") do |f| @@ -92,10 +112,14 @@ module Puma end end + # :nodoc: def parse_options @parser.parse! @argv end + # Parse the options, load the rackup, start the server and wait + # for it to finish. + # def run parse_options diff --git a/lib/puma/events.rb b/lib/puma/events.rb index e47d0338..77b3484c 100644 --- a/lib/puma/events.rb +++ b/lib/puma/events.rb @@ -2,10 +2,17 @@ require 'puma/const' require 'stringio' module Puma + # The default implement of an event sink object used by Server + # for when certain kinds of events occur in the life of the server. + # + # The methods available are the events that the Server fires. + # class Events include Const + # Create an Events object that prints to +stdout+ and +stderr+. + # def initialize(stdout, stderr) @stdout = stdout @stderr = stderr @@ -13,11 +20,19 @@ module Puma attr_reader :stdout, :stderr + # An HTTP parse error has occured. + # +server+ is the Server object, +env+ the request, and +error+ a + # parsing exception. + # def parse_error(server, env, error) @stderr.puts "#{Time.now}: HTTP parse error, malformed request (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}" @stderr.puts "#{Time.now}: ENV: #{env.inspect}\n---\n" end + # An unknown error has occured. + # +server+ is the Server object, +env+ the request, +error+ an exception + # object, and +kind+ some additional info. + # def unknown_error(server, env, error, kind="Unknown") if error.respond_to? :render error.render "#{Time.now}: #{kind} error", @stderr @@ -29,6 +44,9 @@ module Puma DEFAULT = new(STDOUT, STDERR) + # Returns an Events object which writes it's status to 2 StringIO + # objects. + # def self.strings Events.new StringIO.new, StringIO.new end diff --git a/lib/puma/null_io.rb b/lib/puma/null_io.rb index 40af0fe7..d739de96 100644 --- a/lib/puma/null_io.rb +++ b/lib/puma/null_io.rb @@ -1,19 +1,34 @@ module Puma + + # Provides an IO-like object that always appears to contain no data. + # Used as the value for rack.input when the request has no body. + # class NullIO + + # Always returns nil + # def gets nil end + # Never yields + # def each end + # Always returns nil + # def read(count) nil end + # Does nothing + # def rewind end + # Does nothing + # def close end end diff --git a/lib/puma/rack_patch.rb b/lib/puma/rack_patch.rb index 63c0a782..f5224930 100644 --- a/lib/puma/rack_patch.rb +++ b/lib/puma/rack_patch.rb @@ -1,7 +1,10 @@ require 'rack/commonlogger' module Rack - # Patch CommonLogger to use after_reply + # Patch CommonLogger to use after_reply. + # + # Simply request this file and CommonLogger will be a bit more + # efficient. class CommonLogger remove_method :call diff --git a/lib/puma/server.rb b/lib/puma/server.rb index 37bad092..0be976c9 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -12,6 +12,8 @@ require 'puma_http11' require 'socket' module Puma + + # The HTTP Server itself. Serves out a single Rack app. class Server include Puma::Const @@ -24,11 +26,13 @@ module Puma attr_accessor :max_threads attr_accessor :persistent_timeout - # Creates a working server on host:port (strange things happen if port - # isn't a Number). + # Create a server for the rack app +app+. # - # Use HttpServer#run to start the server and HttpServer#acceptor.join to - # join the thread that's processing incoming requests on the socket. + # +events+ is an object which will be called when certain error events occur + # to be handled. See Puma::Events for the list of current methods to implement. + # + # Server#run returns a thread that you can join on to wait for the server + # to do it's work. # def initialize(app, events=Events::DEFAULT) @app = app @@ -62,6 +66,9 @@ module Puma } end + # On Linux, use TCP_CORK to better control how the TCP stack + # packetizes our stream. This improves both latency and throughput. + # if RUBY_PLATFORM =~ /linux/ # 6 == Socket::IPPROTO_TCP # 3 == TCP_CORK @@ -81,6 +88,10 @@ module Puma end end + # Tell the server to listen on host +host+, port +port+. + # If optimize_for_latency is true (the default) then clients connecting + # will be optimized for latency over throughput. + # def add_tcp_listener(host, port, optimize_for_latency=true) s = TCPServer.new(host, port) if optimize_for_latency @@ -89,13 +100,15 @@ module Puma @ios << s end + # Tell the server to listen on +path+ as a UNIX domain socket. + # def add_unix_listener(path) @ios << UNIXServer.new(path) end - # Runs the server. It returns the thread used so you can "join" it. - # You can also access the HttpServer#acceptor attribute to get the - # thread later. + # Runs the server. It returns the thread used so you can join it. + # The thread is always available via #thread to be join'd + # def run BasicSocket.do_not_reverse_lookup = true @@ -137,6 +150,7 @@ module Puma return @thread end + # :nodoc: def handle_check cmd = @check.read(1) @@ -149,6 +163,12 @@ module Puma return false end + # Given a connection on +client+, handle the incoming requests. + # + # This method support HTTP Keep-Alive so it may, depending on if the client + # indicates that it supports keep alive, wait for another request before + # returning. + # def process_client(client) parser = HttpParser.new @@ -198,12 +218,16 @@ module Puma end end end - rescue EOFError, SystemCallError - client.close rescue nil + # The client disconnected while we were reading data + rescue EOFError, SystemCallError + # Swallow them. The ensure tries to close +client+ down + + # The client doesn't know HTTP well rescue HttpParserError => e @events.parse_error self, env, e + # Server error rescue StandardError => e @events.unknown_error self, env, e, "Read" @@ -218,6 +242,9 @@ module Puma end end + # Given a Hash +env+ for the request read from +client+, add + # and fixup keys to comply with Rack's env guidelines. + # def normalize_env(env, client) if host = env[HTTP_HOST] if colon = host.index(":") @@ -250,8 +277,19 @@ module Puma env[REMOTE_ADDR] = client.peeraddr.last end + # The object used for a request with no body. All requests with + # no body share this one object since it has no state. EmptyBody = NullIO.new + # Given the request +env+ from +client+ and a partial request body + # in +body+, finish reading the body if there is one and invoke + # the rack app. Then construct the response and write it back to + # +client+ + # + # +cl+ is the previously fetched Content-Length header if there + # was one. This is an optimization to keep from having to look + # it up again. + # def handle_request(env, client, body, cl) normalize_env env, client @@ -265,6 +303,9 @@ module Puma env[RACK_INPUT] = body env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP + # A rack extension. If the app writes #call'ables to this + # array, we will invoke them when the request is done. + # after_reply = env[RACK_AFTER_REPLY] = [] begin @@ -287,6 +328,10 @@ module Puma keep_alive = env[HTTP_CONNECTION] != CLOSE include_keepalive_header = false + # An optimization. The most common response is 200, so we can + # reply with the proper 200 status without having to compute + # the response header. + # if status == 200 client.write HTTP_11_200 else @@ -301,6 +346,8 @@ module Puma keep_alive = env[HTTP_CONNECTION] == KEEP_ALIVE include_keepalive_header = keep_alive + # Same optimization as above for HTTP/1.1 + # if status == 200 client.write HTTP_10_200 else @@ -381,6 +428,13 @@ module Puma return keep_alive end + # Given the requset +env+ from +client+ and the partial body +body+ + # plus a potential Content-Length value +cl+, finish reading + # the body and return it. + # + # If the body is larger than MAX_BODY, a Tempfile object is used + # for the body, otherwise a StringIO is used. + # def read_body(env, client, body, cl) content_length = cl.to_i @@ -426,23 +480,32 @@ module Puma return stream end + # A fallback rack response if +@app+ raises as exception. + # def lowlevel_error(e) [500, {}, ["No application configured"]] end # Wait for all outstanding requests to finish. + # def graceful_shutdown @thread_pool.shutdown if @thread_pool end # Stops the acceptor thread and then causes the worker threads to finish # off the request queue before finally exiting. + # def stop(sync=false) @notify << STOP_COMMAND @thread.join if @thread && sync end + # If the dnssd gem is installed, advertise any TCPServer's configured + # out via bonjour. + # + # +name+ is the host name to use when advertised. + # def attempt_bonjour(name) begin require 'dnssd' @@ -468,6 +531,8 @@ module Puma return announced end + # Indicate if attempt_bonjour worked. + # def bonjour_registered? @bonjour_registered ||= false end diff --git a/lib/puma/thread_pool.rb b/lib/puma/thread_pool.rb index 5ec0ad4e..19cfe6a7 100644 --- a/lib/puma/thread_pool.rb +++ b/lib/puma/thread_pool.rb @@ -1,7 +1,16 @@ require 'thread' module Puma + # A simple thread pool management object. + # class ThreadPool + + # Maintain a minimum of +min+ and maximum of +max+ threads + # in the pool. + # + # The block passed is the work that will be performed in each + # thread. + # def initialize(min, max, &blk) @todo = Queue.new @mutex = Mutex.new @@ -20,6 +29,8 @@ module Puma attr_reader :spawned + # How many objects have yet to be processed by the pool? + # def backlog @todo.size end @@ -27,6 +38,7 @@ module Puma Stop = Object.new Trim = Object.new + # :nodoc: def spawn_thread @mutex.synchronize do @spawned += 1 @@ -64,6 +76,7 @@ module Puma th end + # Add +work+ to the todo list for a Thread to pickup and process. def <<(work) if @todo.num_waiting == 0 and @spawned < @max spawn_thread @@ -72,6 +85,9 @@ module Puma @todo << work end + # If too many threads are in the pool, tell one to finish go ahead + # and exit. + # def trim @mutex.synchronize do if @spawned - @trim_requested > @min @@ -81,6 +97,8 @@ module Puma end end + # Tell all threads in the pool to exit and wait for them to finish. + # def shutdown @spawned.times do @todo << Stop