Permalink
Browse files

Use SIGHUP to dynamically reload an fcgi process without restarting i…

…t. 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
  • Loading branch information...
1 parent 8335fc6 commit 3cc47a4297d9c43e88972555e853e2d5359d804f @jamis jamis committed Jun 29, 2005
View
@@ -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]
@@ -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!
@@ -33,9 +33,16 @@ def dispatch(cgi = CGI.new, session_options = ActionController::CgiRequest::DEFA
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 @@ def prepare_application
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
@@ -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
@@ -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 @@ def exit(code=0)
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 @@ def setup
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 @@ def test_uninterrupted_processing
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 @@ def test_uninterrupted_processing
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

0 comments on commit 3cc47a4

Please sign in to comment.