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.rb120
1 files changed, 68 insertions, 52 deletions
diff --git a/lib/unicorn.rb b/lib/unicorn.rb
index d442f63..2f86de2 100644
--- a/lib/unicorn.rb
+++ b/lib/unicorn.rb
@@ -23,7 +23,6 @@ module Unicorn
   # forked worker children.
   class HttpServer
     attr_reader :logger
-    include Process
     include ::Unicorn::SocketHelper
 
     DEFAULT_START_CTX = {
@@ -53,7 +52,7 @@ module Unicorn
       @start_ctx = DEFAULT_START_CTX.dup
       @start_ctx.merge!(start_ctx) if start_ctx
       @app = app
-      @mode = :idle
+      @sig_queue = []
       @master_pid = $$
       @workers = Hash.new
       @io_purgatory = [] # prevents IO objects in here from being GC-ed
@@ -160,33 +159,45 @@ module Unicorn
       # are trapped.  See trap_deferred
       @rd_sig, @wr_sig = IO.pipe unless (@rd_sig && @wr_sig)
       @rd_sig.nonblock = @wr_sig.nonblock = true
+      mode = nil
+      respawn = true
 
-      reset_master
+      QUEUE_SIGS.each { |sig| trap_deferred(sig) }
+      trap('CHLD') { |sig_nr| awaken_master }
       $0 = "unicorn master"
-      logger.info "master process ready" # test relies on this message
+      logger.info "master process ready" # test_exec.rb relies on this message
       begin
         loop do
           reap_all_workers
-          case @mode
-          when :idle
+          case (mode = @sig_queue.shift)
+          when nil
             murder_lazy_workers
-            spawn_missing_workers
+            spawn_missing_workers if respawn
+            master_sleep
           when 'QUIT' # graceful shutdown
             break
           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 rotating logs..."
             Unicorn::Util.reopen_logs
-            reset_master
+            logger.info "master done rotating logs"
+            kill_each_worker('USR1')
           when 'USR2' # exec binary, stay alive in case something went wrong
             reexec
-            reset_master
+          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 '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"
@@ -194,19 +205,7 @@ module Unicorn
               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
+            logger.error "master process in unknown mode: #{mode}"
           end
         end
       rescue Errno::EINTR
@@ -214,7 +213,6 @@ 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
@@ -241,48 +239,57 @@ 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 =
+      %w(WINCH QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.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([@rd_sig], nil, nil, 1)
+        ready && ready[0] && ready[0][0] or return
+        loop { @rd_sig.sysread(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
+        @wr_sig.syswrite('.') # wakeup master process from IO.select
+      rescue Errno::EAGAIN # pipe is full, master should wake up anyways
+      rescue Errno::EINTR
+        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
+            $0 = "unicorn master"
           else
             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
@@ -330,6 +337,7 @@ module Unicorn
         @before_exec.call(self) if @before_exec
         exec(*cmd)
       end
+      $0 = "unicorn master (old)"
     end
 
     # forcibly terminate all workers that haven't checked in in @timeout
@@ -352,6 +360,13 @@ module Unicorn
       return if @workers.size == @worker_processes
       (0...@worker_processes).each do |worker_nr|
         @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('') # as short as possible to save dir space
         tempfile.unlink # don't allow other processes to find or see it
         tempfile.sync = true
@@ -389,7 +404,8 @@ module Unicorn
     # by the user.
     def init_worker_process(worker)
       build_app! unless @preload_app
-      TRAP_SIGS.each { |sig| trap(sig, 'IGNORE') }
+      @sig_queue.clear
+      QUEUE_SIGS.each { |sig| trap(sig, 'IGNORE') }
       trap('CHLD', 'DEFAULT')
       trap('USR1') do
         @logger.info "worker=#{worker.nr} rotating logs..."
@@ -403,7 +419,7 @@ module Unicorn
       @workers.values.each { |other| other.tempfile.close rescue nil }
       @workers.clear
       @start_ctx.clear
-      @mode = @start_ctx = @workers = @rd_sig = @wr_sig = nil
+      @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
@@ -426,7 +442,7 @@ module Unicorn
         @listeners.each { |sock| sock.close rescue nil } # break IO.select
       end
 
-      while alive && @master_pid == ppid
+      while alive && @master_pid == Process.ppid
         # we're a goner in @timeout seconds anyways if tempfile.chmod
         # breaks, so don't trap the exception.  Using fchmod() since
         # futimes() is not available in base Ruby and I very strongly
@@ -492,7 +508,7 @@ module Unicorn
     # 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
       end
@@ -514,7 +530,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