diff options
-rwxr-xr-x | GIT-VERSION-GEN | 2 | ||||
-rw-r--r-- | GNUmakefile | 2 | ||||
-rwxr-xr-x | bin/unicorn | 22 | ||||
-rwxr-xr-x | bin/unicorn_rails | 26 | ||||
-rw-r--r-- | lib/unicorn.rb | 200 | ||||
-rw-r--r-- | lib/unicorn/configurator.rb | 27 | ||||
-rw-r--r-- | lib/unicorn/const.rb | 5 | ||||
-rw-r--r-- | lib/unicorn/launcher.rb | 7 | ||||
-rwxr-xr-x | t/t0003-working_directory.sh | 5 | ||||
-rwxr-xr-x | t/t0010-reap-logging.sh | 55 | ||||
-rwxr-xr-x | t/t0303-rails3-alt-working_directory_config.ru.sh | 5 | ||||
-rw-r--r-- | test/exec/test_exec.rb | 2 |
12 files changed, 188 insertions, 170 deletions
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 755e132..88b943a 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.1.4.GIT +DEF_VER=v1.0.0.GIT LF=' ' diff --git a/GNUmakefile b/GNUmakefile index 3354ff1..b5fe9fd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -169,7 +169,7 @@ NEWS: GIT-VERSION-FILE .manifest $(RAKE) -s news_rdoc > $@+ mv $@+ $@ -SINCE = 1.0.0 +SINCE = 0.991.0 ChangeLog: LOG_VERSION = \ $(shell git rev-parse -q "$(GIT_VERSION)" >/dev/null 2>&1 && \ echo $(GIT_VERSION) || git describe) diff --git a/bin/unicorn b/bin/unicorn index 8d984bd..73bff3a 100755 --- a/bin/unicorn +++ b/bin/unicorn @@ -4,16 +4,13 @@ require 'unicorn/launcher' require 'optparse' ENV["RACK_ENV"] ||= "development" -daemonize = false -options = { :listeners => [] } -host, port = Unicorn::Const::DEFAULT_HOST, Unicorn::Const::DEFAULT_PORT -set_listener = false +rackup_opts = Unicorn::Configurator::RACKUP +options = rackup_opts[:options] opts = OptionParser.new("", 24, ' ') do |opts| cmd = File.basename($0) opts.banner = "Usage: #{cmd} " \ "[ruby options] [#{cmd} options] [rackup config file]" - opts.separator "Ruby options:" lineno = 1 @@ -46,14 +43,14 @@ opts = OptionParser.new("", 24, ' ') do |opts| opts.on("-o", "--host HOST", "listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h| - host = h - set_listener = true + rackup_opts[:host] = h + rackup_opts[:set_listener] = true end opts.on("-p", "--port PORT", "use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |p| - port = p.to_i - set_listener = true + rackup_opts[:port] = p.to_i + rackup_opts[:set_listener] = true end opts.on("-E", "--env ENVIRONMENT", @@ -62,7 +59,7 @@ opts = OptionParser.new("", 24, ' ') do |opts| end opts.on("-D", "--daemonize", "run daemonized in the background") do |d| - daemonize = d ? true : false + rackup_opts[:daemonize] = !!d end opts.on("-P", "--pid FILE", "DEPRECATED") do |f| @@ -109,16 +106,15 @@ opts = OptionParser.new("", 24, ' ') do |opts| end app = Unicorn.builder(ARGV[0] || 'config.ru', opts) -options[:listeners] << "#{host}:#{port}" if set_listener if $DEBUG require 'pp' pp({ :unicorn_options => options, :app => app, - :daemonize => daemonize, + :daemonize => rackup_opts[:daemonize], }) end -Unicorn::Launcher.daemonize!(options) if daemonize +Unicorn::Launcher.daemonize!(options) if rackup_opts[:daemonize] Unicorn.run(app, options) diff --git a/bin/unicorn_rails b/bin/unicorn_rails index 0b2d92f..0294b59 100755 --- a/bin/unicorn_rails +++ b/bin/unicorn_rails @@ -4,11 +4,9 @@ require 'unicorn/launcher' require 'optparse' require 'fileutils' -daemonize = false -options = { :listeners => [] } -host, port = Unicorn::Const::DEFAULT_HOST, Unicorn::Const::DEFAULT_PORT -set_listener = false ENV['RAILS_ENV'] ||= "development" +rackup_opts = Unicorn::Configurator::RACKUP +options = rackup_opts[:options] opts = OptionParser.new("", 24, ' ') do |opts| cmd = File.basename($0) @@ -46,13 +44,14 @@ opts = OptionParser.new("", 24, ' ') do |opts| opts.on("-o", "--host HOST", "listen on HOST (default: #{Unicorn::Const::DEFAULT_HOST})") do |h| - host = h - set_listener = true + rackup_opts[:host] = h + rackup_opts[:set_listener] = true end - opts.on("-p", "--port PORT", "use PORT (default: #{port})") do |p| - port = p.to_i - set_listener = true + opts.on("-p", "--port PORT", + "use PORT (default: #{Unicorn::Const::DEFAULT_PORT})") do |p| + rackup_opts[:port] = p.to_i + rackup_opts[:set_listener] = true end opts.on("-E", "--env RAILS_ENV", @@ -61,7 +60,7 @@ opts = OptionParser.new("", 24, ' ') do |opts| end opts.on("-D", "--daemonize", "run daemonized in the background") do |d| - daemonize = d ? true : false + rackup_opts[:daemonize] = !!d end # Unicorn-specific stuff @@ -186,15 +185,14 @@ def rails_builder(ru, opts, daemonize) end end -app = rails_builder(ARGV[0], opts, daemonize) -options[:listeners] << "#{host}:#{port}" if set_listener +app = rails_builder(ARGV[0], opts, rackup_opts[:daemonize]) if $DEBUG require 'pp' pp({ :unicorn_options => options, :app => app, - :daemonize => daemonize, + :daemonize => rackup_opts[:daemonize], }) end @@ -203,7 +201,7 @@ options[:after_reload] = lambda do FileUtils.mkdir_p(%w(cache pids sessions sockets).map! { |d| "tmp/#{d}" }) end -if daemonize +if rackup_opts[:daemonize] options[:pid] = "tmp/pids/unicorn.pid" Unicorn::Launcher.daemonize!(options) end diff --git a/lib/unicorn.rb b/lib/unicorn.rb index 8f490bb..7f91352 100644 --- a/lib/unicorn.rb +++ b/lib/unicorn.rb @@ -392,65 +392,59 @@ module Unicorn self.ready_pipe = nil end begin - loop do - reap_all_workers - case SIG_QUEUE.shift - when nil - # avoid murdering workers after our master process (or the - # machine) comes out of suspend/hibernation - if (last_check + timeout) >= (last_check = Time.now) - murder_lazy_workers - else - # wait for workers to wakeup on suspend - master_sleep(timeout/2.0 + 1) - end - maintain_worker_count if respawn - master_sleep(1) - when :QUIT # graceful shutdown - break - when :TERM, :INT # immediate shutdown - stop(false) - break - when :USR1 # rotate logs - logger.info "master reopening logs..." - Unicorn::Util.reopen_logs - logger.info "master done reopening logs" - kill_each_worker(:USR1) - when :USR2 # exec binary, stay alive in case something went wrong + reap_all_workers + case SIG_QUEUE.shift + when nil + # avoid murdering workers after our master process (or the + # machine) comes out of suspend/hibernation + if (last_check + timeout) >= (last_check = Time.now) + murder_lazy_workers + else + # wait for workers to wakeup on suspend + master_sleep(timeout/2.0 + 1) + end + maintain_worker_count if respawn + master_sleep(1) + when :QUIT # graceful shutdown + break + when :TERM, :INT # immediate shutdown + stop(false) + break + when :USR1 # rotate logs + logger.info "master reopening logs..." + Unicorn::Util.reopen_logs + logger.info "master done reopening logs" + kill_each_worker(:USR1) + when :USR2 # exec binary, stay alive in case something went wrong + reexec + when :WINCH + if Process.ppid == 1 || Process.getpgrp != $$ + respawn = false + logger.info "gracefully stopping all workers" + kill_each_worker(:QUIT) + self.worker_processes = 0 + else + logger.info "SIGWINCH ignored because we're not daemonized" + end + when :TTIN + respawn = true + self.worker_processes += 1 + when :TTOU + self.worker_processes -= 1 if self.worker_processes > 0 + when :HUP + respawn = true + if config.config_file + load_config! + else # exec binary and exit if there's no config file + logger.info "config_file not present, reexecuting binary" reexec - when :WINCH - if Process.ppid == 1 || Process.getpgrp != $$ - respawn = false - logger.info "gracefully stopping all workers" - kill_each_worker(:QUIT) - self.worker_processes = 0 - else - logger.info "SIGWINCH ignored because we're not daemonized" - end - when :TTIN - respawn = true - self.worker_processes += 1 - when :TTOU - self.worker_processes -= 1 if self.worker_processes > 0 - when :HUP - respawn = true - if config.config_file - load_config! - 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 end end rescue Errno::EINTR - retry rescue => e logger.error "Unhandled master loop exception #{e.inspect}." logger.error e.backtrace.join("\n") - retry - end + end while true stop # gracefully shutdown all workers on our way out logger.info "master complete" unlink_pid_safe(pid) if pid @@ -489,42 +483,34 @@ module Unicorn # 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(sec) - begin - IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return - SELF_PIPE[0].read_nonblock(Const::CHUNK_SIZE, HttpRequest::BUF) + IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return + SELF_PIPE[0].read_nonblock(Const::CHUNK_SIZE, HttpRequest::BUF) rescue Errno::EAGAIN, Errno::EINTR - break - end while true end def awaken_master - begin - SELF_PIPE[1].write_nonblock('.') # wakeup master process from select + SELF_PIPE[1].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 - wpid, status = Process.waitpid2(-1, Process::WNOHANG) - wpid or break - if reexec_pid == wpid - logger.error "reaped #{status.inspect} exec()-ed" - self.reexec_pid = 0 - self.pid = pid.chomp('.oldbin') if pid - proc_name 'master' - else - worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil - logger.info "reaped #{status.inspect} " \ - "worker=#{worker.nr rescue 'unknown'}" - end + wpid, status = Process.waitpid2(-1, Process::WNOHANG) + wpid or return + if reexec_pid == wpid + logger.error "reaped #{status.inspect} exec()-ed" + self.reexec_pid = 0 + self.pid = pid.chomp('.oldbin') if pid + proc_name 'master' + else + worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil + m = "reaped #{status.inspect} worker=#{worker.nr rescue 'unknown'}" + status.success? ? logger.info(m) : logger.error(m) end rescue Errno::ECHILD - end + break + end while true end # reexecutes the START_CTX with a new binary @@ -565,8 +551,7 @@ module Unicorn # 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 = IO.for_fd(io) rescue next IO_PURGATORY << io io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) end @@ -730,15 +715,13 @@ module Unicorn ppid == Process.ppid or return alive.chmod(m = 0 == m ? 1 : 0) - begin - # timeout used so we can detect parent death: - ret = IO.select(LISTENERS, nil, SELF_PIPE, timeout) or redo - ready = ret[0] - rescue Errno::EINTR - ready = LISTENERS - rescue Errno::EBADF - nr < 0 or return - end + + # timeout used so we can detect parent death: + ret = IO.select(LISTENERS, nil, SELF_PIPE, timeout) and ready = ret[0] + rescue Errno::EINTR + ready = LISTENERS + rescue Errno::EBADF + nr < 0 or return rescue => e if alive logger.error "Unhandled listen loop exception #{e.inspect}." @@ -750,11 +733,9 @@ module Unicorn # delivers a signal to a worker and fails gracefully if the worker # is no longer running. def kill_worker(signal, wpid) - begin - Process.kill(signal, wpid) + Process.kill(signal, wpid) rescue Errno::ESRCH worker = WORKERS.delete(wpid) and worker.tmp.close rescue nil - end end # delivers a signal to each worker @@ -774,33 +755,28 @@ module Unicorn # nil otherwise. def valid_pid?(path) wpid = File.read(path).to_i - wpid <= 0 and return nil - begin - Process.kill(0, wpid) - wpid - rescue Errno::ESRCH + wpid <= 0 and return + Process.kill(0, wpid) + wpid + rescue Errno::ESRCH, Errno::ENOENT # don't unlink stale pid files, racy without non-portable locking... - end - rescue Errno::ENOENT end def load_config! loaded_app = app - begin - logger.info "reloading config_file=#{config.config_file}" - config[:listeners].replace(init_listeners) - config.reload - config.commit!(self) - kill_each_worker(:QUIT) - Unicorn::Util.reopen_logs - self.app = orig_app - build_app! if preload_app - logger.info "done reloading config_file=#{config.config_file}" - rescue StandardError, LoadError, SyntaxError => e - logger.error "error reloading config_file=#{config.config_file}: " \ - "#{e.class} #{e.message} #{e.backtrace}" - self.app = loaded_app - end + logger.info "reloading config_file=#{config.config_file}" + config[:listeners].replace(init_listeners) + config.reload + config.commit!(self) + kill_each_worker(:QUIT) + Unicorn::Util.reopen_logs + self.app = orig_app + build_app! if preload_app + logger.info "done reloading config_file=#{config.config_file}" + rescue StandardError, LoadError, SyntaxError => e + logger.error "error reloading config_file=#{config.config_file}: " \ + "#{e.class} #{e.message} #{e.backtrace}" + self.app = loaded_app end # returns an array of string names for the given listener array diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index 6be6fbd..ce886cf 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -9,13 +9,19 @@ require 'logger' # nginx is also available at # http://unicorn.bogomips.org/examples/nginx.conf class Unicorn::Configurator < Struct.new(:set, :config_file, :after_reload) + # :stopdoc: # used to stash stuff for deferred processing of cli options in # config.ru after "working_directory" is bound. Do not rely on # this being around later on... - RACKUP = {} # :nodoc: + RACKUP = { + :daemonize => false, + :host => Unicorn::Const::DEFAULT_HOST, + :port => Unicorn::Const::DEFAULT_PORT, + :set_listener => false, + :options => { :listeners => [] } + } # Default settings for Unicorn - # :stopdoc: DEFAULTS = { :timeout => 60, :logger => Logger.new($stderr), @@ -55,6 +61,9 @@ class Unicorn::Configurator < Struct.new(:set, :config_file, :after_reload) parse_rackup_file + RACKUP[:set_listener] and + set[:listeners] << "#{RACKUP[:host]}:#{RACKUP[:port]}" + # unicorn_rails creates dirs here after working_directory is bound after_reload.call if after_reload @@ -489,23 +498,15 @@ private /^#\\(.*)/ =~ File.read(ru) or return RACKUP[:optparse].parse!($1.split(/\s+/)) - # XXX ugly as hell, WILL FIX in 2.x (along with Rainbows!/Zbatery) - host, port, set_listener, options, daemonize = - eval("[ host, port, set_listener, options, daemonize ]", - TOPLEVEL_BINDING) - - # XXX duplicate code from bin/unicorn{,_rails} - set[:listeners] << "#{host}:#{port}" if set_listener - - if daemonize + if RACKUP[:daemonize] # unicorn_rails wants a default pid path, (not plain 'unicorn') if after_reload spid = set[:pid] pid('tmp/pids/unicorn.pid') if spid.nil? || spid == :unset end unless RACKUP[:daemonized] - Unicorn::Launcher.daemonize!(options) - RACKUP[:ready_pipe] = options.delete(:ready_pipe) + Unicorn::Launcher.daemonize!(RACKUP[:options]) + RACKUP[:ready_pipe] = RACKUP[:options].delete(:ready_pipe) end end end diff --git a/lib/unicorn/const.rb b/lib/unicorn/const.rb index a166780..6be5941 100644 --- a/lib/unicorn/const.rb +++ b/lib/unicorn/const.rb @@ -8,8 +8,9 @@ module Unicorn # Symbols did not really improve things much compared to constants. module Const - # The current version of Unicorn, currently 1.1.4 - UNICORN_VERSION="1.1.4" + # The current version of Unicorn, currently 2.0.0pre + # this constant is deprecated and will soon move to Unicorn::VERSION + UNICORN_VERSION="2.0.0pre" DEFAULT_HOST = "0.0.0.0" # default TCP listen host address DEFAULT_PORT = 8080 # default TCP listen port diff --git a/lib/unicorn/launcher.rb b/lib/unicorn/launcher.rb index 0d415dd..662b603 100644 --- a/lib/unicorn/launcher.rb +++ b/lib/unicorn/launcher.rb @@ -20,6 +20,7 @@ module Unicorn::Launcher # to pickup code changes if the original deployment directory # is a symlink or otherwise got replaced. def self.daemonize!(options) + cfg = Unicorn::Configurator $stdin.reopen("/dev/null") # We only start a new process group if we're not being reexecuted @@ -52,9 +53,9 @@ module Unicorn::Launcher end end # $stderr/$stderr can/will be redirected separately in the Unicorn config - Unicorn::Configurator::DEFAULTS[:stderr_path] ||= "/dev/null" - Unicorn::Configurator::DEFAULTS[:stdout_path] ||= "/dev/null" - Unicorn::Configurator::RACKUP[:daemonized] = true + cfg::DEFAULTS[:stderr_path] ||= "/dev/null" + cfg::DEFAULTS[:stdout_path] ||= "/dev/null" + cfg::RACKUP[:daemonized] = true end end diff --git a/t/t0003-working_directory.sh b/t/t0003-working_directory.sh index 53345ae..79988d8 100755 --- a/t/t0003-working_directory.sh +++ b/t/t0003-working_directory.sh @@ -1,9 +1,4 @@ #!/bin/sh -if test -n "$RBX_SKIP" -then - echo "$0 is broken under Rubinius for now" - exit 0 -fi . ./test-lib.sh t_plan 4 "config.ru inside alt working_directory" diff --git a/t/t0010-reap-logging.sh b/t/t0010-reap-logging.sh new file mode 100755 index 0000000..93d8c60 --- /dev/null +++ b/t/t0010-reap-logging.sh @@ -0,0 +1,55 @@ +#!/bin/sh +. ./test-lib.sh +t_plan 9 "reap worker logging messages" + +t_begin "setup and start" && { + unicorn_setup + cat >> $unicorn_config <<EOF +after_fork { |s,w| File.open('$fifo','w') { |f| f.write '.' } } +EOF + unicorn -c $unicorn_config pid.ru & + test '.' = $(cat $fifo) + unicorn_wait_start +} + +t_begin "kill 1st worker=0" && { + pid_1=$(curl http://$listen/) + kill -9 $pid_1 +} + +t_begin "wait for 2nd worker to start" && { + test '.' = $(cat $fifo) +} + +t_begin "ensure log of 1st reap is an ERROR" && { + dbgcat r_err + grep 'ERROR.*reaped.*worker=0' $r_err | grep $pid_1 + dbgcat r_err + > $r_err +} + +t_begin "kill 2nd worker gracefully" && { + pid_2=$(curl http://$listen/) + kill -QUIT $pid_2 +} + +t_begin "wait for 3rd worker=0 to start " && { + test '.' = $(cat $fifo) +} + +t_begin "ensure log of 2nd reap is a INFO" && { + grep 'INFO.*reaped.*worker=0' $r_err | grep $pid_2 + > $r_err +} + +t_begin "killing succeeds" && { + kill $unicorn_pid + wait + kill -0 $unicorn_pid && false +} + +t_begin "check stderr" && { + check_stderr +} + +t_done diff --git a/t/t0303-rails3-alt-working_directory_config.ru.sh b/t/t0303-rails3-alt-working_directory_config.ru.sh index 444f05a..1433f94 100755 --- a/t/t0303-rails3-alt-working_directory_config.ru.sh +++ b/t/t0303-rails3-alt-working_directory_config.ru.sh @@ -1,9 +1,4 @@ #!/bin/sh -if test -n "$RBX_SKIP" -then - echo "$0 is broken under Rubinius for now" - exit 0 -fi . ./test-rails3.sh t_plan 5 "Rails 3 (beta) inside alt working_directory (w/ config.ru)" diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb index 1d24ca3..581d5d5 100644 --- a/test/exec/test_exec.rb +++ b/test/exec/test_exec.rb @@ -614,7 +614,7 @@ EOF results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class assert_shutdown(pid) - end unless ENV['RBX_SKIP'] + end def test_config_ru_alt_path config_path = "#{@tmpdir}/foo.ru" |