about summary refs log tree commit homepage
path: root/test/exec/test_exec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/exec/test_exec.rb')
-rw-r--r--test/exec/test_exec.rb370
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