mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
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:
@@ -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 @@ 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
|
||||
|
||||
112
railties/lib/fcgi_handler.rb
Normal file
112
railties/lib/fcgi_handler.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user