Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Write a bunch of documentation

  • Loading branch information...
commit b18cfae4b119dca1e1bfe2d84f61e15c978db353 1 parent 6d1d60a
@evanphx evanphx authored
View
24 lib/puma/cli.rb
@@ -7,10 +7,18 @@
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 @@ def initialize(argv, stdout=STDOUT, stderr=STDERR)
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 @@ def setup_options
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 @@ def load_rackup
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 @@ def write_pid
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
View
18 lib/puma/events.rb
@@ -2,10 +2,17 @@
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 @@ def initialize(stdout, stderr)
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 @@ def unknown_error(server, env, error, kind="Unknown")
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
View
15 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
View
5 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
View
81 lib/puma/server.rb
@@ -12,6 +12,8 @@
require 'socket'
module Puma
+
+ # The HTTP Server itself. Serves out a single Rack app.
class Server
include Puma::Const
@@ -24,11 +26,13 @@ class Server
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+.
+ #
+ # +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.
#
- # Use HttpServer#run to start the server and HttpServer#acceptor.join to
- # join the thread that's processing incoming requests on the socket.
+ # 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 @@ def initialize(app, events=Events::DEFAULT)
}
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 @@ def uncork_socket(socket)
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 @@ def add_tcp_listener(host, port, optimize_for_latency=true)
@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 @@ def run
return @thread
end
+ # :nodoc:
def handle_check
cmd = @check.read(1)
@@ -149,6 +163,12 @@ def handle_check
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 @@ def process_client(client)
end
end
end
+
+ # The client disconnected while we were reading data
rescue EOFError, SystemCallError
- client.close rescue nil
+ # 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 @@ def process_client(client)
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 @@ def normalize_env(env, client)
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 @@ def handle_request(env, client, body, cl)
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 @@ def handle_request(env, client, body, cl)
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 @@ def handle_request(env, client, body, cl)
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 @@ def handle_request(env, client, body, cl)
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 @@ def read_body(env, client, body, cl)
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 @@ def attempt_bonjour(name)
return announced
end
+ # Indicate if attempt_bonjour worked.
+ #
def bonjour_registered?
@bonjour_registered ||= false
end
View
18 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 @@ def initialize(min, max, &blk)
attr_reader :spawned
+ # How many objects have yet to be processed by the pool?
+ #
def backlog
@todo.size
end
@@ -27,6 +38,7 @@ def backlog
Stop = Object.new
Trim = Object.new
+ # :nodoc:
def spawn_thread
@mutex.synchronize do
@spawned += 1
@@ -64,6 +76,7 @@ def spawn_thread
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 @@ def <<(work)
@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 @@ def trim
end
end
+ # Tell all threads in the pool to exit and wait for them to finish.
+ #
def shutdown
@spawned.times do
@todo << Stop
Please sign in to comment.
Something went wrong with that request. Please try again.