Handling of SIGUSR1

When you request a 'graceful' restart of Apache, mod_fastcgi sends a SIGUSR1 to each of the fastcgi worker processes. The intention is that each one should finish the current request, and then exit, at which point Apache will restart it. Of course, if the worker isn't doing anything, it should die immediately.

This is implemented in the fcgi C library as follows:

Unfortunately, Ruby defeats this mechanism in at least two ways:

  1. Ruby installs its own signal handlers for a host of common signals,

including USR1. The fcgi library will not install its own handler if it detects that a handler has already been set (i.e. the current handler is not SIG_DFL)

  1. When Ruby installs its own signal handlers, it does so with SA_RESTART

set. This means that the accept() call does not terminate with EINTR; it is restarted automatically by the OS.

When a signal comes in during the accept(), Ruby's own handler does nothing except store it in a queue to be processed later. It is only when the accept() call completes, i.e. when a genuine new request comes in, that Ruby takes action. Unfortunately it's too late by then, and if that already-accepted request is not honoured, a 500 Internal Error will be returned to the client.

The simplest solution to this would be to remove Ruby's SIGUSR1 handler before initialising the FastCGI library.

However, a cleaner solution is to call rb_thread_select before going into FastCGI's accept loop. If a signal happens during the select, it can be handled using Ruby's normal mechanisms. This also gives a very useful side-benefit, which is that FCGI::accept no longer blocks out other Ruby threads. The program below demonstrates this problem; its background logging thread is supposed to write a message every 10 seconds, but under older versions of ruby-fcgi it does not do so if it is waiting for a new request.

#!/usr/local/bin/ruby
require "fcgi"

Thread.new do
  f = File.new("/tmp/fcgi.log","a")
  f.sync=true
  while true
    f.puts "#{Time.now.to_s} pid #{$$}"
    sleep 10
  end
end

FCGI.each_cgi {|cgi|
  name = cgi['name'][0]
  puts cgi.header
  puts "Hey! You are #{name} " if name
  puts "Connecting from #{cgi.remote_addr}"
}

Having protected the accept() with a ruby select(), you can then handle signals as follows:

The USR1 handler should set a flag to note if a USR1 signal came in while the request was being processed; you terminate the loop if it was set. The overall effect is that USR1 will cause the process to terminate, but without causing a half-completed request.