diff options
author | Eric Wong <normalperson@yhbt.net> | 2009-02-09 14:26:34 -0800 |
---|---|---|
committer | Eric Wong <normalperson@yhbt.net> | 2009-02-09 19:52:20 -0800 |
commit | 101fb9ad1372e97ddf998c7fd677e352719c90e8 (patch) | |
tree | 354b0cd16fa1ca81b2f24b843e7bedf2f1eeef97 /lib/unicorn | |
parent | 0b9dac5de7ecf8111dd3d9fa621edc759c9c47e3 (diff) | |
download | unicorn-101fb9ad1372e97ddf998c7fd677e352719c90e8.tar.gz |
Along with worker process management. This is nginx-style inplace upgrading (I don't know of another web server that does this). Basically we can preserve our opened listen sockets across entire executable upgrades. Signals: USR2 - Sending USR2 to the master unicorn process will cause it to exec a new master and keep the original workers running. This is useful to validate that the new code changes took place are valid and don't immediately die. Once the changes are validated (manually), you may send QUIT to the original master process to have it gracefully exit. HUP - Sending this to the master will make it immediately exec a new binary and cause the old workers to gracefully exit. Use this if you're certain the latest changes to Unicorn (and your app) are ready and don't need validating. Unlike nginx, re-execing a new binary will pick up any and all configuration changes. However listener sockets cannot be removed when exec-ing; only added (for now). I apologize for making such a big change in one commit, but once I got the ability to replace the entire codebase while preserving connections, it was too tempting to continue working. So I wrote a large chunk of this while hitting the unicorn-hello-world app with the following loop: while curl -vSsfN http://0:8080; do date +%N; done _Zero_ requests lost across multiple restarts.
Diffstat (limited to 'lib/unicorn')
-rw-r--r-- | lib/unicorn/http_request.rb | 14 | ||||
-rw-r--r-- | lib/unicorn/http_response.rb | 5 | ||||
-rw-r--r-- | lib/unicorn/socket.rb | 158 |
3 files changed, 111 insertions, 66 deletions
diff --git a/lib/unicorn/http_request.rb b/lib/unicorn/http_request.rb index 0a8c5b1..47600d6 100644 --- a/lib/unicorn/http_request.rb +++ b/lib/unicorn/http_request.rb @@ -1,3 +1,9 @@ +require 'tempfile' +require 'uri' +require 'stringio' + +# compiled extension +require 'http11' module Unicorn # @@ -54,7 +60,7 @@ module Unicorn # 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] = socket.unicorn_peeraddr.last + @params[Const::REMOTE_ADDR] = socket.unicorn_peeraddr handle_body(socket) and return rack_env # success! return nil # fail @@ -72,10 +78,10 @@ module Unicorn rescue HttpParserError => e @logger.error "HTTP parse error, malformed request " \ "(#{@params[Const::HTTP_X_FORWARDED_FOR] || - socket.unicorn_peeraddr.last}): #{e.inspect}" + socket.unicorn_peeraddr}): #{e.inspect}" @logger.error "REQUEST DATA: #{data.inspect}\n---\n" \ "PARAMS: #{@params.inspect}\n---\n" - socket.close rescue nil + socket.closed? or socket.close rescue nil nil end @@ -152,7 +158,7 @@ module Unicorn true # success! rescue Object => e logger.error "Error reading HTTP body: #{e.inspect}" - socket.close rescue nil + socket.closed? or socket.close rescue nil # Any errors means we should delete the file, including if the file # is dumped. Truncate it ASAP to help avoid page flushes to disk. diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb index eab3a82..1192d48 100644 --- a/lib/unicorn/http_response.rb +++ b/lib/unicorn/http_response.rb @@ -1,3 +1,5 @@ +require 'time' + module Unicorn # Writes a Rack response to your client using the HTTP/1.1 specification. # You use it by simply doing: @@ -33,11 +35,12 @@ module Unicorn 'WWW-Authenticate' => true, }.freeze + # writes the rack_response to socket as an HTTP response def self.write(socket, rack_response) status, headers, body = rack_response # Rack does not set/require Date, but don't worry about Content-Length - # since Rack enforces that in Rack::Lint. + # since Rack applications that conform to Rack::Lint enforce that out = [ "#{Const::DATE}: #{Time.now.httpdate}\r\n" ] sent = { Const::CONNECTION => true, Const::DATE => true } diff --git a/lib/unicorn/socket.rb b/lib/unicorn/socket.rb index 3f567c6..bc09688 100644 --- a/lib/unicorn/socket.rb +++ b/lib/unicorn/socket.rb @@ -1,84 +1,120 @@ +require 'fcntl' +require 'socket' +require 'io/nonblock' + # non-portable Socket code goes here: class Socket + module Constants + # configure platform-specific options (only tested on Linux 2.6 so far) + case RUBY_PLATFORM + when /linux/ + # from /usr/include/linux/tcp.h + TCP_DEFER_ACCEPT = 9 unless defined?(TCP_DEFER_ACCEPT) + TCP_CORK = 3 unless defined?(TCP_CORK) + when /freebsd(([1-4]\..{1,2})|5\.[0-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? + unless defined?(SO_ACCEPTFILTER_HTTPREADY) + SO_ACCEPTFILTER_HTTPREADY = ['httpready',nil].pack('a16a240').freeze + end - # configure platform-specific options (only tested on Linux 2.6 so far) - case RUBY_PLATFORM - when /linux/ - # from /usr/include/linux/tcp.h - TCP_DEFER_ACCEPT = 9 unless defined?(TCP_DEFER_ACCEPT) - TCP_CORK = 3 unless defined?(TCP_CORK) - - def unicorn_server_init - self.setsockopt(SOL_TCP, TCP_DEFER_ACCEPT, 1) - end - when /freebsd(([1-4]\..{1,2})|5\.[0-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? - unless defined?(SO_ACCEPTFILTER_HTTPREADY) - SO_ACCEPTFILTER_HTTPREADY = ['httpready',nil].pack('a16a240').freeze - end - - def unicorn_server_init - self.setsockopt(SOL_SOCKET, SO_ACCEPTFILTER, SO_ACCEPTFILTER_HTTPREADY) end end end +end - def unicorn_client_init - self.sync = true - self.nonblock = false - self.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) if defined?(TCP_NODELAY) - self.setsockopt(SOL_TCP, TCP_CORK, 1) if defined?(TCP_CORK) +class UNIXSocket + UNICORN_PEERADDR = '127.0.0.1'.freeze + def unicorn_peeraddr + UNICORN_PEERADDR end +end +class TCPSocket def unicorn_peeraddr - Socket.unpack_sockaddr_in(getpeername) + peeraddr.last end +end - # returns the config-friendly name of the current listener socket, this is - # useful for config reloads and even works across execs where the Unicorn - # binary is replaced - def unicorn_addr - @unicorn_addr ||= if respond_to?(:getsockname) - port, host = Socket.unpack_sockaddr_in(getsockname) - "#{host}:#{port}" - elsif respond_to?(:getsockname) - addr = Socket.unpack_sockaddr_un(getsockname) - # strip the pid from the temp socket path - addr.gsub!(/\.\d+\.tmp$/, '') or - raise ArgumentError, "PID not found in path: #{addr}" - else - raise ArgumentError, "could not determine unicorn_addr for #{self}" +module Unicorn + module SocketHelper + include Socket::Constants + + def set_client_sockopt(sock) + sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) if defined?(TCP_NODELAY) + sock.setsockopt(SOL_TCP, TCP_CORK, 1) if defined?(TCP_CORK) end - end - class << self + def set_server_sockopt(sock) + if defined?(TCP_DEFER_ACCEPT) + sock.setsockopt(SOL_TCP, TCP_DEFER_ACCEPT, 1) rescue nil + end + if defined?(SO_ACCEPTFILTER_HTTPREADY) + sock.setsockopt(SOL_SOCKET, SO_ACCEPTFILTER, + SO_ACCEPTFILTER_HTTPREADY) rescue nil + end + end + + # creates a new server, socket. address may be a HOST:PORT or + # an absolute path to a UNIX socket. address can even be a Socket + # object in which case it is immediately returned + def bind_listen(address = '0.0.0.0:8080', backlog = 1024) + return address if address.kind_of?(Socket) - # creates a new server, address may be a HOST:PORT or - # an absolute path to a UNIX socket. When creating a UNIX - # socket to listen on, we always add a PID suffix to it - # when binding and then rename it into its intended name to - # atomically replace and start listening for new connections. - def unicorn_server_new(address = '0.0.0.0:8080', backlog = 1024) domain, bind_addr = if address[0..0] == "/" - [ AF_UNIX, pack_sockaddr_un("#{address}.#{$$}.tmp") ] + [ AF_UNIX, Socket.pack_sockaddr_un(address) ] elsif address =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)$/ - [ AF_INET, pack_sockaddr_in($2.to_i, $1) ] + [ AF_INET, Socket.pack_sockaddr_in($2.to_i, $1) ] + else + raise ArgumentError, "Don't know how to bind: #{address}" end - s = new(domain, SOCK_STREAM, 0) - s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) if defined?(SO_REUSEADDR) - s.bind(bind_addr) - s.listen(backlog) - s.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if defined?(Fcntl::FD_CLOEXEC) - # atomically replace existing domain socket - File.rename("#{address}.#{$$}.tmp", address) if domain == AF_UNIX - s + sock = Socket.new(domain, SOCK_STREAM, 0) + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) if defined?(SO_REUSEADDR) + begin + sock.bind(bind_addr) + rescue Errno::EADDRINUSE + sock.close rescue nil + return nil + end + sock.listen(backlog) + set_server_sockopt(sock) if domain == AF_INET + sock end - end + # Returns the configuration name of a socket as a string. sock may + # be a string value, in which case it is returned as-is + # Warning: TCP sockets may not always return the name given to it. + def sock_name(sock) + case sock + when String then sock + when UNIXServer + Socket.unpack_sockaddr_un(sock.getsockname) + when TCPServer + Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':') + when Socket + begin + Socket.unpack_sockaddr_in(sock.getsockname).reverse!.join(':') + rescue ArgumentError + Socket.unpack_sockaddr_un(sock.getsockname) + end + else + raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}" + end + end -end + # casts a given Socket to be a TCPServer or UNIXServer + def server_cast(sock) + begin + Socket.unpack_sockaddr_in(sock.getsockname) + TCPServer.for_fd(sock.fileno) + rescue ArgumentError + UNIXServer.for_fd(sock.fileno) + end + end + end # module SocketHelper +end # module Unicorn |