1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Write a bunch of documentation

This commit is contained in:
Evan Phoenix 2011-12-01 15:23:14 -08:00
parent 6d1d60abfa
commit b18cfae4b1
6 changed files with 153 additions and 10 deletions

View file

@ -7,10 +7,18 @@ require 'puma/const'
require 'rack/commonlogger' require 'rack/commonlogger'
module Puma module Puma
# Handles invoke a Puma::Server in a command line style.
#
class CLI class CLI
DefaultTCPHost = "0.0.0.0" DefaultTCPHost = "0.0.0.0"
DefaultTCPPort = 9292 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) def initialize(argv, stdout=STDOUT, stderr=STDERR)
@argv = argv @argv = argv
@stdout = stdout @stdout = stdout
@ -21,15 +29,21 @@ module Puma
setup_options setup_options
end end
# Write +str+ to +@stdout+
#
def log(str) def log(str)
@stdout.puts str @stdout.puts str
end end
# Write +str+ to +@stderr+
#
def error(str) def error(str)
@stderr.puts "ERROR: #{str}" @stderr.puts "ERROR: #{str}"
exit 1 exit 1
end end
# Build the OptionParser object to handle the available options.
#
def setup_options def setup_options
@options = { @options = {
:min_threads => 0, :min_threads => 0,
@ -73,6 +87,9 @@ module Puma
end end
end end
# Load the specified rackup file, pull an options from
# the rackup file, and set @app.
#
def load_rackup def load_rackup
@app, options = Rack::Builder.parse_file @rackup @app, options = Rack::Builder.parse_file @rackup
@options.merge! options @options.merge! options
@ -84,6 +101,9 @@ module Puma
end end
end end
# If configured, write the pid of the current process out
# to a file.
#
def write_pid def write_pid
if path = @options[:pidfile] if path = @options[:pidfile]
File.open(path, "w") do |f| File.open(path, "w") do |f|
@ -92,10 +112,14 @@ module Puma
end end
end end
# :nodoc:
def parse_options def parse_options
@parser.parse! @argv @parser.parse! @argv
end end
# Parse the options, load the rackup, start the server and wait
# for it to finish.
#
def run def run
parse_options parse_options

View file

@ -2,10 +2,17 @@ require 'puma/const'
require 'stringio' require 'stringio'
module Puma 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 class Events
include Const include Const
# Create an Events object that prints to +stdout+ and +stderr+.
#
def initialize(stdout, stderr) def initialize(stdout, stderr)
@stdout = stdout @stdout = stdout
@stderr = stderr @stderr = stderr
@ -13,11 +20,19 @@ module Puma
attr_reader :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) 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}: HTTP parse error, malformed request (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}"
@stderr.puts "#{Time.now}: ENV: #{env.inspect}\n---\n" @stderr.puts "#{Time.now}: ENV: #{env.inspect}\n---\n"
end 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") def unknown_error(server, env, error, kind="Unknown")
if error.respond_to? :render if error.respond_to? :render
error.render "#{Time.now}: #{kind} error", @stderr error.render "#{Time.now}: #{kind} error", @stderr
@ -29,6 +44,9 @@ module Puma
DEFAULT = new(STDOUT, STDERR) DEFAULT = new(STDOUT, STDERR)
# Returns an Events object which writes it's status to 2 StringIO
# objects.
#
def self.strings def self.strings
Events.new StringIO.new, StringIO.new Events.new StringIO.new, StringIO.new
end end

View file

@ -1,19 +1,34 @@
module Puma 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 class NullIO
# Always returns nil
#
def gets def gets
nil nil
end end
# Never yields
#
def each def each
end end
# Always returns nil
#
def read(count) def read(count)
nil nil
end end
# Does nothing
#
def rewind def rewind
end end
# Does nothing
#
def close def close
end end
end end

View file

@ -1,7 +1,10 @@
require 'rack/commonlogger' require 'rack/commonlogger'
module Rack 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 class CommonLogger
remove_method :call remove_method :call

View file

@ -12,6 +12,8 @@ require 'puma_http11'
require 'socket' require 'socket'
module Puma module Puma
# The HTTP Server itself. Serves out a single Rack app.
class Server class Server
include Puma::Const include Puma::Const
@ -24,11 +26,13 @@ module Puma
attr_accessor :max_threads attr_accessor :max_threads
attr_accessor :persistent_timeout attr_accessor :persistent_timeout
# Creates a working server on host:port (strange things happen if port # Create a server for the rack app +app+.
# isn't a Number).
# #
# Use HttpServer#run to start the server and HttpServer#acceptor.join to # +events+ is an object which will be called when certain error events occur
# join the thread that's processing incoming requests on the socket. # 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) def initialize(app, events=Events::DEFAULT)
@app = app @app = app
@ -62,6 +66,9 @@ module Puma
} }
end 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/ if RUBY_PLATFORM =~ /linux/
# 6 == Socket::IPPROTO_TCP # 6 == Socket::IPPROTO_TCP
# 3 == TCP_CORK # 3 == TCP_CORK
@ -81,6 +88,10 @@ module Puma
end end
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) def add_tcp_listener(host, port, optimize_for_latency=true)
s = TCPServer.new(host, port) s = TCPServer.new(host, port)
if optimize_for_latency if optimize_for_latency
@ -89,13 +100,15 @@ module Puma
@ios << s @ios << s
end end
# Tell the server to listen on +path+ as a UNIX domain socket.
#
def add_unix_listener(path) def add_unix_listener(path)
@ios << UNIXServer.new(path) @ios << UNIXServer.new(path)
end end
# Runs the server. It returns the thread used so you can "join" it. # Runs the server. It returns the thread used so you can join it.
# You can also access the HttpServer#acceptor attribute to get the # The thread is always available via #thread to be join'd
# thread later. #
def run def run
BasicSocket.do_not_reverse_lookup = true BasicSocket.do_not_reverse_lookup = true
@ -137,6 +150,7 @@ module Puma
return @thread return @thread
end end
# :nodoc:
def handle_check def handle_check
cmd = @check.read(1) cmd = @check.read(1)
@ -149,6 +163,12 @@ module Puma
return false return false
end 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) def process_client(client)
parser = HttpParser.new parser = HttpParser.new
@ -198,12 +218,16 @@ module Puma
end end
end 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 rescue HttpParserError => e
@events.parse_error self, env, e @events.parse_error self, env, e
# Server error
rescue StandardError => e rescue StandardError => e
@events.unknown_error self, env, e, "Read" @events.unknown_error self, env, e, "Read"
@ -218,6 +242,9 @@ module Puma
end end
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) def normalize_env(env, client)
if host = env[HTTP_HOST] if host = env[HTTP_HOST]
if colon = host.index(":") if colon = host.index(":")
@ -250,8 +277,19 @@ module Puma
env[REMOTE_ADDR] = client.peeraddr.last env[REMOTE_ADDR] = client.peeraddr.last
end 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 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) def handle_request(env, client, body, cl)
normalize_env env, client normalize_env env, client
@ -265,6 +303,9 @@ module Puma
env[RACK_INPUT] = body env[RACK_INPUT] = body
env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP 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] = [] after_reply = env[RACK_AFTER_REPLY] = []
begin begin
@ -287,6 +328,10 @@ module Puma
keep_alive = env[HTTP_CONNECTION] != CLOSE keep_alive = env[HTTP_CONNECTION] != CLOSE
include_keepalive_header = false 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 if status == 200
client.write HTTP_11_200 client.write HTTP_11_200
else else
@ -301,6 +346,8 @@ module Puma
keep_alive = env[HTTP_CONNECTION] == KEEP_ALIVE keep_alive = env[HTTP_CONNECTION] == KEEP_ALIVE
include_keepalive_header = keep_alive include_keepalive_header = keep_alive
# Same optimization as above for HTTP/1.1
#
if status == 200 if status == 200
client.write HTTP_10_200 client.write HTTP_10_200
else else
@ -381,6 +428,13 @@ module Puma
return keep_alive return keep_alive
end 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) def read_body(env, client, body, cl)
content_length = cl.to_i content_length = cl.to_i
@ -426,23 +480,32 @@ module Puma
return stream return stream
end end
# A fallback rack response if +@app+ raises as exception.
#
def lowlevel_error(e) def lowlevel_error(e)
[500, {}, ["No application configured"]] [500, {}, ["No application configured"]]
end end
# Wait for all outstanding requests to finish. # Wait for all outstanding requests to finish.
#
def graceful_shutdown def graceful_shutdown
@thread_pool.shutdown if @thread_pool @thread_pool.shutdown if @thread_pool
end end
# Stops the acceptor thread and then causes the worker threads to finish # Stops the acceptor thread and then causes the worker threads to finish
# off the request queue before finally exiting. # off the request queue before finally exiting.
#
def stop(sync=false) def stop(sync=false)
@notify << STOP_COMMAND @notify << STOP_COMMAND
@thread.join if @thread && sync @thread.join if @thread && sync
end 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) def attempt_bonjour(name)
begin begin
require 'dnssd' require 'dnssd'
@ -468,6 +531,8 @@ module Puma
return announced return announced
end end
# Indicate if attempt_bonjour worked.
#
def bonjour_registered? def bonjour_registered?
@bonjour_registered ||= false @bonjour_registered ||= false
end end

View file

@ -1,7 +1,16 @@
require 'thread' require 'thread'
module Puma module Puma
# A simple thread pool management object.
#
class ThreadPool 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) def initialize(min, max, &blk)
@todo = Queue.new @todo = Queue.new
@mutex = Mutex.new @mutex = Mutex.new
@ -20,6 +29,8 @@ module Puma
attr_reader :spawned attr_reader :spawned
# How many objects have yet to be processed by the pool?
#
def backlog def backlog
@todo.size @todo.size
end end
@ -27,6 +38,7 @@ module Puma
Stop = Object.new Stop = Object.new
Trim = Object.new Trim = Object.new
# :nodoc:
def spawn_thread def spawn_thread
@mutex.synchronize do @mutex.synchronize do
@spawned += 1 @spawned += 1
@ -64,6 +76,7 @@ module Puma
th th
end end
# Add +work+ to the todo list for a Thread to pickup and process.
def <<(work) def <<(work)
if @todo.num_waiting == 0 and @spawned < @max if @todo.num_waiting == 0 and @spawned < @max
spawn_thread spawn_thread
@ -72,6 +85,9 @@ module Puma
@todo << work @todo << work
end end
# If too many threads are in the pool, tell one to finish go ahead
# and exit.
#
def trim def trim
@mutex.synchronize do @mutex.synchronize do
if @spawned - @trim_requested > @min if @spawned - @trim_requested > @min
@ -81,6 +97,8 @@ module Puma
end end
end end
# Tell all threads in the pool to exit and wait for them to finish.
#
def shutdown def shutdown
@spawned.times do @spawned.times do
@todo << Stop @todo << Stop