Skip to content
Browse files

Refactored dispatch.fcgi. Added unit tests for dispatch.fcgi. Added t…

…rap to recognize HUP as a graceful termination command.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1479 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 053cb22 commit f69f3848727362648e1b44a2450d0f89dce32bb2 @jamis jamis committed
View
136 railties/dispatches/dispatch.fcgi
@@ -1,69 +1,99 @@
#!/usr/local/bin/ruby
-def dispatcher_log(level, path,msg)
- Logger.new(path).send(level, msg)
-rescue Object => log_error
- STDERR << "Couldn't write to #{path}: #{msg}"
+# to allow unit testing
+if !defined?(RAILS_ROOT)
+ require File.dirname(__FILE__) + "/../config/environment"
end
-def dispatcher_error(path,e,msg="")
- error_message =
- "[#{Time.now}] Dispatcher failed to catch: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}\n#{msg}"
- dispatcher_log(:error, path, error_message)
-end
+require 'dispatcher'
+require 'fcgi'
+require 'logger'
-last_error_on = nil
-begin
- require File.dirname(__FILE__) + "/../config/environment"
- require 'dispatcher'
- require 'fcgi'
-
- log_file_path = "#{RAILS_ROOT}/log/fastcgi.crash.log"
- dispatcher_log(:info, log_file_path, "fcgi #{$$} starting")
-
- # Allow graceful exits by sending the process SIGUSR1. If the process is
- # currently handling a request, the request will be allowed to complete and
- # then will terminate itself. If a request is not being handled, the
- # process is terminated immediately (via #exit).
-
- $please_exit_at_your_earliest_convenience = false
- $i_am_currently_processing_a_request = false
- trap("USR1") do
- if $i_am_currently_processing_a_request
- dispatcher_log(:info, log_file_path, "asking #{$$} to terminate ASAP")
- $please_exit_at_your_earliest_convenience = true
+class RailsFCGIHandler
+ attr_reader :please_exit_at_your_earliest_convenience
+ attr_reader :i_am_currently_processing_a_request
+
+ def initialize(log_file_path = "#{RAILS_ROOT}/log/fastcgi.crash.log")
+ @please_exit_at_your_earliest_convenience = false
+ @i_am_currently_processing_a_request = false
+
+ trap_handler = method(:trap_handler).to_proc
+ trap("HUP", trap_handler)
+ trap("USR1", trap_handler)
+
+ # initialize to 11 seconds from now to minimize special cases
+ @last_error_on = Time.now + 11
+
+ @log_file_path = log_file_path
+ dispatcher_log(:info, "fcgi #{$$} starting")
+ end
+
+ def process!
+ FCGI.each_cgi do |cgi|
+ process_request(cgi)
+ break if please_exit_at_your_earliest_convenience
+ end
+
+ dispatcher_log(:info, "fcgi #{$$} terminated gracefully")
+
+ rescue SystemExit => exit_error
+ dispatcher_log(:info, "fcgi #{$$} terminated by explicit exit")
+
+ rescue Object => fcgi_error
+ # retry on errors that would otherwise have terminated the FCGI process,
+ # but only if they occur more than 10 seconds apart.
+ if !(SignalException === fcgi_error) && @last_error_on - Time.now > 10
+ @last_error_on = Time.now
+ dispatcher_error(fcgi_error,
+ "FCGI process #{$$} almost killed by this error\n")
+ retry
else
- dispatcher_log(:info, log_file_path, "telling #{$$} to terminate NOW")
- exit
+ dispatcher_error(fcgi_error, "FCGI process #{$$} killed by this error\n")
end
end
- # Process each request as it comes in, as a pseudo-CGI.
+ private
+ def logger
+ @logger ||= Logger.new(@log_file_path)
+ end
+
+ def dispatcher_log(level, msg)
+ logger.send(level, msg)
+ rescue Object => log_error
+ STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n"
+ STDERR << " #{log_error.class}: #{log_error.message}\n"
+ end
+
+ def dispatcher_error(e,msg="")
+ error_message =
+ "[#{Time.now}] Dispatcher failed to catch: #{e} (#{e.class})\n" +
+ " #{e.backtrace.join("\n ")}\n#{msg}"
+ dispatcher_log(:error, error_message)
+ end
+
+ def trap_handler
+ if i_am_currently_processing_a_request
+ dispatcher_log(:info, "asking #{$$} to terminate ASAP")
+ @please_exit_at_your_earliest_convenience = true
+ else
+ dispatcher_log(:info, "telling #{$$} to terminate NOW")
+ exit
+ end
+ end
- FCGI.each_cgi do |cgi|
- begin
- $i_am_currently_processing_a_request = true
+ def process_request(cgi)
+ @i_am_currently_processing_a_request = true
Dispatcher.dispatch(cgi)
rescue Object => e
- dispatcher_error(log_file_path, e)
+ raise if SignalException === e
+ dispatcher_error(e)
ensure
$stdout.flush
- $i_am_currently_processing_a_request = false
- break if $please_exit_at_your_earliest_convenience
+ @i_am_currently_processing_a_request = false
end
- end
+end
- dispatcher_log(:info, log_file_path, "fcgi #{$$} terminated gracefully")
-rescue SystemExit => exit_error
- dispatcher_log(:info, log_file_path, "fcgi #{$$} terminated by explicit exit")
-rescue Object => fcgi_error
- # retry on errors that would otherwise have terminated the FCGI process, but
- # only if they occur more than 10 seconds apart.
- if !(SignalException === fcgi_error) && (last_error_on.nil? || last_error_on - Time.now > 10)
- last_error_on = Time.now
- dispatcher_error(log_file_path, fcgi_error, "FCGI process #{$$} almost killed by this error\n")
- retry
- else
- dispatcher_error(log_file_path, fcgi_error, "FCGI process #{$$} killed by this error\n")
- end
-end
+if __FILE__ == $0
+ handler = RailsFCGIHandler.new
+ handler.process!
+end
View
96 railties/test/fcgi_dispatcher_test.rb
@@ -0,0 +1,96 @@
+$:.unshift File.dirname(__FILE__) + "/mocks"
+
+require 'test/unit'
+require 'stringio'
+
+if !defined?(RailsFCGIHandler)
+ RAILS_ROOT = File.dirname(__FILE__)
+ load File.dirname(__FILE__) + "/../dispatches/dispatch.fcgi"
+end
+
+class RailsFCGIHandler
+ attr_reader :exit_code
+ attr_accessor :thread
+
+ def trap(signal, handler, &block)
+ handler ||= block
+ (@signal_handlers ||= Hash.new)[signal] = handler
+ end
+
+ def exit(code=0)
+ @exit_code = code
+ (thread || Thread.current).exit
+ end
+
+ def send_signal(which)
+ @signal_handlers[which].call
+ end
+end
+
+class RailsFCGIHandlerTest < Test::Unit::TestCase
+ def setup
+ @log = StringIO.new
+ @handler = RailsFCGIHandler.new(@log)
+ FCGI.time_to_sleep = nil
+ FCGI.raise_exception = nil
+ Dispatcher.time_to_sleep = nil
+ Dispatcher.raise_exception = nil
+ end
+
+ def test_uninterrupted_processing
+ @handler.process!
+ assert_nil @handler.exit_code
+ assert !@handler.please_exit_at_your_earliest_convenience
+ assert !@handler.i_am_currently_processing_a_request
+ end
+
+ %w(HUP USR1).each do |signal|
+ define_method("test_interrupted_via_#{signal}_when_not_in_request") do
+ FCGI.time_to_sleep = 1
+ @handler.thread = Thread.new { @handler.process! }
+ sleep 0.1 # let the thread get started
+ @handler.send_signal(signal)
+ @handler.thread.join
+ assert_equal 0, @handler.exit_code
+ assert !@handler.please_exit_at_your_earliest_convenience
+ assert !@handler.i_am_currently_processing_a_request
+ end
+
+ define_method("test_interrupted_via_#{signal}_when_in_request") do
+ Dispatcher.time_to_sleep = 1
+ @handler.thread = Thread.new { @handler.process! }
+ sleep 0.1 # let the thread get started
+ @handler.send_signal(signal)
+ @handler.thread.join
+ assert_nil @handler.exit_code
+ assert @handler.please_exit_at_your_earliest_convenience
+ assert !@handler.i_am_currently_processing_a_request
+ end
+ end
+
+ %w(RuntimeError SignalException).each do |exception|
+ define_method("test_#{exception}_in_fcgi") do
+ FCGI.raise_exception = Object.const_get(exception)
+ @handler.process!
+ assert_match %r{Dispatcher failed to catch}, @log.string
+ case exception
+ when "RuntimeError"
+ assert_match %r{almost killed}, @log.string
+ when "SignalException"
+ assert_match %r{\d killed}, @log.string
+ end
+ end
+
+ define_method("test_#{exception}_in_dispatcher") do
+ Dispatcher.raise_exception = Object.const_get(exception)
+ @handler.process!
+ assert_match %r{Dispatcher failed to catch}, @log.string
+ case exception
+ when "RuntimeError"
+ assert_no_match %r{killed}, @log.string
+ when "SignalException"
+ assert_match %r{\d killed}, @log.string
+ end
+ end
+ end
+end
View
11 railties/test/mocks/dispatcher.rb
@@ -0,0 +1,11 @@
+class Dispatcher
+ class <<self
+ attr_accessor :time_to_sleep
+ attr_accessor :raise_exception
+
+ def dispatch(cgi)
+ sleep(time_to_sleep || 0)
+ raise raise_exception, "Something died" if raise_exception
+ end
+ end
+end
View
12 railties/test/mocks/fcgi.rb
@@ -0,0 +1,12 @@
+class FCGI
+ class << self
+ attr_accessor :time_to_sleep
+ attr_accessor :raise_exception
+
+ def each_cgi
+ sleep(time_to_sleep || 0)
+ raise raise_exception, "Something died" if raise_exception
+ yield "mock cgi value"
+ end
+ end
+end

0 comments on commit f69f384

Please sign in to comment.
Something went wrong with that request. Please try again.