Use SIGHUP to dynamically reload an fcgi process without restarting it. Refactored dispatch.fcgi so that the RailsFCGIHandler is in the lib dir.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1565 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jamis Buck
2005-06-29 11:07:20 +00:00
parent 8335fc610c
commit 3cc47a4297
5 changed files with 182 additions and 134 deletions

View File

@@ -1,5 +1,7 @@
*SVN*
* Allow dynamic application reloading for dispatch.fcgi processes by sending a SIGHUP. If the process is currently handling a request, the request will be allowed to complete first. This allows production fcgi's to be reloaded without having to restart them.
* RailsFCGIHandler (dispatch.fcgi) no longer tries to explicitly flush $stdout (CgiProcess#out always calls flush)
* Fixed rakefile actions against PostgreSQL when the password is all numeric #1462 [michael@schubert.cx]
@@ -22,7 +24,7 @@
* Added graceful exit from pressing CTRL-C during the run of the rails command #1150 [Caleb Tennis]
* Allow graceful exits for dispatch.fcgi processes by sending a SIGUSR1 or SIGHUP. 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). This basically works like restart graceful on Apache. [Jamis Buck]
* Allow graceful exits for dispatch.fcgi processes by sending a 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). This basically works like restart graceful on Apache. [Jamis Buck]
* Made dispatch.fcgi more robust by catching fluke errors and retrying unless its a permanent condition. [Jamis Buck]

View File

@@ -1,98 +1,6 @@
#!/usr/local/bin/ruby
# to allow unit testing
if !defined?(RAILS_ROOT)
require File.dirname(__FILE__) + "/../config/environment"
end
require File.dirname(__FILE__) + "/../config/environment"
require 'fcgi_handler'
require 'dispatcher'
require 'fcgi'
require 'logger'
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) && Time.now - @last_error_on > 10
@last_error_on = Time.now
dispatcher_error(fcgi_error,
"FCGI process #{$$} almost killed by this error\n")
retry
else
dispatcher_error(fcgi_error, "FCGI process #{$$} killed by this error\n")
end
end
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(signal)
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
def process_request(cgi)
@i_am_currently_processing_a_request = true
Dispatcher.dispatch(cgi)
rescue Object => e
raise if SignalException === e
dispatcher_error(e)
ensure
@i_am_currently_processing_a_request = false
end
end
if __FILE__ == $0
handler = RailsFCGIHandler.new
handler.process!
end
RailsFCGIHandler.process!

View File

@@ -33,9 +33,16 @@ class Dispatcher
rescue Object => exception
ActionController::Base.process_with_exception(request, response, exception).out(output)
ensure
reset_application
reset_after_dispatch
end
end
def reset_application!
Controllers.clear!
Dependencies.clear
Dependencies.remove_subclasses_for(ActiveRecord::Base, ActiveRecord::Observer, ActionController::Base)
Dependencies.remove_subclasses_for(ActionMailer::Base) if defined?(ActionMailer::Base)
end
private
def prepare_application
@@ -44,14 +51,8 @@ class Dispatcher
Controllers.const_load!(:ApplicationController, "application") unless Controllers.const_defined?(:ApplicationController)
end
def reset_application
if Dependencies.load?
Controllers.clear!
Dependencies.clear
Dependencies.remove_subclasses_for(ActiveRecord::Base, ActiveRecord::Observer, ActionController::Base)
Dependencies.remove_subclasses_for(ActionMailer::Base) if defined?(ActionMailer::Base)
end
def reset_after_dispatch
reset_application! if Dependencies.load?
Breakpoint.deactivate_drb if defined?(BREAKPOINT_SERVER_PORT)
end
end

View File

@@ -0,0 +1,112 @@
require 'fcgi'
require 'logger'
require 'dispatcher'
class RailsFCGIHandler
attr_reader :when_ready
attr_reader :processing
def self.process!
new.process!
end
def initialize(log_file_path = "#{RAILS_ROOT}/log/fastcgi.crash.log")
@when_ready = nil
@processing = false
trap("HUP", method(:restart_handler).to_proc)
trap("USR1", method(:trap_handler).to_proc)
# initialize to 11 seconds ago to minimize special cases
@last_error_on = Time.now - 11
@log_file_path = log_file_path
dispatcher_log(:info, "starting")
end
def process!
mark!
FCGI.each_cgi do |cgi|
if when_ready == :restart
restore!
@when_ready = nil
dispatcher_log(:info, "restarted")
end
process_request(cgi)
break if when_ready == :exit
end
dispatcher_log(:info, "terminated gracefully")
rescue SystemExit => exit_error
dispatcher_log(:info, "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) && Time.now - @last_error_on > 10
@last_error_on = Time.now
dispatcher_error(fcgi_error, "almost killed by this error")
retry
else
dispatcher_error(fcgi_error, "killed by this error")
end
end
private
def logger
@logger ||= Logger.new(@log_file_path)
end
def dispatcher_log(level, msg)
time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S")
logger.send(level, "[#{time_str} :: #{$$}] #{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 =
"Dispatcher failed to catch: #{e} (#{e.class})\n" +
" #{e.backtrace.join("\n ")}\n#{msg}"
dispatcher_log(:error, error_message)
end
def trap_handler(signal)
if processing
dispatcher_log :info, "asked to terminate ASAP"
@when_ready = :exit
else
dispatcher_log :info, "told to terminate NOW"
exit
end
end
def restart_handler(signal)
@when_ready = :restart
dispatcher_log :info, "asked to restart ASAP"
end
def process_request(cgi)
@processing = true
Dispatcher.dispatch(cgi)
rescue Object => e
raise if SignalException === e
dispatcher_error(e)
ensure
@processing = false
end
def mark!
@features = $".clone
end
def restore!
$".replace @features
Dispatcher.reset_application!
ActionController::Routing::Routes.reload
end
end

View File

@@ -1,15 +1,15 @@
$:.unshift File.dirname(__FILE__) + "/../lib"
$:.unshift File.dirname(__FILE__) + "/mocks"
require 'test/unit'
require 'stringio'
require 'fcgi_handler'
if !defined?(RailsFCGIHandler)
RAILS_ROOT = File.dirname(__FILE__)
load File.dirname(__FILE__) + "/../dispatches/dispatch.fcgi"
end
RAILS_ROOT = File.dirname(__FILE__) if !defined?(RAILS_ROOT)
class RailsFCGIHandler
attr_reader :exit_code
attr_reader :restarted
attr_accessor :thread
def trap(signal, handler, &block)
@@ -25,6 +25,10 @@ class RailsFCGIHandler
def send_signal(which)
@signal_handlers[which].call(which)
end
def restore!
@restarted = true
end
end
class RailsFCGIHandlerTest < Test::Unit::TestCase
@@ -40,32 +44,53 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase
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
assert_nil @handler.when_ready
assert !@handler.processing
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
def test_interrupted_via_HUP_when_not_in_request
FCGI.time_to_sleep = 1
@handler.thread = Thread.new { @handler.process! }
sleep 0.1 # let the thread get started
@handler.send_signal("HUP")
@handler.thread.join
assert_nil @handler.exit_code
assert_nil @handler.when_ready
assert !@handler.processing
assert @handler.restarted
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
def test_interrupted_via_HUP_when_in_request
Dispatcher.time_to_sleep = 1
@handler.thread = Thread.new { @handler.process! }
sleep 0.1 # let the thread get started
@handler.send_signal("HUP")
@handler.thread.join
assert_nil @handler.exit_code
assert_equal :restart, @handler.when_ready
assert !@handler.processing
end
def test_interrupted_via_USR1_when_not_in_request
FCGI.time_to_sleep = 1
@handler.thread = Thread.new { @handler.process! }
sleep 0.1 # let the thread get started
@handler.send_signal("USR1")
@handler.thread.join
assert_equal 0, @handler.exit_code
assert_nil @handler.when_ready
assert !@handler.processing
end
def test_interrupted_via_USR1_when_in_request
Dispatcher.time_to_sleep = 1
@handler.thread = Thread.new { @handler.process! }
sleep 0.1 # let the thread get started
@handler.send_signal("USR1")
@handler.thread.join
assert_nil @handler.exit_code
assert @handler.when_ready
assert !@handler.processing
end
%w(RuntimeError SignalException).each do |exception|
@@ -77,7 +102,7 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase
when "RuntimeError"
assert_match %r{almost killed}, @log.string
when "SignalException"
assert_match %r{\d killed}, @log.string
assert_match %r{^killed}, @log.string
end
end
@@ -89,7 +114,7 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase
when "RuntimeError"
assert_no_match %r{killed}, @log.string
when "SignalException"
assert_match %r{\d killed}, @log.string
assert_match %r{^killed}, @log.string
end
end
end