From 6f6e4115b4bb03e5e7c55def91527799190566f2 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Mon, 9 Dec 2013 09:20:39 +0000 Subject: rework master-to-worker signaling to use a pipe Signaling using normal kill(2) is preserved, but the master now prefers to signal workers using a pipe rather than kill(2). Non-graceful signals (:TERM/:KILL) are still sent using kill(2), as they ask for immediate shutdown. This change is necessary to avoid triggering the ubf (unblocking function) for rb_thread_call_without_gvl (and similar) functions extensions. Most notably, this fixes compatibility with newer versions of the 'pg' gem which will cancel a running DB query if signaled[1]. This also has the nice side-effect of allowing a premature master death (assuming preload_app didn't cause the master to spawn off rogue child daemons). Note: users should also refrain from using "killall" if using the 'pg' gem or something like it. Unfortunately, this increases FD usage in the master as the writable end of the pipe is preserved in the master. This limit the number of worker processes the master may run to the open file limit of the master process. Increasing the open file limit of the master process may be needed. However, the FD use on the workers is reduced by one as the internal self-pipe is no longer used. Thus, overall pipe allocation for the kernel remains unchanged. [1] - pg is correct to cancel a query, as it cannot know if the signal was for a) graceful unicorn shutdown or b) oh-noes-I-started-a-bad-query-ABORT-ABORT-ABORT!! --- lib/unicorn/worker.rb | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) (limited to 'lib/unicorn/worker.rb') diff --git a/lib/unicorn/worker.rb b/lib/unicorn/worker.rb index 1fb6a4a..e74a1c9 100644 --- a/lib/unicorn/worker.rb +++ b/lib/unicorn/worker.rb @@ -12,6 +12,7 @@ class Unicorn::Worker # :stopdoc: attr_accessor :nr, :switched attr_writer :tmp + attr_reader :to_io # IO.select-compatible PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE DROPS = [] @@ -23,6 +24,66 @@ class Unicorn::Worker @raindrop[@offset] = 0 @nr = nr @tmp = @switched = false + @to_io, @master = Unicorn.pipe + end + + def atfork_child # :nodoc: + # we _must_ close in child, parent just holds this open to signal + @master = @master.close + end + + # master fakes SIGQUIT using this + def quit # :nodoc: + @master = @master.close if @master + end + + # parent does not read + def atfork_parent # :nodoc: + @to_io = @to_io.close + end + + # call a signal handler immediately without triggering EINTR + # We do not use the more obvious Process.kill(sig, $$) here since + # that signal delivery may be deferred. We want to avoid signal delivery + # while the Rack app.call is running because some database drivers + # (e.g. ruby-pg) may cancel pending requests. + def fake_sig(sig) # :nodoc: + old_cb = trap(sig, "IGNORE") + old_cb.call + ensure + trap(sig, old_cb) + end + + # master sends fake signals to children + def soft_kill(sig) # :nodoc: + case sig + when Integer + signum = sig + else + signum = Signal.list[sig.to_s] or + raise ArgumentError, "BUG: bad signal: #{sig.inspect}" + end + # writing and reading 4 bytes on a pipe is atomic on all POSIX platforms + # Do not care in the odd case the buffer is full, here. + @master.kgio_trywrite([signum].pack('l')) + rescue Errno::EPIPE + # worker will be reaped soon + end + + # this only runs when the Rack app.call is not running + # act like a listener + def kgio_tryaccept # :nodoc: + case buf = @to_io.kgio_tryread(4) + when String + # unpack the buffer and trigger the signal handler + signum = buf.unpack('l') + fake_sig(signum[0]) + # keep looping, more signals may be queued + when nil # EOF: master died, but we are at a safe place to exit + fake_sig(:QUIT) + when :wait_readable # keep waiting + return false + end while true # loop, as multiple signals may be sent end # worker objects may be compared to just plain Integers @@ -49,8 +110,11 @@ class Unicorn::Worker end end + # called in both the master (reaping worker) and worker (SIGQUIT handler) def close # :nodoc: @tmp.close if @tmp + @master.close if @master + @to_io.close if @to_io end # :startdoc: -- cgit v1.2.3-24-ge0c7