about summary refs log tree commit homepage
path: root/lib/unicorn.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/unicorn.rb')
-rw-r--r--lib/unicorn.rb495
1 files changed, 276 insertions, 219 deletions
diff --git a/lib/unicorn.rb b/lib/unicorn.rb
index d442f63..4a4e2e1 100644
--- a/lib/unicorn.rb
+++ b/lib/unicorn.rb
@@ -1,6 +1,6 @@
-require 'logger'
+require 'fcntl'
 
-require 'unicorn/socket'
+require 'unicorn/socket_helper'
 require 'unicorn/const'
 require 'unicorn/http_request'
 require 'unicorn/http_response'
@@ -23,18 +23,32 @@ module Unicorn
   # forked worker children.
   class HttpServer
     attr_reader :logger
-    include Process
     include ::Unicorn::SocketHelper
 
-    DEFAULT_START_CTX = {
+    # prevents IO objects in here from being GC-ed
+    IO_PURGATORY = []
+
+    # all bound listener sockets
+    LISTENERS = []
+
+    # This hash maps PIDs to Workers
+    WORKERS = {}
+
+    # See: http://cr.yp.to/docs/selfpipe.html
+    SELF_PIPE = []
+
+    # signal queue used for self-piping
+    SIG_QUEUE = []
+
+    # We populate this at startup so we can figure out how to reexecute
+    # and upgrade the currently running instance of Unicorn
+    START_CTX = {
       :argv => ARGV.map { |arg| arg.dup },
       # don't rely on Dir.pwd here since it's not symlink-aware, and
       # symlink dirs are the default with Capistrano...
       :cwd => `/bin/sh -c pwd`.chomp("\n"),
       :zero => $0.dup,
-      :environ => {}.merge!(ENV),
-      :umask => File.umask,
-    }.freeze
+    }
 
     Worker = Struct.new(:nr, :tempfile) unless defined?(Worker)
     class Worker
@@ -46,22 +60,17 @@ module Unicorn
 
     # 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.workers.join to join the thread that's processing
+    # HttpServer.run.join to join the thread that's processing
     # incoming requests on the socket.
     def initialize(app, options = {})
-      start_ctx = options.delete(:start_ctx)
-      @start_ctx = DEFAULT_START_CTX.dup
-      @start_ctx.merge!(start_ctx) if start_ctx
       @app = app
-      @mode = :idle
-      @master_pid = $$
-      @workers = Hash.new
-      @io_purgatory = [] # prevents IO objects in here from being GC-ed
-      @request = @rd_sig = @wr_sig = nil
+      @pid = nil
       @reexec_pid = 0
+      @init_listeners = options[:listeners] ? options[:listeners].dup : []
       @config = Configurator.new(options.merge(:use_defaults => true))
+      @listener_opts = {}
       @config.commit!(self, :skip => [:listeners, :pid])
-      @listeners = []
+      @request = HttpRequest.new(@logger)
     end
 
     # Runs the thing.  Returns self so you can run join on it
@@ -72,44 +81,55 @@ module Unicorn
       # before they become UNIXServer or TCPServer
       inherited = ENV['UNICORN_FD'].to_s.split(/,/).map do |fd|
         io = Socket.for_fd(fd.to_i)
-        set_server_sockopt(io)
-        @io_purgatory << io
-        logger.info "inherited: #{io} fd=#{fd} addr=#{sock_name(io)}"
+        set_server_sockopt(io, @listener_opts[sock_name(io)])
+        IO_PURGATORY << io
+        logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
         server_cast(io)
       end
 
       config_listeners = @config[:listeners].dup
-      @listeners.replace(inherited)
+      LISTENERS.replace(inherited)
 
       # we start out with generic Socket objects that get cast to either
       # TCPServer or UNIXServer objects; but since the Socket objects
       # share the same OS-level file descriptor as the higher-level *Server
       # objects; we need to prevent Socket objects from being garbage-collected
       config_listeners -= listener_names
+      if config_listeners.empty? && LISTENERS.empty?
+        config_listeners << Unicorn::Const::DEFAULT_LISTEN
+      end
       config_listeners.each { |addr| listen(addr) }
-      raise ArgumentError, "no listeners" if @listeners.empty?
+      raise ArgumentError, "no listeners" if LISTENERS.empty?
       self.pid = @config[:pid]
       build_app! if @preload_app
-      $stderr.reopen(File.open(@stderr_path, "a")) if @stderr_path
-      $stdout.reopen(File.open(@stdout_path, "a")) if @stdout_path
-      $stderr.sync = $stdout.sync = true
-      spawn_missing_workers
+      maintain_worker_count
       self
     end
 
     # replaces current listener set with +listeners+.  This will
     # close the socket if it will not exist in the new listener set
     def listeners=(listeners)
-      cur_names = listener_names
+      cur_names, dead_names = [], []
+      listener_names.each do |name|
+        if "/" == name[0..0]
+          # mark unlinked sockets as dead so we can rebind them
+          (File.socket?(name) ? cur_names : dead_names) << name
+        else
+          cur_names << name
+        end
+      end
       set_names = listener_names(listeners)
-      dead_names = cur_names - set_names
+      dead_names += cur_names - set_names
+      dead_names.uniq!
 
-      @listeners.delete_if do |io|
+      LISTENERS.delete_if do |io|
         if dead_names.include?(sock_name(io))
-          @io_purgatory.delete_if { |pio| pio.fileno == io.fileno }
-          destroy_safely(io)
-          true
+          IO_PURGATORY.delete_if do |pio|
+            pio.fileno == io.fileno && (pio.close rescue nil).nil? # true
+          end
+          (io.close rescue nil).nil? # true
         else
+          set_server_sockopt(io, @listener_opts[sock_name(io)])
           false
         end
       end
@@ -117,6 +137,9 @@ module Unicorn
       (set_names - cur_names).each { |addr| listen(addr) }
     end
 
+    def stdout_path=(path); redirect_io($stdout, path); end
+    def stderr_path=(path); redirect_io($stderr, path); end
+
     # sets the path for the PID file of the master process
     def pid=(path)
       if path
@@ -125,26 +148,25 @@ module Unicorn
           raise ArgumentError, "Already running on PID:#{x} " \
                                "(or pid=#{path} is stale)"
         end
-        File.open(path, 'wb') { |fp| fp.syswrite("#{$$}\n") }
       end
-      unlink_pid_safe(@pid) if @pid && @pid != path
+      unlink_pid_safe(@pid) if @pid
+      File.open(path, 'wb') { |fp| fp.syswrite("#$$\n") } if path
       @pid = path
     end
 
     # add a given address to the +listeners+ set, idempotently
     # Allows workers to add a private, per-process listener via the
     # @after_fork hook.  Very useful for debugging and testing.
-    def listen(address)
+    def listen(address, opt = {}.merge(@listener_opts[address] || {}))
       return if String === address && listener_names.include?(address)
 
-      if io = bind_listen(address, @backlog)
-        if Socket == io.class
-          @io_purgatory << io
+      if io = bind_listen(address, opt)
+        unless TCPServer === io || UNIXServer === io
+          IO_PURGATORY << io
           io = server_cast(io)
         end
-        logger.info "#{io} listening on PID:#{$$} " \
-                    "fd=#{io.fileno} addr=#{sock_name(io)}"
-        @listeners << io
+        logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
+        LISTENERS << io
       else
         logger.error "adding listener failed addr=#{address} (in use)"
         raise Errno::EADDRINUSE, address
@@ -158,55 +180,55 @@ module Unicorn
     def join
       # this pipe is used to wake us up from select(2) in #join when signals
       # are trapped.  See trap_deferred
-      @rd_sig, @wr_sig = IO.pipe unless (@rd_sig && @wr_sig)
-      @rd_sig.nonblock = @wr_sig.nonblock = true
+      init_self_pipe!
+      respawn = true
 
-      reset_master
-      $0 = "unicorn master"
-      logger.info "master process ready" # test relies on this message
+      QUEUE_SIGS.each { |sig| trap_deferred(sig) }
+      trap(:CHLD) { |sig_nr| awaken_master }
+      proc_name 'master'
+      logger.info "master process ready" # test_exec.rb relies on this message
       begin
         loop do
           reap_all_workers
-          case @mode
-          when :idle
+          case SIG_QUEUE.shift
+          when nil
             murder_lazy_workers
-            spawn_missing_workers
-          when 'QUIT' # graceful shutdown
+            maintain_worker_count if respawn
+            master_sleep
+          when :QUIT # graceful shutdown
             break
-          when 'TERM', 'INT' # immediate shutdown
+          when :TERM, :INT # immediate shutdown
             stop(false)
             break
-          when 'USR1' # user-defined (probably something like log reopening)
-            kill_each_worker('USR1')
+          when :USR1 # rotate logs
+            logger.info "master reopening logs..."
             Unicorn::Util.reopen_logs
-            reset_master
-          when 'USR2' # exec binary, stay alive in case something went wrong
+            logger.info "master done reopening logs"
+            kill_each_worker(:USR1)
+          when :USR2 # exec binary, stay alive in case something went wrong
             reexec
-            reset_master
-          when 'HUP'
+          when :WINCH
+            if Process.ppid == 1 || Process.getpgrp != $$
+              respawn = false
+              logger.info "gracefully stopping all workers"
+              kill_each_worker(:QUIT)
+            else
+              logger.info "SIGWINCH ignored because we're not daemonized"
+            end
+          when :TTIN
+            @worker_processes += 1
+          when :TTOU
+            @worker_processes -= 1 if @worker_processes > 0
+          when :HUP
+            respawn = true
             if @config.config_file
               load_config!
-              reset_master
               redo # immediate reaping since we may have QUIT workers
             else # exec binary and exit if there's no config file
               logger.info "config_file not present, reexecuting binary"
               reexec
               break
             end
-          else
-            logger.error "master process in unknown mode: #{@mode}, resetting"
-            reset_master
-          end
-          reap_all_workers
-
-          ready = begin
-            IO.select([@rd_sig], nil, nil, 1) or next
-          rescue Errno::EINTR # next
-          end
-          ready[0] && ready[0][0] or next
-          begin # just consume the pipe when we're awakened, @mode is set
-            loop { @rd_sig.sysread(Const::CHUNK_SIZE) }
-          rescue Errno::EAGAIN, Errno::EINTR # next
           end
         end
       rescue Errno::EINTR
@@ -214,25 +236,24 @@ module Unicorn
       rescue Object => e
         logger.error "Unhandled master loop exception #{e.inspect}."
         logger.error e.backtrace.join("\n")
-        reset_master
         retry
       end
       stop # gracefully shutdown all workers on our way out
-      logger.info "master PID:#{$$} join complete"
+      logger.info "master complete"
       unlink_pid_safe(@pid) if @pid
     end
 
     # Terminates all workers, but does not exit master process
     def stop(graceful = true)
-      kill_each_worker(graceful ? 'QUIT' : 'TERM')
+      kill_each_worker(graceful ? :QUIT : :TERM)
       timeleft = @timeout
       step = 0.2
       reap_all_workers
-      until @workers.empty?
+      until WORKERS.empty?
         sleep(step)
         reap_all_workers
         (timeleft -= step) > 0 and next
-        kill_each_worker('KILL')
+        kill_each_worker(:KILL)
       end
     ensure
       self.listeners = []
@@ -241,55 +262,64 @@ module Unicorn
     private
 
     # list of signals we care about and trap in master.
-    TRAP_SIGS = %w(QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.freeze
+    QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP,
+                   :TTIN, :TTOU ].freeze
 
     # defer a signal for later processing in #join (master process)
     def trap_deferred(signal)
       trap(signal) do |sig_nr|
-        # we only handle/defer one signal at a time and ignore all others
-        # until we're ready again.  Queueing signals can lead to more bugs,
-        # and simplicity is the most important thing
-        TRAP_SIGS.each { |sig| trap(sig, 'IGNORE') }
-        if Symbol === @mode
-          @mode = signal
-          begin
-            @wr_sig.syswrite('.') # wakeup master process from IO.select
-          rescue Errno::EAGAIN
-          rescue Errno::EINTR
-            retry
-          end
+        if SIG_QUEUE.size < 5
+          SIG_QUEUE << signal
+          awaken_master
+        else
+          logger.error "ignoring SIG#{signal}, queue=#{SIG_QUEUE.inspect}"
         end
       end
     end
 
+    # wait for a signal hander to wake us up and then consume the pipe
+    # Wake up every second anyways to run murder_lazy_workers
+    def master_sleep
+      begin
+        ready = IO.select([SELF_PIPE.first], nil, nil, 1) or return
+        ready.first && ready.first.first or return
+        loop { SELF_PIPE.first.read_nonblock(Const::CHUNK_SIZE) }
+      rescue Errno::EAGAIN, Errno::EINTR
+      end
+    end
 
-    def reset_master
-      @mode = :idle
-      TRAP_SIGS.each { |sig| trap_deferred(sig) }
+    def awaken_master
+      begin
+        SELF_PIPE.last.write_nonblock('.') # wakeup master process from select
+      rescue Errno::EAGAIN, Errno::EINTR
+        # pipe is full, master should wake up anyways
+        retry
+      end
     end
 
     # reaps all unreaped workers
     def reap_all_workers
       begin
         loop do
-          pid = waitpid(-1, WNOHANG) or break
+          pid, status = Process.waitpid2(-1, Process::WNOHANG)
+          pid or break
           if @reexec_pid == pid
-            logger.error "reaped exec()-ed PID:#{pid} status=#{$?.exitstatus}"
+            logger.error "reaped #{status.inspect} exec()-ed"
             @reexec_pid = 0
             self.pid = @pid.chomp('.oldbin') if @pid
+            proc_name 'master'
           else
-            worker = @workers.delete(pid)
+            worker = WORKERS.delete(pid)
             worker.tempfile.close rescue nil
-            logger.info "reaped PID:#{pid} " \
-                        "worker=#{worker.nr rescue 'unknown'} " \
-                        "status=#{$?.exitstatus}"
+            logger.info "reaped #{status.inspect} " \
+                        "worker=#{worker.nr rescue 'unknown'}"
           end
         end
       rescue Errno::ECHILD
       end
     end
 
-    # reexecutes the @start_ctx with a new binary
+    # reexecutes the START_CTX with a new binary
     def reexec
       if @reexec_pid > 0
         begin
@@ -317,19 +347,26 @@ module Unicorn
       end
 
       @reexec_pid = fork do
-        @rd_sig.close if @rd_sig
-        @wr_sig.close if @wr_sig
-        @workers.values.each { |other| other.tempfile.close rescue nil }
-
-        ENV.replace(@start_ctx[:environ])
-        ENV['UNICORN_FD'] = @listeners.map { |sock| sock.fileno }.join(',')
-        File.umask(@start_ctx[:umask])
-        Dir.chdir(@start_ctx[:cwd])
-        cmd = [ @start_ctx[:zero] ] + @start_ctx[:argv]
+        listener_fds = LISTENERS.map { |sock| sock.fileno }
+        ENV['UNICORN_FD'] = listener_fds.join(',')
+        Dir.chdir(START_CTX[:cwd])
+        cmd = [ START_CTX[:zero] ] + START_CTX[:argv]
+
+        # avoid leaking FDs we don't know about, but let before_exec
+        # unset FD_CLOEXEC, if anything else in the app eventually
+        # relies on FD inheritence.
+        (3..1024).each do |io|
+          next if listener_fds.include?(io)
+          io = IO.for_fd(io) rescue nil
+          io or next
+          IO_PURGATORY << io
+          io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
+        end
         logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
-        @before_exec.call(self) if @before_exec
+        @before_exec.call(self)
         exec(*cmd)
       end
+      proc_name 'master (old)'
     end
 
     # forcibly terminate all workers that haven't checked in in @timeout
@@ -339,48 +376,62 @@ module Unicorn
     # is stale for >@timeout seconds, then we'll kill the corresponding
     # worker.
     def murder_lazy_workers
-      now = Time.now
-      @workers.each_pair do |pid, worker|
-        (now - worker.tempfile.ctime) <= @timeout and next
+      WORKERS.each_pair do |pid, worker|
+        stat = worker.tempfile.stat
+        stat.mode == 0100000 and next
+        Time.now - stat.ctime <= @timeout and next
         logger.error "worker=#{worker.nr} PID:#{pid} is too old, killing"
-        kill_worker('KILL', pid) # take no prisoners for @timeout violations
+        kill_worker(:KILL, pid) # take no prisoners for @timeout violations
         worker.tempfile.close rescue nil
       end
     end
 
     def spawn_missing_workers
-      return if @workers.size == @worker_processes
       (0...@worker_processes).each do |worker_nr|
-        @workers.values.include?(worker_nr) and next
-        tempfile = Tempfile.new('') # as short as possible to save dir space
+        WORKERS.values.include?(worker_nr) and next
+        begin
+          Dir.chdir(START_CTX[:cwd])
+        rescue Errno::ENOENT => err
+          logger.fatal "#{err.inspect} (#{START_CTX[:cwd]})"
+          SIG_QUEUE << :QUIT # forcibly emulate SIGQUIT
+          return
+        end
+        tempfile = Tempfile.new(nil) # as short as possible to save dir space
         tempfile.unlink # don't allow other processes to find or see it
-        tempfile.sync = true
         worker = Worker.new(worker_nr, tempfile)
-        @before_fork.call(self, worker.nr)
+        @before_fork.call(self, worker)
         pid = fork { worker_loop(worker) }
-        @workers[pid] = worker
+        WORKERS[pid] = worker
       end
     end
 
+    def maintain_worker_count
+      (off = WORKERS.size - @worker_processes) == 0 and return
+      off < 0 and return spawn_missing_workers
+      WORKERS.each_pair { |pid,w|
+        w.nr >= @worker_processes and kill_worker(:QUIT, pid) rescue nil
+      }
+    end
+
     # once a client is accepted, it is processed in its entirety here
     # in 3 easy steps: read request, call app, write app response
     def process_client(client)
-      env = @request.read(client) or return
-      app_response = @app.call(env)
-      HttpResponse.write(client, app_response)
+      HttpResponse.write(client, @app.call(@request.read(client)))
+    # if we get any error, try to write something back to the client
+    # assuming we haven't closed the socket, but don't get hung up
+    # if the socket is already closed or broken.  We'll always ensure
+    # the socket is closed at the end of this function
     rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
-      client.closed? or client.close rescue nil
+      client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
+      client.close rescue nil
+    rescue HttpParserError # try to tell the client they're bad
+      client.write_nonblock(Const::ERROR_400_RESPONSE) rescue nil
+      client.close rescue nil
     rescue Object => e
+      client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
+      client.close rescue nil
       logger.error "Read error: #{e.inspect}"
       logger.error e.backtrace.join("\n")
-    ensure
-      begin
-        client.closed? or client.close
-      rescue Object => e
-        logger.error "Client error: #{e.inspect}"
-        logger.error e.backtrace.join("\n")
-      end
-      @request.reset
     end
 
     # gets rid of stuff the worker has no business keeping track of
@@ -388,119 +439,107 @@ module Unicorn
     # traps for USR1, USR2, and HUP may be set in the @after_fork Proc
     # by the user.
     def init_worker_process(worker)
-      build_app! unless @preload_app
-      TRAP_SIGS.each { |sig| trap(sig, 'IGNORE') }
-      trap('CHLD', 'DEFAULT')
-      trap('USR1') do
-        @logger.info "worker=#{worker.nr} rotating logs..."
-        Unicorn::Util.reopen_logs
-        @logger.info "worker=#{worker.nr} done rotating logs"
-      end
+      QUEUE_SIGS.each { |sig| trap(sig, 'IGNORE') }
+      trap(:CHLD, 'DEFAULT')
+      SIG_QUEUE.clear
+      proc_name "worker[#{worker.nr}]"
+      START_CTX.clear
+      init_self_pipe!
+      WORKERS.values.each { |other| other.tempfile.close! rescue nil }
+      WORKERS.clear
+      LISTENERS.each { |sock| sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
+      worker.tempfile.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
+      @after_fork.call(self, worker) # can drop perms
+      @timeout /= 2.0 # halve it for select()
+      build_app! unless @config[:preload_app]
+    end
 
-      $0 = "unicorn worker[#{worker.nr}]"
-      @rd_sig.close if @rd_sig
-      @wr_sig.close if @wr_sig
-      @workers.values.each { |other| other.tempfile.close rescue nil }
-      @workers.clear
-      @start_ctx.clear
-      @mode = @start_ctx = @workers = @rd_sig = @wr_sig = nil
-      @listeners.each { |sock| set_cloexec(sock) }
-      ENV.delete('UNICORN_FD')
-      @after_fork.call(self, worker.nr) if @after_fork
-      @request = HttpRequest.new(logger)
+    def reopen_worker_logs(worker_nr)
+      @logger.info "worker=#{worker_nr} reopening logs..."
+      Unicorn::Util.reopen_logs
+      @logger.info "worker=#{worker_nr} done reopening logs"
+      init_self_pipe!
     end
 
     # runs inside each forked worker, this sits around and waits
     # for connections and doesn't die until the parent dies (or is
     # given a INT, QUIT, or TERM signal)
     def worker_loop(worker)
+      master_pid = Process.ppid # slightly racy, but less memory usage
       init_worker_process(worker)
-      nr = 0
-      tempfile = worker.tempfile
-      alive = true
-      ready = @listeners
-      client = nil
-      %w(TERM INT).each { |sig| trap(sig) { exit(0) } } # instant shutdown
-      trap('QUIT') do
-        alive = false
-        @listeners.each { |sock| sock.close rescue nil } # break IO.select
-      end
+      nr = 0 # this becomes negative if we need to reopen logs
+      alive = worker.tempfile # tempfile is our lifeline to the master process
+      ready = LISTENERS
+      t = ti = 0
+
+      # closing anything we IO.select on will raise EBADF
+      trap(:USR1) { nr = -65536; SELF_PIPE.first.close rescue nil }
+      trap(:QUIT) { alive = nil; LISTENERS.each { |s| s.close rescue nil } }
+      [:TERM, :INT].each { |sig| trap(sig) { exit!(0) } } # instant shutdown
+      @logger.info "worker=#{worker.nr} ready"
 
-      while alive && @master_pid == ppid
-        # we're a goner in @timeout seconds anyways if tempfile.chmod
+      begin
+        nr < 0 and reopen_worker_logs(worker.nr)
+        nr = 0
+
+        # we're a goner in @timeout seconds anyways if alive.chmod
         # breaks, so don't trap the exception.  Using fchmod() since
         # futimes() is not available in base Ruby and I very strongly
         # prefer temporary files to be unlinked for security,
         # performance and reliability reasons, so utime is out.  No-op
         # changes with chmod doesn't update ctime on all filesystems; so
-        # we increment our counter each and every time.
-        tempfile.chmod(nr += 1)
+        # we change our counter each and every time (after process_client
+        # and before IO.select).
+        t == (ti = Time.now.to_i) or alive.chmod(t = ti)
 
-        begin
-          accepted = false
-          ready.each do |sock|
-            begin
-              client = begin
-                sock.accept_nonblock
-              rescue Errno::EAGAIN
-                next
-              end
-              accepted = client.sync = true
-              client.nonblock = false
-              set_client_sockopt(client) if TCPSocket === client
-              process_client(client)
-            rescue Errno::ECONNABORTED
-              # client closed the socket even before accept
-              if client && !client.closed?
-                client.close rescue nil
-              end
-            end
-            tempfile.chmod(nr += 1)
-          end
-          client = nil
-
-          # make the following bet: if we accepted clients this round,
-          # we're probably reasonably busy, so avoid calling select(2)
-          # and try to do a blind non-blocking accept(2) on everything
-          # before we sleep again in select
-          if accepted
-            ready = @listeners
-          else
-            begin
-              tempfile.chmod(nr += 1)
-              # timeout used so we can detect parent death:
-              ret = IO.select(@listeners, nil, nil, @timeout/2.0) or next
-              ready = ret[0]
-            rescue Errno::EINTR
-              ready = @listeners
-            rescue Errno::EBADF => e
-              exit(alive ? 1 : 0)
-            end
-          end
-        rescue SystemExit => e
-          exit(e.status)
-        rescue Object => e
-          if alive
-            logger.error "Unhandled listen loop exception #{e.inspect}."
-            logger.error e.backtrace.join("\n")
+        ready.each do |sock|
+          begin
+            process_client(sock.accept_nonblock)
+            nr += 1
+            t == (ti = Time.now.to_i) or alive.chmod(t = ti)
+          rescue Errno::EAGAIN, Errno::ECONNABORTED
           end
+          break if nr < 0
         end
-      end
+
+        # make the following bet: if we accepted clients this round,
+        # we're probably reasonably busy, so avoid calling select()
+        # and do a speculative accept_nonblock on every listener
+        # before we sleep again in select().
+        redo unless nr == 0 # (nr < 0) => reopen logs
+
+        master_pid == Process.ppid or return
+        alive.chmod(t = 0)
+        begin
+          # timeout used so we can detect parent death:
+          ret = IO.select(LISTENERS, nil, SELF_PIPE, @timeout) or redo
+          ready = ret.first
+        rescue Errno::EINTR
+          ready = LISTENERS
+        rescue Errno::EBADF
+          nr < 0 or return
+        end
+      rescue Object => e
+        if alive
+          logger.error "Unhandled listen loop exception #{e.inspect}."
+          logger.error e.backtrace.join("\n")
+        end
+      end while alive
     end
 
     # delivers a signal to a worker and fails gracefully if the worker
     # is no longer running.
     def kill_worker(signal, pid)
       begin
-        kill(signal, pid)
+        Process.kill(signal, pid)
       rescue Errno::ESRCH
-        worker = @workers.delete(pid) and worker.tempfile.close rescue nil
+        worker = WORKERS.delete(pid) and worker.tempfile.close rescue nil
       end
     end
 
     # delivers a signal to each worker
     def kill_each_worker(signal)
-      @workers.keys.each { |pid| kill_worker(signal, pid) }
+      WORKERS.keys.each { |pid| kill_worker(signal, pid) }
     end
 
     # unlinks a PID file at given +path+ if it contains the current PID
@@ -514,7 +553,7 @@ module Unicorn
     def valid_pid?(path)
       if File.exist?(path) && (pid = File.read(path).to_i) > 1
         begin
-          kill(0, pid)
+          Process.kill(0, pid)
           return pid
         rescue Errno::ESRCH
         end
@@ -525,9 +564,11 @@ module Unicorn
     def load_config!
       begin
         logger.info "reloading config_file=#{@config.config_file}"
+        @config[:listeners].replace(@init_listeners)
         @config.reload
         @config.commit!(self)
-        kill_each_worker('QUIT')
+        kill_each_worker(:QUIT)
+        Unicorn::Util.reopen_logs
         logger.info "done reloading config_file=#{@config.config_file}"
       rescue Object => e
         logger.error "error reloading config_file=#{@config.config_file}: " \
@@ -536,7 +577,7 @@ module Unicorn
     end
 
     # returns an array of string names for the given listener array
-    def listener_names(listeners = @listeners)
+    def listener_names(listeners = LISTENERS)
       listeners.map { |io| sock_name(io) }
     end
 
@@ -544,5 +585,21 @@ module Unicorn
       @app = @app.call if @app.respond_to?(:arity) && @app.arity == 0
     end
 
+    def proc_name(tag)
+      $0 = ([ File.basename(START_CTX[:zero]), tag ] +
+              START_CTX[:argv]).join(' ')
+    end
+
+    def redirect_io(io, path)
+      File.open(path, 'a') { |fp| io.reopen(fp) } if path
+      io.sync = true
+    end
+
+    def init_self_pipe!
+      SELF_PIPE.each { |io| io.close rescue nil }
+      SELF_PIPE.replace(IO.pipe)
+      SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
+    end
+
   end
 end