From 28d571b7cca709641d964e00e6004facb6bfcc7e Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Wed, 4 Feb 2009 14:30:40 -0800 Subject: s/Mongrel/Unicorn/g Avoid conflicting with existing Mongrel libraries since we'll be incompatible and break things w/o disrupting Mongrel installations. --- lib/mongrel.rb | 285 -------------------- lib/mongrel/const.rb | 113 -------- lib/mongrel/header_out.rb | 34 --- lib/mongrel/http_request.rb | 106 -------- lib/mongrel/http_response.rb | 167 ------------ lib/mongrel/init.rb | 10 - lib/mongrel/mime_types.yml | 616 ------------------------------------------- lib/mongrel/semaphore.rb | 46 ---- lib/mongrel/tcphack.rb | 18 -- lib/unicorn.rb | 284 ++++++++++++++++++++ lib/unicorn/const.rb | 113 ++++++++ lib/unicorn/header_out.rb | 34 +++ lib/unicorn/http_request.rb | 106 ++++++++ lib/unicorn/http_response.rb | 167 ++++++++++++ lib/unicorn/semaphore.rb | 46 ++++ lib/unicorn/tcphack.rb | 18 ++ 16 files changed, 768 insertions(+), 1395 deletions(-) delete mode 100644 lib/mongrel.rb delete mode 100644 lib/mongrel/const.rb delete mode 100644 lib/mongrel/header_out.rb delete mode 100644 lib/mongrel/http_request.rb delete mode 100644 lib/mongrel/http_response.rb delete mode 100644 lib/mongrel/init.rb delete mode 100644 lib/mongrel/mime_types.yml delete mode 100644 lib/mongrel/semaphore.rb delete mode 100644 lib/mongrel/tcphack.rb create mode 100644 lib/unicorn.rb create mode 100644 lib/unicorn/const.rb create mode 100644 lib/unicorn/header_out.rb create mode 100644 lib/unicorn/http_request.rb create mode 100644 lib/unicorn/http_response.rb create mode 100644 lib/unicorn/semaphore.rb create mode 100644 lib/unicorn/tcphack.rb (limited to 'lib') diff --git a/lib/mongrel.rb b/lib/mongrel.rb deleted file mode 100644 index dceb138..0000000 --- a/lib/mongrel.rb +++ /dev/null @@ -1,285 +0,0 @@ - -# Standard libraries -require 'socket' -require 'tempfile' -require 'yaml' -require 'time' -require 'etc' -require 'uri' -require 'stringio' -require 'fcntl' -require 'logger' - -# Compiled Mongrel extension -require 'http11' - -# Gem conditional loader -require 'thread' -require 'rack' - -# Ruby Mongrel -require 'mongrel/tcphack' -require 'mongrel/const' -require 'mongrel/http_request' -require 'mongrel/header_out' -require 'mongrel/http_response' -require 'mongrel/semaphore' - -# Mongrel module containing all of the classes (include C extensions) for running -# a Mongrel web server. It contains a minimalist HTTP server with just enough -# functionality to service web application requests fast as possible. -module Mongrel - class << self - # A logger instance that conforms to the API of stdlib's Logger. - attr_accessor :logger - - def run(app, options = {}) - HttpServer.new(app, options).start.join - end - end - - # Used to stop the HttpServer via Thread.raise. - class StopServer < Exception; end - - # Thrown at a thread when it is timed out. - class TimeoutError < Exception; end - - # Thrown by HttpServer#stop if the server is not started. - class AcceptorError < StandardError; end - - # - # This is the main driver of Mongrel, while the Mongrel::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. - # - class HttpServer - attr_reader :acceptor, :workers, :logger, :host, :port, :timeout, :max_queued_threads, :max_concurrent_threads - - DEFAULTS = { - :timeout => 60, - :host => '0.0.0.0', - :port => 8080, - :logger => Logger.new(STDERR), - :max_queued_threads => 12, - :max_concurrent_threads => 4 - } - - # 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 max_queued_threads 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. - # - def initialize(app, options = {}) - @app = app - @workers = ThreadGroup.new - - (DEFAULTS.to_a + options.to_a).each do |key, value| - instance_variable_set("@#{key.to_s.downcase}", value) - end - - @socket = TCPServer.new(@host, @port) - @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined?(Fcntl::FD_CLOEXEC) - end - - # 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. - def process_client(client) - begin - parser = HttpParser.new - params = Hash.new - request = nil - data = client.readpartial(Const::CHUNK_SIZE) - nparsed = 0 - - # Assumption: nparsed will always be less since data will get filled with more - # after each parsing. If it doesn't get more then there was a problem - # with the read operation on the client socket. Effect is to stop processing when the - # socket can't fill the buffer for further parsing. - while nparsed < data.length - nparsed = parser.execute(params, data, nparsed) - - if parser.finished? - if !params[Const::REQUEST_PATH] - # It might be a dumbass full host request header - uri = URI.parse(params[Const::REQUEST_URI]) - params[Const::REQUEST_PATH] = uri.path - end - - raise "No REQUEST PATH" if !params[Const::REQUEST_PATH] - - params[Const::PATH_INFO] = params[Const::REQUEST_PATH] - params[Const::SCRIPT_NAME] = Const::SLASH - - # From http://www.ietf.org/rfc/rfc3875 : - # "Script authors should be aware that the REMOTE_ADDR and REMOTE_HOST - # meta-variables (see sections 4.1.8 and 4.1.9) may not identify the - # ultimate source of the request. They identify the client for the - # immediate request to the server; that client may be a proxy, gateway, - # or other intermediary acting on behalf of the actual source client." - params[Const::REMOTE_ADDR] = client.peeraddr.last - - # Select handlers that want more detailed request notification - request = HttpRequest.new(params, client, logger) - - # in the case of large file uploads the user could close the socket, so skip those requests - break if request.body == nil # nil signals from HttpRequest::initialize that the request was aborted - app_response = @app.call(request.env) - response = HttpResponse.new(client, app_response).start - break #done - else - # Parser is not done, queue up more data to read and continue parsing - chunk = client.readpartial(Const::CHUNK_SIZE) - break if !chunk or chunk.length == 0 # read failed, stop processing - - data << chunk - if data.length >= Const::MAX_HEADER - raise HttpParserError.new("HEADER is longer than allowed, aborting client early.") - end - end - end - rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF - client.close rescue nil - rescue HttpParserError => e - logger.error "HTTP parse error, malformed request (#{params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #{e.inspect}" - logger.error "REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n" - rescue Errno::EMFILE - reap_dead_workers('too many files') - rescue Object => e - logger.error "Read error: #{e.inspect}" - logger.error e.backtrace.join("\n") - ensure - begin - client.close - rescue IOError - # Already closed - rescue Object => e - logger.error "Client error: #{e.inspect}" - logger.error e.backtrace.join("\n") - end - request.body.close! if request and request.body.class == Tempfile - 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. It returns the count of workers still active - # after the reap is done. It only runs if there are workers to reap. - def reap_dead_workers(reason='unknown') - if @workers.list.length > 0 - logger.info "Reaping #{@workers.list.length} threads for slow workers because of '#{reason}'" - error_msg = "Mongrel timed out this thread: #{reason}" - mark = Time.now - @workers.list.each do |worker| - worker[:started_on] = Time.now if not worker[:started_on] - - if mark - worker[:started_on] > @timeout - logger.info "Thread #{worker.inspect} is too old, killing." - worker.raise(TimeoutError.new(error_msg)) - end - end - end - - return @workers.list.length - end - - # Performs a wait on all the currently running threads and kills any that take - # too long. It waits by @timeout seconds, which can be set in .initialize or - # via mongrel_rails. - def graceful_shutdown - while reap_dead_workers("shutdown") > 0 - logger.info "Waiting for #{@workers.list.length} requests to finish, could take #{@timeout} seconds." - sleep @timeout / 10 - end - end - - def configure_socket_options - case RUBY_PLATFORM - when /linux/ - # 9 is currently TCP_DEFER_ACCEPT - $tcp_defer_accept_opts = [Socket::SOL_TCP, 9, 1] - $tcp_cork_opts = [Socket::SOL_TCP, 3, 1] - when /freebsd(([1-4]\..{1,2})|5\.[0-4])/ - # Do nothing, just closing a bug when freebsd <= 5.4 - when /freebsd/ - # Use the HTTP accept filter if available. - # The struct made by pack() is defined in /usr/include/sys/socket.h as accept_filter_arg - unless `/sbin/sysctl -nq net.inet.accf.http`.empty? - $tcp_defer_accept_opts = [Socket::SOL_SOCKET, Socket::SO_ACCEPTFILTER, ['httpready', nil].pack('a16a240')] - 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 start - semaphore = Semaphore.new(@max_concurrent_threads) - BasicSocket.do_not_reverse_lookup = true - - configure_socket_options - - if defined?($tcp_defer_accept_opts) and $tcp_defer_accept_opts - @socket.setsockopt(*$tcp_defer_accept_opts) rescue nil - end - - @acceptor = Thread.new do - begin - while true - begin - client = @socket.accept - - if defined?($tcp_cork_opts) and $tcp_cork_opts - client.setsockopt(*$tcp_cork_opts) rescue nil - end - - worker_list = @workers.list - if worker_list.length >= @max_queued_threads - logger.error "Server overloaded with #{worker_list.length} processors (#@max_queued_threads max). Dropping connection." - client.close rescue nil - reap_dead_workers("max processors") - else - thread = Thread.new(client) {|c| semaphore.synchronize { process_client(c) } } - thread[:started_on] = Time.now - @workers.add(thread) - end - rescue StopServer - break - rescue Errno::EMFILE - reap_dead_workers("too many open files") - sleep 0.5 - rescue Errno::ECONNABORTED - # client closed the socket even before accept - client.close rescue nil - rescue Object => e - logger.error "Unhandled listen loop exception #{e.inspect}." - logger.error e.backtrace.join("\n") - end - end - graceful_shutdown - ensure - @socket.close - logger.info "Closed socket." - end - end - - @acceptor - end - - # Stops the acceptor thread and then causes the worker threads to finish - # off the request queue before finally exiting. - def stop(synchronous = false) - raise AcceptorError, "Server was not started." unless @acceptor - @acceptor.raise(StopServer.new) - (sleep(0.5) while @acceptor.alive?) if synchronous - @acceptor = nil - end - end -end diff --git a/lib/mongrel/const.rb b/lib/mongrel/const.rb deleted file mode 100644 index 994a2bf..0000000 --- a/lib/mongrel/const.rb +++ /dev/null @@ -1,113 +0,0 @@ - -module Mongrel - - # Every standard HTTP code mapped to the appropriate message. These are - # used so frequently that they are placed directly in Mongrel for easy - # access rather than Mongrel::Const itself. - HTTP_STATUS_CODES = { - 100 => 'Continue', - 101 => 'Switching Protocols', - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Moved Temporarily', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', - 415 => 'Unsupported Media Type', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported' - } - - # Frequently used constants when constructing requests or responses. Many times - # the constant just refers to a string with the same contents. Using these constants - # gave about a 3% to 10% performance improvement over using the strings directly. - # Symbols did not really improve things much compared to constants. - # - # While Mongrel does try to emulate the CGI/1.2 protocol, it does not use the REMOTE_IDENT, - # REMOTE_USER, or REMOTE_HOST parameters since those are either a security problem or - # too taxing on performance. - module Const - DATE="Date".freeze - - # This is the part of the path after the SCRIPT_NAME. - PATH_INFO="PATH_INFO".freeze - - # Request body - HTTP_BODY="HTTP_BODY".freeze - - # This is the initial part that your handler is identified as by URIClassifier. - SCRIPT_NAME="SCRIPT_NAME".freeze - - # The original URI requested by the client. Passed to URIClassifier to build PATH_INFO and SCRIPT_NAME. - REQUEST_URI='REQUEST_URI'.freeze - REQUEST_PATH='REQUEST_PATH'.freeze - - MONGREL_VERSION="2.0".freeze - - MONGREL_TMP_BASE="mongrel".freeze - - # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff. - ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Mongrel #{MONGREL_VERSION}\r\n\r\nNOT FOUND".freeze - - CONTENT_LENGTH="CONTENT_LENGTH".freeze - - # A common header for indicating the server is too busy. Not used yet. - ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze - - # The basic max request size we'll try to read. - CHUNK_SIZE=(16 * 1024) - - # This is the maximum header that is allowed before a client is booted. The parser detects - # this, but we'd also like to do this as well. - MAX_HEADER=1024 * (80 + 32) - - # Maximum request body size before it is moved out of memory and into a tempfile for reading. - MAX_BODY=MAX_HEADER - - # A frozen format for this is about 15% faster - STATUS_FORMAT = "HTTP/1.1 %d %s\r\nConnection: close\r\n".freeze - CONTENT_TYPE = "Content-Type".freeze - LAST_MODIFIED = "Last-Modified".freeze - ETAG = "ETag".freeze - SLASH = "/".freeze - REQUEST_METHOD="REQUEST_METHOD".freeze - GET="GET".freeze - HEAD="HEAD".freeze - # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32) - ETAG_FORMAT="\"%x-%x-%x\"".freeze - HEADER_FORMAT="%s: %s\r\n".freeze - LINE_END="\r\n".freeze - REMOTE_ADDR="REMOTE_ADDR".freeze - HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze - HTTP_IF_MODIFIED_SINCE="HTTP_IF_MODIFIED_SINCE".freeze - HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze - REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze - HOST = "HOST".freeze - end -end \ No newline at end of file diff --git a/lib/mongrel/header_out.rb b/lib/mongrel/header_out.rb deleted file mode 100644 index 008bff8..0000000 --- a/lib/mongrel/header_out.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Mongrel - # This class implements a simple way of constructing the HTTP headers dynamically - # via a Hash syntax. Think of it as a write-only Hash. Refer to HttpResponse for - # information on how this is used. - # - # One consequence of this write-only nature is that you can write multiple headers - # by just doing them twice (which is sometimes needed in HTTP), but that the normal - # semantics for Hash (where doing an insert replaces) is not there. - class HeaderOut - attr_reader :out - attr_accessor :allowed_duplicates - - def initialize(out = StringIO.new) - @sent = {} - @allowed_duplicates = {"Set-Cookie" => true, "Set-Cookie2" => true, - "Warning" => true, "WWW-Authenticate" => true} - @out = out - end - - def merge!(hash) - hash.each do |key, value| - self[key] = value - end - end - - # Simply writes "#{key}: #{value}" to an output buffer. - def[]=(key,value) - if not @sent.has_key?(key) or @allowed_duplicates.has_key?(key) - @sent[key] = true - @out.write(Const::HEADER_FORMAT % [key, value]) - end - end - end -end diff --git a/lib/mongrel/http_request.rb b/lib/mongrel/http_request.rb deleted file mode 100644 index 86c0e15..0000000 --- a/lib/mongrel/http_request.rb +++ /dev/null @@ -1,106 +0,0 @@ - -module Mongrel - # - # The HttpRequest.initialize method will convert any request that is larger than - # Const::MAX_BODY into a Tempfile and use that as the body. Otherwise it uses - # a StringIO object. To be safe, you should assume it works like a file. - # - class HttpRequest - attr_reader :body, :params, :logger - - # You don't really call this. It's made for you. - # Main thing it does is hook up the params, and store any remaining - # body data into the HttpRequest.body attribute. - def initialize(params, socket, logger) - @params = params - @socket = socket - @logger = logger - - content_length = @params[Const::CONTENT_LENGTH].to_i - remain = content_length - @params[Const::HTTP_BODY].length - - # Some clients (like FF1.0) report 0 for body and then send a body. This will probably truncate them but at least the request goes through usually. - if remain <= 0 - # we've got everything, pack it up - @body = StringIO.new - @body.write @params[Const::HTTP_BODY] - elsif remain > 0 - # must read more data to complete body - if remain > Const::MAX_BODY - # huge body, put it in a tempfile - @body = Tempfile.new(Const::MONGREL_TMP_BASE) - @body.binmode - else - # small body, just use that - @body = StringIO.new - end - - @body.write @params[Const::HTTP_BODY] - read_body(remain, content_length) - end - - @body.rewind if @body - end - - # Returns an environment which is rackable: http://rack.rubyforge.org/doc/files/SPEC.html - # Copied directly from Rack's old Mongrel handler. - def env - env = params.clone - env["QUERY_STRING"] ||= '' - env.delete "HTTP_CONTENT_TYPE" - env.delete "HTTP_CONTENT_LENGTH" - env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" - env.update({"rack.version" => [0,1], - "rack.input" => @body, - "rack.errors" => STDERR, - - "rack.multithread" => true, - "rack.multiprocess" => false, # ??? - "rack.run_once" => false, - - "rack.url_scheme" => "http", - }) - end - - # Does the heavy lifting of properly reading the larger body requests in - # small chunks. It expects @body to be an IO object, @socket to be valid, - # and will set @body = nil if the request fails. It also expects any initial - # part of the body that has been read to be in the @body already. - def read_body(remain, total) - begin - # Write the odd sized chunk first - @params[Const::HTTP_BODY] = read_socket(remain % Const::CHUNK_SIZE) - - remain -= @body.write(@params[Const::HTTP_BODY]) - - # Then stream out nothing but perfectly sized chunks - until remain <= 0 or @socket.closed? - # ASSUME: we are writing to a disk and these writes always write the requested amount - @params[Const::HTTP_BODY] = read_socket(Const::CHUNK_SIZE) - remain -= @body.write(@params[Const::HTTP_BODY]) - end - rescue Object => e - logger.error "Error reading HTTP body: #{e.inspect}" - # Any errors means we should delete the file, including if the file is dumped - @socket.close rescue nil - @body.close! if @body.class == Tempfile - @body = nil # signals that there was a problem - end - end - - def read_socket(len) - if !@socket.closed? - data = @socket.read(len) - if !data - raise "Socket read return nil" - elsif data.length != len - raise "Socket read returned insufficient data: #{data.length}" - else - data - end - else - raise "Socket already closed when reading." - end - end - end -end diff --git a/lib/mongrel/http_response.rb b/lib/mongrel/http_response.rb deleted file mode 100644 index 17b6d6b..0000000 --- a/lib/mongrel/http_response.rb +++ /dev/null @@ -1,167 +0,0 @@ -module Mongrel - # Writes and controls your response to the client using the HTTP/1.1 specification. - # You use it by simply doing: - # - # response.start(200) do |head,out| - # head['Content-Type'] = 'text/plain' - # out.write("hello\n") - # end - # - # The parameter to start is the response code--which Mongrel will translate for you - # based on HTTP_STATUS_CODES. The head parameter is how you write custom headers. - # The out parameter is where you write your body. The default status code for - # HttpResponse.start is 200 so the above example is redundant. - # - # As you can see, it's just like using a Hash and as you do this it writes the proper - # header to the output on the fly. You can even intermix specifying headers and - # writing content. The HttpResponse class with write the things in the proper order - # once the HttpResponse.block is ended. - # - # You may also work the HttpResponse object directly using the various attributes available - # for the raw socket, body, header, and status codes. If you do this you're on your own. - # A design decision was made to force the client to not pipeline requests. HTTP/1.1 - # pipelining really kills the performance due to how it has to be handled and how - # unclear the standard is. To fix this the HttpResponse gives a "Connection: close" - # header which forces the client to close right away. The bonus for this is that it - # gives a pretty nice speed boost to most clients since they can close their connection - # immediately. - # - # One additional caveat is that you don't have to specify the Content-length header - # as the HttpResponse will write this for you based on the out length. - class HttpResponse - attr_reader :socket - attr_reader :body - attr_writer :body - attr_reader :header - attr_reader :status - attr_writer :status - attr_reader :body_sent - attr_reader :header_sent - attr_reader :status_sent - - def initialize(socket, app_response) - @socket = socket - @app_response = app_response - @body = StringIO.new - app_response[2].each {|x| @body << x} - @status = app_response[0] - @reason = nil - @header = HeaderOut.new - @header[Const::DATE] = Time.now.httpdate - @header.merge!(app_response[1]) - @body_sent = false - @header_sent = false - @status_sent = false - end - - # Receives a block passing it the header and body for you to work with. - # When the block is finished it writes everything you've done to - # the socket in the proper order. This lets you intermix header and - # body content as needed. Handlers are able to modify pretty much - # any part of the request in the chain, and can stop further processing - # by simple passing "finalize=true" to the start method. By default - # all handlers run and then mongrel finalizes the request when they're - # all done. - # TODO: docs - def start #(status=200, finalize=false, reason=nil) - finished - end - - # Primarily used in exception handling to reset the response output in order to write - # 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 - if @body_sent - raise "You have already sent the request body." - elsif @header_sent - raise "You have already sent the request headers." - else - # XXX Dubious ( http://mongrel.rubyforge.org/ticket/19 ) - @header.out.close - @header = HeaderOut.new(StringIO.new) - - @body.close - @body = StringIO.new - end - end - - def send_status(content_length=@body.length) - if not @status_sent - @header['Content-Length'] = content_length if content_length and @status != 304 - write(Const::STATUS_FORMAT % [@status, HTTP_STATUS_CODES[@status]]) - @status_sent = true - end - end - - def send_header - if not @header_sent - @header.out.rewind - write(@header.out.read + Const::LINE_END) - @header_sent = true - end - end - - def send_body - if not @body_sent - @body.rewind - write(@body.read) - @body_sent = true - end - end - - # Appends the contents of +path+ to the response stream. The file is opened for binary - # reading and written in chunks to the socket. - # - # Sendfile API support has been removed in 0.3.13.4 due to stability problems. - def send_file(path, small_file = false) - if small_file - File.open(path, "rb") {|f| @socket << f.read } - else - File.open(path, "rb") do |f| - while chunk = f.read(Const::CHUNK_SIZE) and chunk.length > 0 - begin - write(chunk) - rescue Object => exc - break - end - end - end - end - @body_sent = true - end - - def socket_error(details) - # ignore these since it means the client closed off early - @socket.close rescue nil - done = true - raise details - end - - def write(data) - @socket.write(data) - rescue => details - socket_error(details) - end - - # This takes whatever has been done to header and body and then writes it in the - # proper format to make an HTTP/1.1 response. - def finished - send_status - send_header - send_body - end - - # Used during error conditions to mark the response as "done" so there isn't any more processing - # sent to the client. - def done=(val) - @status_sent = true - @header_sent = true - @body_sent = true - end - - def done - (@status_sent and @header_sent and @body_sent) - end - - end -end diff --git a/lib/mongrel/init.rb b/lib/mongrel/init.rb deleted file mode 100644 index 00911f4..0000000 --- a/lib/mongrel/init.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (c) 2005 Zed A. Shaw -# You can redistribute it and/or modify it under the same terms as Ruby. -# -# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html -# for more information. - -require 'mongrel/gems' -Mongrel::Gems.require 'gem_plugin' - -# File is just a stub that makes sure the mongrel_plugins gem is loaded and ready diff --git a/lib/mongrel/mime_types.yml b/lib/mongrel/mime_types.yml deleted file mode 100644 index 3ce1739..0000000 --- a/lib/mongrel/mime_types.yml +++ /dev/null @@ -1,616 +0,0 @@ ---- -.a: application/octet-stream -.abc: text/vnd.abc -.acgi: text/html -.afl: video/animaflex -.ai: application/postscript -.aif: audio/aiff -.aifc: audio/aiff -.aiff: audio/aiff -.aip: text/x-audiosoft-intra -.ani: application/x-navi-animation -.aps: application/mime -.arc: application/octet-stream -.arj: application/octet-stream -.art: image/x-jg -.asf: video/x-ms-asf -.asm: text/x-asm -.asp: text/asp -.asr: video/x-ms-asf -.asx: video/x-ms-asf -.atom: application/atom+xml -.au: audio/basic -.au: audio/x-au -.avi: video/avi -.avs: video/avs-video -.axs: application/olescript -.bas: text/plain -.bcpio: application/x-bcpio -.bin: application/octet-stream -.bm: image/bmp -.bmp: image/bmp -.boo: application/book -.book: application/book -.boz: application/x-bzip2 -.bsh: application/x-bsh -.bz2: application/x-bzip2 -.bz: application/x-bzip -.c: text/plain -.cat: application/octet-stream -.cc: text/plain -.ccad: application/clariscad -.cco: application/x-cocoa -.cdf: application/cdf -.cer: application/x-x509-ca-cert -.cha: application/x-chat -.chat: application/x-chat -.class: application/java -.class: application/octet-stream -.clp: application/x-msclip -.cmx: image/x-cmx -.cod: image/cis-cod -.com: application/octet-stream -.com: text/plain -.conf: text/plain -.cpio: application/x-cpio -.cpp: text/x-c -.cpt: application/x-cpt -.crd: application/x-mscardfile -.crl: application/pkcs-crl -.crl: application/pkix-crl -.crt: application/x-x509-ca-cert -.csh: application/x-csh -.csh: text/x-script.csh -.css: text/css -.cxx: text/plain -.dcr: application/x-director -.deb: application/octet-stream -.deepv: application/x-deepv -.def: text/plain -.der: application/x-x509-ca-cert -.dhh: application/david-heinemeier-hansson -.dif: video/x-dv -.dir: application/x-director -.dl: video/dl -.dll: application/octet-stream -.dmg: application/octet-stream -.dms: application/octet-stream -.doc: application/msword -.dp: application/commonground -.drw: application/drafting -.dump: application/octet-stream -.dv: video/x-dv -.dvi: application/x-dvi -.dwg: application/acad -.dwg: image/x-dwg -.dxf: application/dxf -.dxf: image/x-dwg -.dxr: application/x-director -.ear: application/java-archive -.el: text/x-script.elisp -.elc: application/x-bytecode.elisp (compiled elisp) -.elc: application/x-elc -.env: application/x-envoy -.eot: application/octet-stream -.eps: application/postscript -.es: application/x-esrehber -.etx: text/x-setext -.evy: application/envoy -.evy: application/x-envoy -.exe: application/octet-stream -.f77: text/x-fortran -.f90: text/plain -.f90: text/x-fortran -.f: text/x-fortran -.fdf: application/vnd.fdf -.fif: application/fractals -.fif: image/fif -.fli: video/fli -.fli: video/x-fli -.flo: image/florian -.flr: x-world/x-vrml -.flv: video/x-flv -.flx: text/vnd.fmi.flexstor -.fmf: video/x-atomic3d-feature -.for: text/plain -.for: text/x-fortran -.fpx: image/vnd.fpx -.fpx: image/vnd.net-fpx -.frl: application/freeloader -.funk: audio/make -.g3: image/g3fax -.g: text/plain -.gif: image/gif -.gl: video/gl -.gl: video/x-gl -.gsd: audio/x-gsm -.gsm: audio/x-gsm -.gsp: application/x-gsp -.gss: application/x-gss -.gtar: application/x-gtar -.gz: application/x-compressed -.gzip: application/x-gzip -.h: text/plain -.hdf: application/x-hdf -.help: application/x-helpfile -.hgl: application/vnd.hp-hpgl -.hh: text/plain -.hlb: text/x-script -.hlp: application/hlp -.hpg: application/vnd.hp-hpgl -.hpgl: application/vnd.hp-hpgl -.hqx: application/binhex -.hta: application/hta -.htc: text/x-component -.htm: text/html -.html: text/html -.htmls: text/html -.htt: text/webviewhtml -.htx: text/html -.ico: image/x-icon -.idc: text/plain -.ief: image/ief -.iefs: image/ief -.iges: application/iges -.igs: application/iges -.iii: application/x-iphone -.ima: application/x-ima -.imap: application/x-httpd-imap -.img: application/octet-stream -.inf: application/inf -.ins: application/x-internet-signup -.ins: application/x-internett-signup -.ip: application/x-ip2 -.iso: application/octet-stream -.isp: application/x-internet-signup -.isu: video/x-isvideo -.it: audio/it -.iv: application/x-inventor -.ivr: i-world/i-vrml -.ivy: application/x-livescreen -.jam: audio/x-jam -.jar: application/java-archive -.jardiff: application/x-java-archive-diff -.jav: text/plain -.jav: text/x-java-source -.java: text/plain -.java: text/x-java-source -.jcm: application/x-java-commerce -.jfif-tbnl: image/jpeg -.jfif: image/jpeg -.jfif: image/pipeg -.jfif: image/pjpeg -.jng: image/x-jng -.jnlp: application/x-java-jnlp-file -.jpe: image/jpeg -.jpeg: image/jpeg -.jpg: image/jpeg -.jps: image/x-jps -.js: application/x-javascript -.js: text/javascript -.jut: image/jutvision -.kar: audio/midi -.kar: music/x-karaoke -.ksh: application/x-ksh -.ksh: text/x-script.ksh -.la: audio/nspaudio -.la: audio/x-nspaudio -.lam: audio/x-liveaudio -.latex: application/x-latex -.lha: application/lha -.lha: application/octet-stream -.lha: application/x-lha -.lhx: application/octet-stream -.list: text/plain -.lma: audio/nspaudio -.lma: audio/x-nspaudio -.log: text/plain -.lsf: video/x-la-asf -.lsp: application/x-lisp -.lsp: text/x-script.lisp -.lst: text/plain -.lsx: text/x-la-asf -.lsx: video/x-la-asf -.ltx: application/x-latex -.lzh: application/octet-stream -.lzh: application/x-lzh -.lzx: application/lzx -.lzx: application/octet-stream -.lzx: application/x-lzx -.m13: application/x-msmediaview -.m14: application/x-msmediaview -.m1v: video/mpeg -.m2a: audio/mpeg -.m2v: video/mpeg -.m3u: audio/x-mpegurl -.m: text/x-m -.man: application/x-troff-man -.map: application/x-navimap -.mar: text/plain -.mbd: application/mbedlet -.mc: application/x-magic-cap-package-1.0 -.mcd: application/mcad -.mcd: application/x-mathcad -.mcf: image/vasa -.mcf: text/mcf -.mcp: application/netmc -.mdb: application/x-msaccess -.me: application/x-troff-me -.mht: message/rfc822 -.mhtml: message/rfc822 -.mid: audio/mid -.mid: audio/midi -.mid: audio/x-mid -.mid: audio/x-midi -.midi: audio/midi -.midi: audio/x-mid -.midi: audio/x-midi -.mif: application/x-frame -.mif: application/x-mif -.mime: message/rfc822 -.mime: www/mime -.mjf: audio/x-vnd.audioexplosion.mjuicemediafile -.mjpg: video/x-motion-jpeg -.mm: application/base64 -.mm: application/x-meme -.mme: application/base64 -.mml: text/mathml -.mng: video/x-mng -.mod: audio/mod -.moov: video/quicktime -.mov: video/quicktime -.movie: video/x-sgi-movie -.mp2: audio/mpeg -.mp3: audio/mpeg -.mpa: audio/mpeg -.mpc: application/x-project -.mpe: video/mpeg -.mpeg: video/mpeg -.mpg: video/mpeg -.mpga: audio/mpeg -.mpp: application/vnd.ms-project -.mpt: application/x-project -.mpv2: video/mpeg -.mpv: application/x-project -.mpx: application/x-project -.mrc: application/marc -.ms: application/x-troff-ms -.msi: application/octet-stream -.msm: application/octet-stream -.msp: application/octet-stream -.mv: video/x-sgi-movie -.mvb: application/x-msmediaview -.my: audio/make -.mzz: application/x-vnd.audioexplosion.mzz -.nap: image/naplps -.naplps: image/naplps -.nc: application/x-netcdf -.ncm: application/vnd.nokia.configuration-message -.nif: image/x-niff -.niff: image/x-niff -.nix: application/x-mix-transfer -.nsc: application/x-conference -.nvd: application/x-navidoc -.nws: message/rfc822 -.o: application/octet-stream -.oda: application/oda -.omc: application/x-omc -.omcd: application/x-omcdatamaker -.omcr: application/x-omcregerator -.p10: application/pkcs10 -.p10: application/x-pkcs10 -.p12: application/pkcs-12 -.p12: application/x-pkcs12 -.p7a: application/x-pkcs7-signature -.p7b: application/x-pkcs7-certificates -.p7c: application/pkcs7-mime -.p7c: application/x-pkcs7-mime -.p7m: application/pkcs7-mime -.p7m: application/x-pkcs7-mime -.p7r: application/x-pkcs7-certreqresp -.p7s: application/pkcs7-signature -.p7s: application/x-pkcs7-signature -.p: text/x-pascal -.part: application/pro_eng -.pas: text/pascal -.pbm: image/x-portable-bitmap -.pcl: application/vnd.hp-pcl -.pcl: application/x-pcl -.pct: image/x-pict -.pcx: image/x-pcx -.pdb: application/x-pilot -.pdf: application/pdf -.pem: application/x-x509-ca-cert -.pfunk: audio/make -.pfunk: audio/make.my.funk -.pfx: application/x-pkcs12 -.pgm: image/x-portable-graymap -.pgm: image/x-portable-greymap -.pic: image/pict -.pict: image/pict -.pkg: application/x-newton-compatible-pkg -.pko: application/vnd.ms-pki.pko -.pko: application/ynd.ms-pkipko -.pl: application/x-perl -.pl: text/plain -.pl: text/x-script.perl -.plx: application/x-pixclscript -.pm4: application/x-pagemaker -.pm5: application/x-pagemaker -.pm: application/x-perl -.pm: image/x-xpixmap -.pm: text/x-script.perl-module -.pma: application/x-perfmon -.pmc: application/x-perfmon -.pml: application/x-perfmon -.pmr: application/x-perfmon -.pmw: application/x-perfmon -.png: image/png -.pnm: application/x-portable-anymap -.pnm: image/x-portable-anymap -.pot,: application/vnd.ms-powerpoint -.pot: application/mspowerpoint -.pot: application/vnd.ms-powerpoint -.pov: model/x-pov -.ppa: application/vnd.ms-powerpoint -.ppm: image/x-portable-pixmap -.pps: application/mspowerpoint -.ppt: application/mspowerpoint -.ppz: application/mspowerpoint -.prc: application/x-pilot -.pre: application/x-freelance -.prf: application/pics-rules -.prt: application/pro_eng -.ps: application/postscript -.psd: application/octet-stream -.pub: application/x-mspublisher -.pvu: paleovu/x-pv -.pwz: application/vnd.ms-powerpoint -.py: text/x-script.phyton -.pyc: applicaiton/x-bytecode.python -.qcp: audio/vnd.qcelp -.qd3: x-world/x-3dmf -.qd3d: x-world/x-3dmf -.qif: image/x-quicktime -.qt: video/quicktime -.qtc: video/x-qtc -.qti: image/x-quicktime -.qtif: image/x-quicktime -.ra: audio/x-pn-realaudio -.ra: audio/x-pn-realaudio-plugin -.ra: audio/x-realaudio -.ram: audio/x-pn-realaudio -.rar: application/x-rar-compressed -.ras: application/x-cmu-raster -.ras: image/cmu-raster -.ras: image/x-cmu-raster -.rast: image/cmu-raster -.rexx: text/x-script.rexx -.rf: image/vnd.rn-realflash -.rgb: image/x-rgb -.rm: application/vnd.rn-realmedia -.rm: audio/x-pn-realaudio -.rmi: audio/mid -.rmm: audio/x-pn-realaudio -.rmp: audio/x-pn-realaudio -.rmp: audio/x-pn-realaudio-plugin -.rng: application/ringing-tones -.rng: application/vnd.nokia.ringing-tone -.rnx: application/vnd.rn-realplayer -.roff: application/x-troff -.rp: image/vnd.rn-realpix -.rpm: application/x-redhat-package-manager -.rpm: audio/x-pn-realaudio-plugin -.rss: text/xml -.rt: text/richtext -.rt: text/vnd.rn-realtext -.rtf: application/rtf -.rtf: application/x-rtf -.rtf: text/richtext -.rtx: application/rtf -.rtx: text/richtext -.run: application/x-makeself -.rv: video/vnd.rn-realvideo -.s3m: audio/s3m -.s: text/x-asm -.saveme: application/octet-stream -.sbk: application/x-tbook -.scd: application/x-msschedule -.scm: application/x-lotusscreencam -.scm: text/x-script.guile -.scm: text/x-script.scheme -.scm: video/x-scm -.sct: text/scriptlet -.sdml: text/plain -.sdp: application/sdp -.sdp: application/x-sdp -.sdr: application/sounder -.sea: application/sea -.sea: application/x-sea -.set: application/set -.setpay: application/set-payment-initiation -.setreg: application/set-registration-initiation -.sgm: text/sgml -.sgm: text/x-sgml -.sgml: text/sgml -.sgml: text/x-sgml -.sh: application/x-bsh -.sh: application/x-sh -.sh: application/x-shar -.sh: text/x-script.sh -.shar: application/x-bsh -.shar: application/x-shar -.shtml: text/html -.shtml: text/x-server-parsed-html -.sid: audio/x-psid -.sit: application/x-sit -.sit: application/x-stuffit -.skd: application/x-koan -.skm: application/x-koan -.skp: application/x-koan -.skt: application/x-koan -.sl: application/x-seelogo -.smi: application/smil -.smil: application/smil -.snd: audio/basic -.snd: audio/x-adpcm -.sol: application/solids -.spc: application/x-pkcs7-certificates -.spc: text/x-speech -.spl: application/futuresplash -.spr: application/x-sprite -.sprite: application/x-sprite -.src: application/x-wais-source -.ssi: text/x-server-parsed-html -.ssm: application/streamingmedia -.sst: application/vnd.ms-pki.certstore -.sst: application/vnd.ms-pkicertstore -.step: application/step -.stl: application/sla -.stl: application/vnd.ms-pki.stl -.stl: application/vnd.ms-pkistl -.stl: application/x-navistyle -.stm: text/html -.stp: application/step -.sv4cpio: application/x-sv4cpio -.sv4crc: application/x-sv4crc -.svf: image/vnd.dwg -.svf: image/x-dwg -.svg: image/svg+xml -.svr: application/x-world -.svr: x-world/x-svr -.swf: application/x-shockwave-flash -.t: application/x-troff -.talk: text/x-speech -.tar: application/x-tar -.tbk: application/toolbook -.tbk: application/x-tbook -.tcl: application/x-tcl -.tcl: text/x-script.tcl -.tcsh: text/x-script.tcsh -.tex: application/x-tex -.texi: application/x-texinfo -.texinfo: application/x-texinfo -.text: application/plain -.text: text/plain -.tgz: application/gnutar -.tgz: application/x-compressed -.tif: image/tiff -.tiff: image/tiff -.tk: application/x-tcl -.tr: application/x-troff -.trm: application/x-msterminal -.tsi: audio/tsp-audio -.tsp: application/dsptype -.tsp: audio/tsplayer -.tsv: text/tab-separated-values -.turbot: image/florian -.txt: text/plain -.uil: text/x-uil -.uls: text/iuls -.uni: text/uri-list -.unis: text/uri-list -.unv: application/i-deas -.uri: text/uri-list -.uris: text/uri-list -.ustar: application/x-ustar -.ustar: multipart/x-ustar -.uu: application/octet-stream -.uu: text/x-uuencode -.uue: text/x-uuencode -.vcd: application/x-cdlink -.vcf: text/x-vcard -.vcs: text/x-vcalendar -.vda: application/vda -.vdo: video/vdo -.vew: application/groupwise -.viv: video/vivo -.viv: video/vnd.vivo -.vivo: video/vivo -.vivo: video/vnd.vivo -.vmd: application/vocaltec-media-desc -.vmf: application/vocaltec-media-file -.voc: audio/voc -.voc: audio/x-voc -.vos: video/vosaic -.vox: audio/voxware -.vqe: audio/x-twinvq-plugin -.vqf: audio/x-twinvq -.vql: audio/x-twinvq-plugin -.vrml: application/x-vrml -.vrml: model/vrml -.vrml: x-world/x-vrml -.vrt: x-world/x-vrt -.vsd: application/x-visio -.vst: application/x-visio -.vsw: application/x-visio -.w60: application/wordperfect6.0 -.w61: application/wordperfect6.1 -.w6w: application/msword -.war: application/java-archive -.wav: audio/wav -.wav: audio/x-wav -.wb1: application/x-qpro -.wbmp: image/vnd.wap.wbmp -.wbmp: image/vnd.wap.wbmp -.wcm: application/vnd.ms-works -.wdb: application/vnd.ms-works -.web: application/vnd.xara -.wiz: application/msword -.wk1: application/x-123 -.wks: application/vnd.ms-works -.wmf: application/x-msmetafile -.wmf: windows/metafile -.wml: text/vnd.wap.wml -.wmlc: application/vnd.wap.wmlc -.wmls: text/vnd.wap.wmlscript -.wmlsc: application/vnd.wap.wmlscriptc -.wmv: video/x-ms-wmv -.word: application/msword -.wp5: application/wordperfect -.wp6: application/wordperfect -.wp: application/wordperfect -.wpd: application/wordperfect -.wps: application/vnd.ms-works -.wq1: application/x-lotus -.wri: application/mswrite -.wrl: application/x-world -.wsc: text/scriplet -.wsrc: application/x-wais-source -.wtk: application/x-wintalk -.x-png: image/png -.xaf: x-world/x-vrml -.xbm: image/xbm -.xdr: video/x-amt-demorun -.xgz: xgl/drawing -.xhtml: application/xhtml+xml -.xif: image/vnd.xiff -.xl: application/excel -.xla: application/excel -.xlb: application/excel -.xlc: application/excel -.xld: application/excel -.xlk: application/excel -.xll: application/excel -.xlm: application/excel -.xls: application/excel -.xlt: application/excel -.xlv: application/excel -.xlw: application/excel -.xm: audio/xm -.xml: text/xml -.xmz: xgl/movie -.xof: x-world/x-vrml -.xpi: application/x-xpinstall -.xpix: application/x-vnd.ls-xpix -.xpm: image/x-xpixmap -.xpm: image/xpm -.xsl: application/xslt+xml -.xsr: video/x-amt-showrun -.xwd: image/x-xwd -.xwd: image/x-xwindowdump -.xyz: chemical/x-pdb -.z: application/x-compressed -.zip: application/zip -.zoo: application/octet-stream -.zsh: text/x-script.zsh diff --git a/lib/mongrel/semaphore.rb b/lib/mongrel/semaphore.rb deleted file mode 100644 index 1c0b87c..0000000 --- a/lib/mongrel/semaphore.rb +++ /dev/null @@ -1,46 +0,0 @@ -class Semaphore - def initialize(resource_count = 0) - @available_resource_count = resource_count - @mutex = Mutex.new - @waiting_threads = [] - end - - def wait - make_thread_wait unless resource_is_available - end - - def signal - schedule_waiting_thread if thread_is_waiting - end - - def synchronize - self.wait - yield - ensure - self.signal - end - - private - - def resource_is_available - @mutex.synchronize do - return (@available_resource_count -= 1) >= 0 - end - end - - def make_thread_wait - @waiting_threads << Thread.current - Thread.stop - end - - def thread_is_waiting - @mutex.synchronize do - return (@available_resource_count += 1) <= 0 - end - end - - def schedule_waiting_thread - thread = @waiting_threads.shift - thread.wakeup if thread - end -end diff --git a/lib/mongrel/tcphack.rb b/lib/mongrel/tcphack.rb deleted file mode 100644 index 634f9dd..0000000 --- a/lib/mongrel/tcphack.rb +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2005 Zed A. Shaw -# You can redistribute it and/or modify it under the same terms as Ruby. -# -# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html -# for more information. - - -# A modification proposed by Sean Treadway that increases the default accept -# queue of TCPServer to 1024 so that it handles more concurrent requests. -class TCPServer - def initialize_with_backlog(*args) - initialize_without_backlog(*args) - listen(1024) - end - - alias_method :initialize_without_backlog, :initialize - alias_method :initialize, :initialize_with_backlog -end diff --git a/lib/unicorn.rb b/lib/unicorn.rb new file mode 100644 index 0000000..e43b676 --- /dev/null +++ b/lib/unicorn.rb @@ -0,0 +1,284 @@ + +# Standard libraries +require 'socket' +require 'tempfile' +require 'yaml' +require 'time' +require 'etc' +require 'uri' +require 'stringio' +require 'fcntl' +require 'logger' + +# Compiled extension +require 'http11' + +# Gem conditional loader +require 'thread' +require 'rack' + +require 'unicorn/tcphack' +require 'unicorn/const' +require 'unicorn/http_request' +require 'unicorn/header_out' +require 'unicorn/http_response' +require 'unicorn/semaphore' + +# Unicorn module containing all of the classes (include C extensions) for running +# a Unicorn web server. It contains a minimalist HTTP server with just enough +# functionality to service web application requests fast as possible. +module Unicorn + class << self + # A logger instance that conforms to the API of stdlib's Logger. + attr_accessor :logger + + def run(app, options = {}) + HttpServer.new(app, options).start.join + end + end + + # Used to stop the HttpServer via Thread.raise. + class StopServer < Exception; end + + # Thrown at a thread when it is timed out. + class TimeoutError < Exception; end + + # Thrown by HttpServer#stop if the server is not started. + class AcceptorError < StandardError; end + + # + # This is the main driver of Unicorn, while the Unicorn::HttpParser and Unicorn::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. + # + class HttpServer + attr_reader :acceptor, :workers, :logger, :host, :port, :timeout, :max_queued_threads, :max_concurrent_threads + + DEFAULTS = { + :timeout => 60, + :host => '0.0.0.0', + :port => 8080, + :logger => Logger.new(STDERR), + :max_queued_threads => 12, + :max_concurrent_threads => 4 + } + + # 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 max_queued_threads 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. + # + def initialize(app, options = {}) + @app = app + @workers = ThreadGroup.new + + (DEFAULTS.to_a + options.to_a).each do |key, value| + instance_variable_set("@#{key.to_s.downcase}", value) + end + + @socket = TCPServer.new(@host, @port) + @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined?(Fcntl::FD_CLOEXEC) + end + + # 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. + def process_client(client) + begin + parser = HttpParser.new + params = Hash.new + request = nil + data = client.readpartial(Const::CHUNK_SIZE) + nparsed = 0 + + # Assumption: nparsed will always be less since data will get filled with more + # after each parsing. If it doesn't get more then there was a problem + # with the read operation on the client socket. Effect is to stop processing when the + # socket can't fill the buffer for further parsing. + while nparsed < data.length + nparsed = parser.execute(params, data, nparsed) + + if parser.finished? + if !params[Const::REQUEST_PATH] + # It might be a dumbass full host request header + uri = URI.parse(params[Const::REQUEST_URI]) + params[Const::REQUEST_PATH] = uri.path + end + + raise "No REQUEST PATH" if !params[Const::REQUEST_PATH] + + params[Const::PATH_INFO] = params[Const::REQUEST_PATH] + params[Const::SCRIPT_NAME] = Const::SLASH + + # From http://www.ietf.org/rfc/rfc3875 : + # "Script authors should be aware that the REMOTE_ADDR and REMOTE_HOST + # meta-variables (see sections 4.1.8 and 4.1.9) may not identify the + # ultimate source of the request. They identify the client for the + # immediate request to the server; that client may be a proxy, gateway, + # or other intermediary acting on behalf of the actual source client." + params[Const::REMOTE_ADDR] = client.peeraddr.last + + # Select handlers that want more detailed request notification + request = HttpRequest.new(params, client, logger) + + # in the case of large file uploads the user could close the socket, so skip those requests + break if request.body == nil # nil signals from HttpRequest::initialize that the request was aborted + app_response = @app.call(request.env) + response = HttpResponse.new(client, app_response).start + break #done + else + # Parser is not done, queue up more data to read and continue parsing + chunk = client.readpartial(Const::CHUNK_SIZE) + break if !chunk or chunk.length == 0 # read failed, stop processing + + data << chunk + if data.length >= Const::MAX_HEADER + raise HttpParserError.new("HEADER is longer than allowed, aborting client early.") + end + end + end + rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF + client.close rescue nil + rescue HttpParserError => e + logger.error "HTTP parse error, malformed request (#{params[Const::HTTP_X_FORWARDED_FOR] || client.peeraddr.last}): #{e.inspect}" + logger.error "REQUEST DATA: #{data.inspect}\n---\nPARAMS: #{params.inspect}\n---\n" + rescue Errno::EMFILE + reap_dead_workers('too many files') + rescue Object => e + logger.error "Read error: #{e.inspect}" + logger.error e.backtrace.join("\n") + ensure + begin + client.close + rescue IOError + # Already closed + rescue Object => e + logger.error "Client error: #{e.inspect}" + logger.error e.backtrace.join("\n") + end + request.body.close! if request and request.body.class == Tempfile + 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. It returns the count of workers still active + # after the reap is done. It only runs if there are workers to reap. + def reap_dead_workers(reason='unknown') + if @workers.list.length > 0 + logger.info "Reaping #{@workers.list.length} threads for slow workers because of '#{reason}'" + error_msg = "Unicorn timed out this thread: #{reason}" + mark = Time.now + @workers.list.each do |worker| + worker[:started_on] = Time.now if not worker[:started_on] + + if mark - worker[:started_on] > @timeout + logger.info "Thread #{worker.inspect} is too old, killing." + worker.raise(TimeoutError.new(error_msg)) + end + end + end + + return @workers.list.length + end + + # Performs a wait on all the currently running threads and kills any that take + # too long. It waits by @timeout seconds, which can be set in .initialize or + # via mongrel_rails. + def graceful_shutdown + while reap_dead_workers("shutdown") > 0 + logger.info "Waiting for #{@workers.list.length} requests to finish, could take #{@timeout} seconds." + sleep @timeout / 10 + end + end + + def configure_socket_options + case RUBY_PLATFORM + when /linux/ + # 9 is currently TCP_DEFER_ACCEPT + $tcp_defer_accept_opts = [Socket::SOL_TCP, 9, 1] + $tcp_cork_opts = [Socket::SOL_TCP, 3, 1] + when /freebsd(([1-4]\..{1,2})|5\.[0-4])/ + # Do nothing, just closing a bug when freebsd <= 5.4 + when /freebsd/ + # Use the HTTP accept filter if available. + # The struct made by pack() is defined in /usr/include/sys/socket.h as accept_filter_arg + unless `/sbin/sysctl -nq net.inet.accf.http`.empty? + $tcp_defer_accept_opts = [Socket::SOL_SOCKET, Socket::SO_ACCEPTFILTER, ['httpready', nil].pack('a16a240')] + 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 start + semaphore = Semaphore.new(@max_concurrent_threads) + BasicSocket.do_not_reverse_lookup = true + + configure_socket_options + + if defined?($tcp_defer_accept_opts) and $tcp_defer_accept_opts + @socket.setsockopt(*$tcp_defer_accept_opts) rescue nil + end + + @acceptor = Thread.new do + begin + while true + begin + client = @socket.accept + + if defined?($tcp_cork_opts) and $tcp_cork_opts + client.setsockopt(*$tcp_cork_opts) rescue nil + end + + worker_list = @workers.list + if worker_list.length >= @max_queued_threads + logger.error "Server overloaded with #{worker_list.length} processors (#@max_queued_threads max). Dropping connection." + client.close rescue nil + reap_dead_workers("max processors") + else + thread = Thread.new(client) {|c| semaphore.synchronize { process_client(c) } } + thread[:started_on] = Time.now + @workers.add(thread) + end + rescue StopServer + break + rescue Errno::EMFILE + reap_dead_workers("too many open files") + sleep 0.5 + rescue Errno::ECONNABORTED + # client closed the socket even before accept + client.close rescue nil + rescue Object => e + logger.error "Unhandled listen loop exception #{e.inspect}." + logger.error e.backtrace.join("\n") + end + end + graceful_shutdown + ensure + @socket.close + logger.info "Closed socket." + end + end + + @acceptor + end + + # Stops the acceptor thread and then causes the worker threads to finish + # off the request queue before finally exiting. + def stop(synchronous = false) + raise AcceptorError, "Server was not started." unless @acceptor + @acceptor.raise(StopServer.new) + (sleep(0.5) while @acceptor.alive?) if synchronous + @acceptor = nil + end + end +end diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb new file mode 100644 index 0000000..56c3bb4 --- /dev/null +++ b/lib/unicorn/const.rb @@ -0,0 +1,113 @@ + +module Unicorn + + # Every standard HTTP code mapped to the appropriate message. These are + # used so frequently that they are placed directly in Unicorn for easy + # access rather than Unicorn::Const itself. + HTTP_STATUS_CODES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Moved Temporarily', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported' + } + + # Frequently used constants when constructing requests or responses. Many times + # the constant just refers to a string with the same contents. Using these constants + # gave about a 3% to 10% performance improvement over using the strings directly. + # Symbols did not really improve things much compared to constants. + # + # While Unicorn does try to emulate the CGI/1.2 protocol, it does not use the REMOTE_IDENT, + # REMOTE_USER, or REMOTE_HOST parameters since those are either a security problem or + # too taxing on performance. + module Const + DATE="Date".freeze + + # This is the part of the path after the SCRIPT_NAME. + PATH_INFO="PATH_INFO".freeze + + # Request body + HTTP_BODY="HTTP_BODY".freeze + + # This is the initial part that your handler is identified as by URIClassifier. + SCRIPT_NAME="SCRIPT_NAME".freeze + + # The original URI requested by the client. Passed to URIClassifier to build PATH_INFO and SCRIPT_NAME. + REQUEST_URI='REQUEST_URI'.freeze + REQUEST_PATH='REQUEST_PATH'.freeze + + UNICORN_VERSION="0.2.0".freeze + + UNICORN_TMP_BASE="unicorn".freeze + + # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff. + ERROR_404_RESPONSE="HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Unicorn #{UNICORN_VERSION}\r\n\r\nNOT FOUND".freeze + + CONTENT_LENGTH="CONTENT_LENGTH".freeze + + # A common header for indicating the server is too busy. Not used yet. + ERROR_503_RESPONSE="HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze + + # The basic max request size we'll try to read. + CHUNK_SIZE=(16 * 1024) + + # This is the maximum header that is allowed before a client is booted. The parser detects + # this, but we'd also like to do this as well. + MAX_HEADER=1024 * (80 + 32) + + # Maximum request body size before it is moved out of memory and into a tempfile for reading. + MAX_BODY=MAX_HEADER + + # A frozen format for this is about 15% faster + STATUS_FORMAT = "HTTP/1.1 %d %s\r\nConnection: close\r\n".freeze + CONTENT_TYPE = "Content-Type".freeze + LAST_MODIFIED = "Last-Modified".freeze + ETAG = "ETag".freeze + SLASH = "/".freeze + REQUEST_METHOD="REQUEST_METHOD".freeze + GET="GET".freeze + HEAD="HEAD".freeze + # ETag is based on the apache standard of hex mtime-size-inode (inode is 0 on win32) + ETAG_FORMAT="\"%x-%x-%x\"".freeze + HEADER_FORMAT="%s: %s\r\n".freeze + LINE_END="\r\n".freeze + REMOTE_ADDR="REMOTE_ADDR".freeze + HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze + HTTP_IF_MODIFIED_SINCE="HTTP_IF_MODIFIED_SINCE".freeze + HTTP_IF_NONE_MATCH="HTTP_IF_NONE_MATCH".freeze + REDIRECT = "HTTP/1.1 302 Found\r\nLocation: %s\r\nConnection: close\r\n\r\n".freeze + HOST = "HOST".freeze + end +end diff --git a/lib/unicorn/header_out.rb b/lib/unicorn/header_out.rb new file mode 100644 index 0000000..a4d987c --- /dev/null +++ b/lib/unicorn/header_out.rb @@ -0,0 +1,34 @@ +module Unicorn + # This class implements a simple way of constructing the HTTP headers dynamically + # via a Hash syntax. Think of it as a write-only Hash. Refer to HttpResponse for + # information on how this is used. + # + # One consequence of this write-only nature is that you can write multiple headers + # by just doing them twice (which is sometimes needed in HTTP), but that the normal + # semantics for Hash (where doing an insert replaces) is not there. + class HeaderOut + attr_reader :out + attr_accessor :allowed_duplicates + + def initialize(out = StringIO.new) + @sent = {} + @allowed_duplicates = {"Set-Cookie" => true, "Set-Cookie2" => true, + "Warning" => true, "WWW-Authenticate" => true} + @out = out + end + + def merge!(hash) + hash.each do |key, value| + self[key] = value + end + end + + # Simply writes "#{key}: #{value}" to an output buffer. + def[]=(key,value) + if not @sent.has_key?(key) or @allowed_duplicates.has_key?(key) + @sent[key] = true + @out.write(Const::HEADER_FORMAT % [key, value]) + end + end + end +end diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb new file mode 100644 index 0000000..a76d4e0 --- /dev/null +++ b/lib/unicorn/http_request.rb @@ -0,0 +1,106 @@ + +module Unicorn + # + # The HttpRequest.initialize method will convert any request that is larger than + # Const::MAX_BODY into a Tempfile and use that as the body. Otherwise it uses + # a StringIO object. To be safe, you should assume it works like a file. + # + class HttpRequest + attr_reader :body, :params, :logger + + # You don't really call this. It's made for you. + # Main thing it does is hook up the params, and store any remaining + # body data into the HttpRequest.body attribute. + def initialize(params, socket, logger) + @params = params + @socket = socket + @logger = logger + + content_length = @params[Const::CONTENT_LENGTH].to_i + remain = content_length - @params[Const::HTTP_BODY].length + + # Some clients (like FF1.0) report 0 for body and then send a body. This will probably truncate them but at least the request goes through usually. + if remain <= 0 + # we've got everything, pack it up + @body = StringIO.new + @body.write @params[Const::HTTP_BODY] + elsif remain > 0 + # must read more data to complete body + if remain > Const::MAX_BODY + # huge body, put it in a tempfile + @body = Tempfile.new(Const::UNICORN_TMP_BASE) + @body.binmode + else + # small body, just use that + @body = StringIO.new + end + + @body.write @params[Const::HTTP_BODY] + read_body(remain, content_length) + end + + @body.rewind if @body + end + + # Returns an environment which is rackable: http://rack.rubyforge.org/doc/files/SPEC.html + # Copied directly from Rack's old Unicorn handler. + def env + env = params.clone + env["QUERY_STRING"] ||= '' + env.delete "HTTP_CONTENT_TYPE" + env.delete "HTTP_CONTENT_LENGTH" + env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + env.update({"rack.version" => [0,1], + "rack.input" => @body, + "rack.errors" => STDERR, + + "rack.multithread" => true, + "rack.multiprocess" => false, # ??? + "rack.run_once" => false, + + "rack.url_scheme" => "http", + }) + end + + # Does the heavy lifting of properly reading the larger body requests in + # small chunks. It expects @body to be an IO object, @socket to be valid, + # and will set @body = nil if the request fails. It also expects any initial + # part of the body that has been read to be in the @body already. + def read_body(remain, total) + begin + # Write the odd sized chunk first + @params[Const::HTTP_BODY] = read_socket(remain % Const::CHUNK_SIZE) + + remain -= @body.write(@params[Const::HTTP_BODY]) + + # Then stream out nothing but perfectly sized chunks + until remain <= 0 or @socket.closed? + # ASSUME: we are writing to a disk and these writes always write the requested amount + @params[Const::HTTP_BODY] = read_socket(Const::CHUNK_SIZE) + remain -= @body.write(@params[Const::HTTP_BODY]) + end + rescue Object => e + logger.error "Error reading HTTP body: #{e.inspect}" + # Any errors means we should delete the file, including if the file is dumped + @socket.close rescue nil + @body.close! if @body.class == Tempfile + @body = nil # signals that there was a problem + end + end + + def read_socket(len) + if !@socket.closed? + data = @socket.read(len) + if !data + raise "Socket read return nil" + elsif data.length != len + raise "Socket read returned insufficient data: #{data.length}" + else + data + end + else + raise "Socket already closed when reading." + end + end + end +end diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb new file mode 100644 index 0000000..5fbc990 --- /dev/null +++ b/lib/unicorn/http_response.rb @@ -0,0 +1,167 @@ +module Unicorn + # Writes and controls your response to the client using the HTTP/1.1 specification. + # You use it by simply doing: + # + # response.start(200) do |head,out| + # head['Content-Type'] = 'text/plain' + # out.write("hello\n") + # end + # + # The parameter to start is the response code--which Unicorn will translate for you + # based on HTTP_STATUS_CODES. The head parameter is how you write custom headers. + # The out parameter is where you write your body. The default status code for + # HttpResponse.start is 200 so the above example is redundant. + # + # As you can see, it's just like using a Hash and as you do this it writes the proper + # header to the output on the fly. You can even intermix specifying headers and + # writing content. The HttpResponse class with write the things in the proper order + # once the HttpResponse.block is ended. + # + # You may also work the HttpResponse object directly using the various attributes available + # for the raw socket, body, header, and status codes. If you do this you're on your own. + # A design decision was made to force the client to not pipeline requests. HTTP/1.1 + # pipelining really kills the performance due to how it has to be handled and how + # unclear the standard is. To fix this the HttpResponse gives a "Connection: close" + # header which forces the client to close right away. The bonus for this is that it + # gives a pretty nice speed boost to most clients since they can close their connection + # immediately. + # + # One additional caveat is that you don't have to specify the Content-length header + # as the HttpResponse will write this for you based on the out length. + class HttpResponse + attr_reader :socket + attr_reader :body + attr_writer :body + attr_reader :header + attr_reader :status + attr_writer :status + attr_reader :body_sent + attr_reader :header_sent + attr_reader :status_sent + + def initialize(socket, app_response) + @socket = socket + @app_response = app_response + @body = StringIO.new + app_response[2].each {|x| @body << x} + @status = app_response[0] + @reason = nil + @header = HeaderOut.new + @header[Const::DATE] = Time.now.httpdate + @header.merge!(app_response[1]) + @body_sent = false + @header_sent = false + @status_sent = false + end + + # Receives a block passing it the header and body for you to work with. + # When the block is finished it writes everything you've done to + # the socket in the proper order. This lets you intermix header and + # body content as needed. Handlers are able to modify pretty much + # any part of the request in the chain, and can stop further processing + # by simple passing "finalize=true" to the start method. By default + # all handlers run and then mongrel finalizes the request when they're + # all done. + # TODO: docs + def start #(status=200, finalize=false, reason=nil) + finished + end + + # Primarily used in exception handling to reset the response output in order to write + # 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 + if @body_sent + raise "You have already sent the request body." + elsif @header_sent + raise "You have already sent the request headers." + else + # XXX Dubious ( http://mongrel.rubyforge.org/ticket/19 ) + @header.out.close + @header = HeaderOut.new(StringIO.new) + + @body.close + @body = StringIO.new + end + end + + def send_status(content_length=@body.length) + if not @status_sent + @header['Content-Length'] = content_length if content_length and @status != 304 + write(Const::STATUS_FORMAT % [@status, HTTP_STATUS_CODES[@status]]) + @status_sent = true + end + end + + def send_header + if not @header_sent + @header.out.rewind + write(@header.out.read + Const::LINE_END) + @header_sent = true + end + end + + def send_body + if not @body_sent + @body.rewind + write(@body.read) + @body_sent = true + end + end + + # Appends the contents of +path+ to the response stream. The file is opened for binary + # reading and written in chunks to the socket. + # + # Sendfile API support has been removed in 0.3.13.4 due to stability problems. + def send_file(path, small_file = false) + if small_file + File.open(path, "rb") {|f| @socket << f.read } + else + File.open(path, "rb") do |f| + while chunk = f.read(Const::CHUNK_SIZE) and chunk.length > 0 + begin + write(chunk) + rescue Object => exc + break + end + end + end + end + @body_sent = true + end + + def socket_error(details) + # ignore these since it means the client closed off early + @socket.close rescue nil + done = true + raise details + end + + def write(data) + @socket.write(data) + rescue => details + socket_error(details) + end + + # This takes whatever has been done to header and body and then writes it in the + # proper format to make an HTTP/1.1 response. + def finished + send_status + send_header + send_body + end + + # Used during error conditions to mark the response as "done" so there isn't any more processing + # sent to the client. + def done=(val) + @status_sent = true + @header_sent = true + @body_sent = true + end + + def done + (@status_sent and @header_sent and @body_sent) + end + + end +end diff --git a/lib/unicorn/semaphore.rb b/lib/unicorn/semaphore.rb new file mode 100644 index 0000000..1c0b87c --- /dev/null +++ b/lib/unicorn/semaphore.rb @@ -0,0 +1,46 @@ +class Semaphore + def initialize(resource_count = 0) + @available_resource_count = resource_count + @mutex = Mutex.new + @waiting_threads = [] + end + + def wait + make_thread_wait unless resource_is_available + end + + def signal + schedule_waiting_thread if thread_is_waiting + end + + def synchronize + self.wait + yield + ensure + self.signal + end + + private + + def resource_is_available + @mutex.synchronize do + return (@available_resource_count -= 1) >= 0 + end + end + + def make_thread_wait + @waiting_threads << Thread.current + Thread.stop + end + + def thread_is_waiting + @mutex.synchronize do + return (@available_resource_count += 1) <= 0 + end + end + + def schedule_waiting_thread + thread = @waiting_threads.shift + thread.wakeup if thread + end +end diff --git a/lib/unicorn/tcphack.rb b/lib/unicorn/tcphack.rb new file mode 100644 index 0000000..634f9dd --- /dev/null +++ b/lib/unicorn/tcphack.rb @@ -0,0 +1,18 @@ +# Copyright (c) 2005 Zed A. Shaw +# You can redistribute it and/or modify it under the same terms as Ruby. +# +# Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html +# for more information. + + +# A modification proposed by Sean Treadway that increases the default accept +# queue of TCPServer to 1024 so that it handles more concurrent requests. +class TCPServer + def initialize_with_backlog(*args) + initialize_without_backlog(*args) + listen(1024) + end + + alias_method :initialize_without_backlog, :initialize + alias_method :initialize, :initialize_with_backlog +end -- cgit v1.2.3-24-ge0c7