diff options
Diffstat (limited to 'test/exec/test_exec.rb')
-rw-r--r-- | test/exec/test_exec.rb | 370 |
1 files changed, 237 insertions, 133 deletions
diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb index 712037c..014b270 100644 --- a/test/exec/test_exec.rb +++ b/test/exec/test_exec.rb @@ -1,43 +1,34 @@ # Copyright (c) 2009 Eric Wong -STDIN.sync = STDOUT.sync = STDERR.sync = true require 'test/test_helper' -require 'pathname' -require 'tempfile' -require 'fileutils' do_test = true -DEFAULT_TRIES = 1000 -DEFAULT_RES = 0.2 - $unicorn_bin = ENV['UNICORN_TEST_BIN'] || "unicorn" redirect_test_io do do_test = system($unicorn_bin, '-v') end unless do_test - STDERR.puts "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \ - "skipping this test" + warn "#{$unicorn_bin} not found in PATH=#{ENV['PATH']}, " \ + "skipping this test" end -begin - require 'rack' -rescue LoadError - STDERR.puts "Unable to load Rack, skipping this test" +unless try_require('rack') + warn "Unable to load Rack, skipping this test" do_test = false end class ExecTest < Test::Unit::TestCase - trap('QUIT', 'IGNORE') + trap(:QUIT, 'IGNORE') HI = <<-EOS use Rack::ContentLength -run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] } +run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] } EOS HELLO = <<-EOS class Hello def call(env) - [ 200, { 'Content-Type' => 'text/plain' }, "HI\\n" ] + [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] end end EOS @@ -47,10 +38,9 @@ end HEAVY_CFG = <<-EOS worker_processes 4 timeout 30 -backlog 128 logger Logger.new('#{COMMON_TMP.path}') -before_fork do |server, worker_nr| - server.logger.info "before_fork: worker=\#{worker_nr}" +before_fork do |server, worker| + server.logger.info "before_fork: worker=\#{worker.nr}" end EOS @@ -82,6 +72,22 @@ end end end + def test_exit_signals + %w(INT TERM QUIT).each do |sig| + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = xfork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } } + wait_master_ready("test_stderr.#{pid}.log") + status = nil + assert_nothing_raised do + Process.kill(sig, pid) + pid, status = Process.waitpid2(pid) + end + reaped = File.readlines("test_stderr.#{pid}.log").grep(/reaped/) + assert_equal 1, reaped.size + assert status.exited? + end + end + def test_basic File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } pid = fork do @@ -92,6 +98,28 @@ end assert_shutdown(pid) end + def test_ttin_ttou + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } } + log = "test_stderr.#{pid}.log" + wait_master_ready(log) + [ 2, 3].each { |i| + assert_nothing_raised { Process.kill(:TTIN, pid) } + wait_workers_ready(log, i) + } + File.truncate(log, 0) + reaped = nil + [ 2, 1, 0].each { |i| + assert_nothing_raised { Process.kill(:TTOU, pid) } + DEFAULT_TRIES.times { + sleep DEFAULT_RES + reaped = File.readlines(log).grep(/reaped.*\s*worker=#{i}$/) + break if reaped.size == 1 + } + assert_equal 1, reaped.size + } + end + def test_help redirect_test_io do assert(system($unicorn_bin, "-h"), "help text returns true") @@ -113,7 +141,7 @@ end pid_file = "#{@tmpdir}/test.pid" old_file = "#{pid_file}.oldbin" ucfg = Tempfile.new('unicorn_test_config') - ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n") + ucfg.syswrite("listen %(#@addr:#@port)\n") ucfg.syswrite("pid %(#{pid_file})\n") ucfg.syswrite("logger Logger.new(%(#{@tmpdir}/log))\n") pid = xfork do @@ -126,14 +154,16 @@ end wait_for_file(pid_file) Process.waitpid(pid) - Process.kill('USR2', File.read(pid_file).to_i) + Process.kill(:USR2, File.read(pid_file).to_i) wait_for_file(old_file) wait_for_file(pid_file) - Process.kill('QUIT', File.read(old_file).to_i) + old_pid = File.read(old_file).to_i + Process.kill(:QUIT, old_pid) + wait_for_death(old_pid) ucfg.syswrite("timeout %(#{pid_file})\n") # introduce a bug current_pid = File.read(pid_file).to_i - Process.kill('USR2', current_pid) + Process.kill(:USR2, current_pid) # wait for pid_file to restore itself tries = DEFAULT_TRIES @@ -156,9 +186,11 @@ end # fix the bug ucfg.sysseek(0) ucfg.truncate(0) - ucfg.syswrite("listeners %w(#{@addr}:#{@port} #{@addr}:#{port2})\n") + ucfg.syswrite("listen %(#@addr:#@port)\n") + ucfg.syswrite("listen %(#@addr:#{port2})\n") ucfg.syswrite("pid %(#{pid_file})\n") - Process.kill('USR2', current_pid) + assert_nothing_raised { Process.kill(:USR2, current_pid) } + wait_for_file(old_file) wait_for_file(pid_file) new_pid = File.read(pid_file).to_i @@ -170,8 +202,8 @@ end assert_equal String, results[1].class assert_nothing_raised do - Process.kill('QUIT', current_pid) - Process.kill('QUIT', new_pid) + Process.kill(:QUIT, current_pid) + Process.kill(:QUIT, new_pid) end end @@ -192,14 +224,16 @@ end wait_for_file(pid_file) Process.waitpid(pid) - Process.kill('USR2', File.read(pid_file).to_i) + Process.kill(:USR2, File.read(pid_file).to_i) wait_for_file(old_file) wait_for_file(pid_file) - Process.kill('QUIT', File.read(old_file).to_i) + old_pid = File.read(old_file).to_i + Process.kill(:QUIT, old_pid) + wait_for_death(old_pid) File.unlink("config.ru") # break reloading current_pid = File.read(pid_file).to_i - Process.kill('USR2', current_pid) + Process.kill(:USR2, current_pid) # wait for pid_file to restore itself tries = DEFAULT_TRIES @@ -210,17 +244,17 @@ end rescue Errno::ENOENT (sleep(DEFAULT_RES) and (tries -= 1) > 0) and retry end - assert_equal current_pid, File.read(pid_file).to_i tries = DEFAULT_TRIES while File.exist?(old_file) (sleep(DEFAULT_RES) and (tries -= 1) > 0) or break end assert ! File.exist?(old_file), "oldbin=#{old_file} gone" + assert_equal current_pid, File.read(pid_file).to_i # fix the bug File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } - Process.kill('USR2', current_pid) + assert_nothing_raised { Process.kill(:USR2, current_pid) } wait_for_file(old_file) wait_for_file(pid_file) new_pid = File.read(pid_file).to_i @@ -230,25 +264,86 @@ end assert_equal String, results[0].class assert_nothing_raised do - Process.kill('QUIT', current_pid) - Process.kill('QUIT', new_pid) + Process.kill(:QUIT, current_pid) + Process.kill(:QUIT, new_pid) end end - def test_unicorn_config_listeners_overrides_cli - port2 = unused_port(@addr) + def test_unicorn_config_listener_swap + port_cli = unused_port File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } - # listeners = [ ... ] => should _override_ command-line options ucfg = Tempfile.new('unicorn_test_config') - ucfg.syswrite("listeners %w(#{@addr}:#{@port})\n") + ucfg.syswrite("listen '#@addr:#@port'\n") pid = xfork do redirect_test_io do - exec($unicorn_bin, "-c#{ucfg.path}", "-l#{@addr}:#{port2}") + exec($unicorn_bin, "-c#{ucfg.path}", "-l#@addr:#{port_cli}") end end + results = retry_hit(["http://#@addr:#{port_cli}/"]) + assert_equal String, results[0].class + results = retry_hit(["http://#@addr:#@port/"]) + assert_equal String, results[0].class + + port2 = unused_port(@addr) + ucfg.sysseek(0) + ucfg.truncate(0) + ucfg.syswrite("listen '#@addr:#{port2}'\n") + Process.kill(:HUP, pid) + + results = retry_hit(["http://#@addr:#{port2}/"]) + assert_equal String, results[0].class + results = retry_hit(["http://#@addr:#{port_cli}/"]) + assert_equal String, results[0].class + assert_nothing_raised do + reuse = TCPServer.new(@addr, @port) + reuse.close + end + assert_shutdown(pid) + end + + def test_unicorn_config_listen_with_options + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen '#{@addr}:#{@port}', :backlog => 512,\n") + ucfg.syswrite(" :rcvbuf => 4096,\n") + ucfg.syswrite(" :sndbuf => 4096\n") + pid = xfork do + redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } + end + results = retry_hit(["http://#{@addr}:#{@port}/"]) + assert_equal String, results[0].class + assert_shutdown(pid) + end + + def test_unicorn_config_per_worker_listen + port2 = unused_port + pid_spit = 'use Rack::ContentLength;' \ + 'run proc { |e| [ 200, {"Content-Type"=>"text/plain"}, ["#$$\\n"] ] }' + File.open("config.ru", "wb") { |fp| fp.syswrite(pid_spit) } + tmp = Tempfile.new('test.socket') + File.unlink(tmp.path) + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen '#@addr:#@port'\n") + ucfg.syswrite("before_fork { |s,w|\n") + ucfg.syswrite(" s.listen('#{tmp.path}', :backlog => 5, :sndbuf => 8192)\n") + ucfg.syswrite(" s.listen('#@addr:#{port2}', :rcvbuf => 8192)\n") + ucfg.syswrite("\n}\n") + pid = xfork do + redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } + end results = retry_hit(["http://#{@addr}:#{@port}/"]) - assert_raises(Errno::ECONNREFUSED) { TCPSocket.new(@addr, port2) } assert_equal String, results[0].class + worker_pid = results[0].to_i + assert_not_equal pid, worker_pid + s = UNIXSocket.new(tmp.path) + s.syswrite("GET / HTTP/1.0\r\n\r\n") + results = '' + loop { results << s.sysread(4096) } rescue nil + assert_nothing_raised { s.close } + assert_equal worker_pid, results.split(/\r\n/).last.to_i + results = hit(["http://#@addr:#{port2}/"]) + assert_equal String, results[0].class + assert_equal worker_pid, results[0].to_i assert_shutdown(pid) end @@ -283,34 +378,36 @@ end results = retry_hit(["http://#{@addr}:#{@port}/"]) assert_equal String, results[0].class wait_master_ready(COMMON_TMP.path) + wait_workers_ready(COMMON_TMP.path, 4) bf = File.readlines(COMMON_TMP.path).grep(/\bbefore_fork: worker=/) assert_equal 4, bf.size rotate = Tempfile.new('unicorn_rotate') assert_nothing_raised do File.rename(COMMON_TMP.path, rotate.path) - Process.kill('USR1', pid) + Process.kill(:USR1, pid) end wait_for_file(COMMON_TMP.path) assert File.exist?(COMMON_TMP.path), "#{COMMON_TMP.path} exists" # USR1 should've been passed to all workers tries = DEFAULT_TRIES log = File.readlines(rotate.path) - while (tries -= 1) > 0 && log.grep(/rotating logs\.\.\./).size < 4 + while (tries -= 1) > 0 && + log.grep(/reopening logs\.\.\./).size < 5 sleep DEFAULT_RES log = File.readlines(rotate.path) end - assert_equal 4, log.grep(/rotating logs\.\.\./).size - assert_equal 0, log.grep(/done rotating logs/).size + assert_equal 5, log.grep(/reopening logs\.\.\./).size + assert_equal 0, log.grep(/done reopening logs/).size tries = DEFAULT_TRIES log = File.readlines(COMMON_TMP.path) - while (tries -= 1) > 0 && log.grep(/done rotating logs/).size < 4 + while (tries -= 1) > 0 && log.grep(/done reopening logs/).size < 5 sleep DEFAULT_RES log = File.readlines(COMMON_TMP.path) end - assert_equal 4, log.grep(/done rotating logs/).size - assert_equal 0, log.grep(/rotating logs\.\.\./).size - assert_nothing_raised { Process.kill('QUIT', pid) } + assert_equal 5, log.grep(/done reopening logs/).size + assert_equal 0, log.grep(/reopening logs\.\.\./).size + assert_nothing_raised { Process.kill(:QUIT, pid) } status = nil assert_nothing_raised { pid, status = Process.waitpid2(pid) } assert status.success?, "exited successfully" @@ -380,6 +477,39 @@ end reexec_basic_test(pid, pid_file) end + def test_socket_unlinked_restore + results = nil + sock = Tempfile.new('unicorn_test_sock') + sock_path = sock.path + @sockets << sock_path + sock.close! + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("listen \"#{sock_path}\"\n") + + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{ucfg.path}") } } + wait_for_file(sock_path) + assert File.socket?(sock_path) + assert_nothing_raised do + sock = UNIXSocket.new(sock_path) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + results = sock.sysread(4096) + end + assert_equal String, results.class + assert_nothing_raised do + File.unlink(sock_path) + Process.kill(:HUP, pid) + end + wait_for_file(sock_path) + assert File.socket?(sock_path) + assert_nothing_raised do + sock = UNIXSocket.new(sock_path) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + results = sock.sysread(4096) + end + assert_equal String, results.class + end + def test_unicorn_config_file pid_file = "#{@tmpdir}/test.pid" sock = Tempfile.new('unicorn_test_sock') @@ -415,7 +545,7 @@ end assert_equal String, results.class # try reloading the config - sock = Tempfile.new('unicorn_test_sock') + sock = Tempfile.new('new_test_sock') new_sock_path = sock.path @sockets << new_sock_path sock.close! @@ -425,11 +555,12 @@ end assert_nothing_raised do ucfg = File.open(ucfg.path, "wb") + ucfg.syswrite("listen \"#{sock_path}\"\n") ucfg.syswrite("listen \"#{new_sock_path}\"\n") ucfg.syswrite("pid \"#{pid_file}\"\n") ucfg.syswrite("logger Logger.new('#{new_log.path}')\n") ucfg.close - Process.kill('HUP', pid) + Process.kill(:HUP, pid) end wait_for_file(new_sock_path) @@ -472,99 +603,72 @@ end reexec_usr2_quit_test(new_pid, pid_file) end - private + def test_reexec_fd_leak + unless RUBY_PLATFORM =~ /linux/ # Solaris may work, too, but I forget... + warn "FD leak test only works on Linux at the moment" + return + end + pid_file = "#{@tmpdir}/test.pid" + log = Tempfile.new('unicorn_test_log') + log.sync = true + ucfg = Tempfile.new('unicorn_test_config') + ucfg.syswrite("pid \"#{pid_file}\"\n") + ucfg.syswrite("logger Logger.new('#{log.path}')\n") + ucfg.syswrite("stderr_path '#{log.path}'\n") + ucfg.syswrite("stdout_path '#{log.path}'\n") + ucfg.close - # sometimes the server may not come up right away - def retry_hit(uris = []) - tries = DEFAULT_TRIES - begin - hit(uris) - rescue Errno::ECONNREFUSED => err - if (tries -= 1) > 0 - sleep DEFAULT_RES - retry - end - raise err + File.open("config.ru", "wb") { |fp| fp.syswrite(HI) } + pid = xfork do + redirect_test_io do + exec($unicorn_bin, "-D", "-l#{@addr}:#{@port}", "-c#{ucfg.path}") end end - def assert_shutdown(pid) - wait_master_ready("#{@tmpdir}/test_stderr.#{pid}.log") - assert_nothing_raised { Process.kill('QUIT', pid) } - status = nil - assert_nothing_raised { pid, status = Process.waitpid2(pid) } - assert status.success?, "exited successfully" - end + wait_master_ready(log.path) + File.truncate(log.path, 0) + wait_for_file(pid_file) + orig_pid = pid = File.read(pid_file).to_i + orig_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) + assert $?.success? + expect_size = orig_fds.size - def wait_master_ready(master_log) - tries = DEFAULT_TRIES - while (tries -= 1) > 0 - begin - File.readlines(master_log).grep(/master process ready/)[0] and return - rescue Errno::ENOENT - end - sleep DEFAULT_RES - end - raise "master process never became ready" + assert_nothing_raised do + Process.kill(:USR2, pid) + wait_for_file("#{pid_file}.oldbin") + Process.kill(:QUIT, pid) end + wait_for_death(pid) + + wait_master_ready(log.path) + File.truncate(log.path, 0) + wait_for_file(pid_file) + pid = File.read(pid_file).to_i + assert_not_equal orig_pid, pid + curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) + assert $?.success? - def reexec_usr2_quit_test(pid, pid_file) - assert File.exist?(pid_file), "pid file OK" - assert ! File.exist?("#{pid_file}.oldbin"), "oldbin pid file" - assert_nothing_raised { Process.kill('USR2', pid) } - assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } + # we could've inherited descriptors the first time around + assert expect_size >= curr_fds.size, curr_fds.inspect + expect_size = curr_fds.size + + assert_nothing_raised do + Process.kill(:USR2, pid) wait_for_file("#{pid_file}.oldbin") - wait_for_file(pid_file) - - # kill old master process - assert_not_equal pid, File.read(pid_file).to_i - assert_equal pid, File.read("#{pid_file}.oldbin").to_i - assert_nothing_raised { Process.kill('QUIT', pid) } - assert_not_equal pid, File.read(pid_file).to_i - assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } - wait_for_file(pid_file) - assert_nothing_raised { retry_hit(["http://#{@addr}:#{@port}/"]) } - assert_nothing_raised { Process.kill('QUIT', File.read(pid_file).to_i) } - end - - def reexec_basic_test(pid, pid_file) - results = retry_hit(["http://#{@addr}:#{@port}/"]) - assert_equal String, results[0].class - assert_nothing_raised { Process.kill(0, pid) } - master_log = "#{@tmpdir}/test_stderr.#{pid}.log" - wait_master_ready(master_log) - File.truncate(master_log, 0) - nr = 50 - kill_point = 2 - assert_nothing_raised do - nr.times do |i| - hit(["http://#{@addr}:#{@port}/#{i}"]) - i == kill_point and Process.kill('HUP', pid) - end - end - wait_master_ready(master_log) - assert File.exist?(pid_file), "pid=#{pid_file} exists" - new_pid = File.read(pid_file).to_i - assert_not_equal pid, new_pid - assert_nothing_raised { Process.kill(0, new_pid) } - assert_nothing_raised { Process.kill('QUIT', new_pid) } + Process.kill(:QUIT, pid) end + wait_for_death(pid) - def wait_for_file(path) - tries = DEFAULT_TRIES - while (tries -= 1) > 0 && ! File.exist?(path) - sleep DEFAULT_RES - end - assert File.exist?(path), "path=#{path} exists" - end + wait_master_ready(log.path) + File.truncate(log.path, 0) + wait_for_file(pid_file) + pid = File.read(pid_file).to_i + curr_fds = `ls -l /proc/#{pid}/fd`.split(/\n/) + assert $?.success? + assert_equal expect_size, curr_fds.size, curr_fds.inspect - def xfork(&block) - fork do - ObjectSpace.each_object(Tempfile) do |tmp| - ObjectSpace.undefine_finalizer(tmp) - end - yield - end - end + Process.kill(:QUIT, pid) + wait_for_death(pid) + end end if do_test |