diff options
Diffstat (limited to 'lib/unicorn.rb')
-rw-r--r-- | lib/unicorn.rb | 120 |
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 |