about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--Documentation/yahns_config.txt11
-rw-r--r--lib/yahns/config.rb7
-rw-r--r--lib/yahns/fdmap.rb1
-rw-r--r--lib/yahns/server.rb15
-rw-r--r--lib/yahns/server_mp.rb2
-rw-r--r--test/test_server.rb33
6 files changed, 66 insertions, 3 deletions
diff --git a/Documentation/yahns_config.txt b/Documentation/yahns_config.txt
index db2634c..b49617b 100644
--- a/Documentation/yahns_config.txt
+++ b/Documentation/yahns_config.txt
@@ -419,6 +419,17 @@ Ruby it is running under.
 
     Default: uses the global, :default queue if none is specified
 
+* shutdown_timeout SECONDS
+
+    This defines the timeout for gracefully exiting the process if there
+    are still connected clients.  This should generally be higher or equal
+    to the app with the highest client_timeout value.
+
+    Increase this if your application has slow endpoints which may take
+    longer than the default timeout.
+
+    Default: max client_timeout value of all apps in the process
+
 * worker_processes INTEGER [&BLOCK]
 
     This directive allows yahns to use a master/worker configuration to
diff --git a/lib/yahns/config.rb b/lib/yahns/config.rb
index c4c1b41..e48ba4e 100644
--- a/lib/yahns/config.rb
+++ b/lib/yahns/config.rb
@@ -73,6 +73,11 @@ class Yahns::Config # :nodoc:
     end
   end
 
+  def shutdown_timeout(sec)
+    var = _check_in_block(nil, :shutdown_timeout)
+    @set[var] = _check_int(var, sec, 0)
+  end
+
   def worker_processes(nr, &blk)
     var =_check_in_block(nil, :worker_processes)
     @set[var] = _check_int(var, nr, 1)
@@ -392,7 +397,7 @@ class Yahns::Config # :nodoc:
       io.sync = true
     end
 
-    [ :logger, :pid, :worker_processes, :user,
+    [ :logger, :pid, :worker_processes, :user, :shutdown_timeout,
       :worker_atfork_prepare, :worker_atfork_parent, :worker_atfork_child
     ].each do |var|
       val = @set[var]
diff --git a/lib/yahns/fdmap.rb b/lib/yahns/fdmap.rb
index 8dda4e6..97e9f44 100644
--- a/lib/yahns/fdmap.rb
+++ b/lib/yahns/fdmap.rb
@@ -56,6 +56,7 @@ class Yahns::Fdmap # :nodoc:
   end
 
   # this is only called in Errno::EMFILE/Errno::ENFILE situations
+  # and graceful shutdown
   def desperate_expire_for(io, timeout)
     @fdmap_mtx.synchronize { __expire_for(io, timeout) }
   end
diff --git a/lib/yahns/server.rb b/lib/yahns/server.rb
index 4bd523f..6439918 100644
--- a/lib/yahns/server.rb
+++ b/lib/yahns/server.rb
@@ -12,12 +12,14 @@ class Yahns::Server # :nodoc:
   attr_accessor :logger
   attr_writer :user
   attr_writer :worker_processes
+  attr_writer :shutdown_timeout
   attr_writer :worker_atfork_prepare
   attr_writer :worker_atfork_parent
   attr_writer :worker_atfork_child
   include Yahns::SocketHelper
 
   def initialize(config)
+    @shutdown_timeout = nil
     @reexec_pid = 0
     @daemon_pipe = nil # writable IO or true
     @config = config
@@ -339,6 +341,8 @@ class Yahns::Server # :nodoc:
     # spin up applications (which are preload: false)
     @config.app_ctx.each(&:after_fork_init)
 
+    @shutdown_timeout ||= @config.app_ctx.map(&:client_timeout).max
+
     # spin up acceptor threads, clients flow into worker queues after this
     @listeners.each do |l|
       opts = sock_opts(l)
@@ -413,6 +417,15 @@ class Yahns::Server # :nodoc:
     alive
   end
 
+  def dropping(fdmap)
+    if drop_acceptors[0] || fdmap.size > 0
+      fdmap.desperate_expire_for(nil, @shutdown_timeout)
+      true
+    else
+      false
+    end
+  end
+
   # single-threaded only, this is overriden if @worker_processes is non-nil
   def join
     daemon_ready
@@ -422,7 +435,7 @@ class Yahns::Server # :nodoc:
       alive = sp_sig_handle(alive)
     rescue => e
       Yahns::Log.exception(@logger, "main loop", e)
-    end while alive || drop_acceptors[0] || fdmap.size > 0
+    end while alive || dropping(fdmap)
     unlink_pid_safe(@pid) if @pid
   ensure
     quit_finish
diff --git a/lib/yahns/server_mp.rb b/lib/yahns/server_mp.rb
index 551cf69..4957c22 100644
--- a/lib/yahns/server_mp.rb
+++ b/lib/yahns/server_mp.rb
@@ -142,7 +142,7 @@ module Yahns::ServerMP # :nodoc:
       alive = mp_sig_handle(worker, alive)
     rescue => e
       Yahns::Log.exception(@logger, "main worker loop", e)
-    end while alive || drop_acceptors[0] || fdmap.size > 0
+    end while alive || dropping(fdmap)
     exit
   ensure
     quit_finish
diff --git a/test/test_server.rb b/test/test_server.rb
index da57f8c..2cfee09 100644
--- a/test/test_server.rb
+++ b/test/test_server.rb
@@ -622,4 +622,37 @@ class TestServer < Testcase
     quit_wait(pid)
     FileUtils.rm_rf(tmpdir)
   end
+
+  def test_persistent_shutdown_timeout; _persistent_shutdown(nil); end
+  def test_persistent_shutdown_timeout_mp; _persistent_shutdown(1); end
+
+  def _persistent_shutdown(nr_workers)
+    err, cfg, host, port = @err, Yahns::Config.new, @srv.addr[3], @srv.addr[1]
+    pid = mkserver(cfg) do
+      ru = lambda { |e| [ 200, {'Content-Length'=>'2'}, %w(OK) ] }
+      cfg.instance_eval do
+        app(:rack, ru) { listen "#{host}:#{port}" }
+        stderr_path err.path
+        shutdown_timeout 1
+        worker_processes(nr_workers) if nr_workers
+      end
+    end
+    c = get_tcp_client(host, port)
+    c.write "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
+    assert_equal c, c.wait(30)
+    buf = ""
+    re = /\r\n\r\nOK\z/
+    Timeout.timeout(30) do
+      begin
+        buf << c.readpartial(666)
+      end until re =~ buf
+    end
+    refute_match %r{Connection: close}, buf
+    assert_nil c.wait(0.001), "connection should still be alive"
+    Process.kill(:QUIT, pid)
+    _, status = Timeout.timeout(5) { Process.waitpid2(pid) }
+    assert status.success?, status.inspect
+    assert_nil c.wait(1)
+    assert_nil c.read(666)
+  end
 end