From 3cc47a4297d9c43e88972555e853e2d5359d804f Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 29 Jun 2005 11:07:20 +0000 Subject: 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 --- railties/CHANGELOG | 4 +- railties/dispatches/dispatch.fcgi | 98 +---------------------------- railties/lib/dispatcher.rb | 19 +++--- railties/lib/fcgi_handler.rb | 112 ++++++++++++++++++++++++++++++++++ railties/test/fcgi_dispatcher_test.rb | 83 ++++++++++++++++--------- 5 files changed, 182 insertions(+), 134 deletions(-) create mode 100644 railties/lib/fcgi_handler.rb (limited to 'railties') diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 867ed839ff..437f78bba8 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -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] diff --git a/railties/dispatches/dispatch.fcgi b/railties/dispatches/dispatch.fcgi index 684f04db86..5164669f2e 100755 --- a/railties/dispatches/dispatch.fcgi +++ b/railties/dispatches/dispatch.fcgi @@ -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! diff --git a/railties/lib/dispatcher.rb b/railties/lib/dispatcher.rb index 1455168ba9..aaea8dd277 100644 --- a/railties/lib/dispatcher.rb +++ b/railties/lib/dispatcher.rb @@ -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 diff --git a/railties/lib/fcgi_handler.rb b/railties/lib/fcgi_handler.rb new file mode 100644 index 0000000000..f615ff6d34 --- /dev/null +++ b/railties/lib/fcgi_handler.rb @@ -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 diff --git a/railties/test/fcgi_dispatcher_test.rb b/railties/test/fcgi_dispatcher_test.rb index 466eefe225..d9f147b13d 100644 --- a/railties/test/fcgi_dispatcher_test.rb +++ b/railties/test/fcgi_dispatcher_test.rb @@ -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 -- cgit v1.2.3