Skip to content

Commit

Permalink
Use SIGHUP to dynamically reload an fcgi process without restarting i…
Browse files Browse the repository at this point in the history
…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
jamis committed Jun 29, 2005
1 parent 8335fc6 commit 3cc47a4
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 134 deletions.
4 changes: 3 additions & 1 deletion railties/CHANGELOG
@@ -1,5 +1,7 @@
*SVN* *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) * 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] * Fixed rakefile actions against PostgreSQL when the password is all numeric #1462 [michael@schubert.cx]
Expand All @@ -22,7 +24,7 @@


* Added graceful exit from pressing CTRL-C during the run of the rails command #1150 [Caleb Tennis] * 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] * Made dispatch.fcgi more robust by catching fluke errors and retrying unless its a permanent condition. [Jamis Buck]


Expand Down
98 changes: 3 additions & 95 deletions railties/dispatches/dispatch.fcgi
@@ -1,98 +1,6 @@
#!/usr/local/bin/ruby #!/usr/local/bin/ruby


# to allow unit testing require File.dirname(__FILE__) + "/../config/environment"
if !defined?(RAILS_ROOT) require 'fcgi_handler'
require File.dirname(__FILE__) + "/../config/environment"
end


require 'dispatcher' RailsFCGIHandler.process!
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
19 changes: 10 additions & 9 deletions railties/lib/dispatcher.rb
Expand Up @@ -33,9 +33,16 @@ def dispatch(cgi = CGI.new, session_options = ActionController::CgiRequest::DEFA
rescue Object => exception rescue Object => exception
ActionController::Base.process_with_exception(request, response, exception).out(output) ActionController::Base.process_with_exception(request, response, exception).out(output)
ensure ensure
reset_application reset_after_dispatch
end end
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 private
def prepare_application def prepare_application
Expand All @@ -44,14 +51,8 @@ def prepare_application
Controllers.const_load!(:ApplicationController, "application") unless Controllers.const_defined?(:ApplicationController) Controllers.const_load!(:ApplicationController, "application") unless Controllers.const_defined?(:ApplicationController)
end end


def reset_application def reset_after_dispatch
if Dependencies.load? 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

Breakpoint.deactivate_drb if defined?(BREAKPOINT_SERVER_PORT) Breakpoint.deactivate_drb if defined?(BREAKPOINT_SERVER_PORT)
end end
end end
Expand Down
112 changes: 112 additions & 0 deletions 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
83 changes: 54 additions & 29 deletions railties/test/fcgi_dispatcher_test.rb
@@ -1,15 +1,15 @@
$:.unshift File.dirname(__FILE__) + "/../lib"
$:.unshift File.dirname(__FILE__) + "/mocks" $:.unshift File.dirname(__FILE__) + "/mocks"


require 'test/unit' require 'test/unit'
require 'stringio' require 'stringio'
require 'fcgi_handler'


if !defined?(RailsFCGIHandler) RAILS_ROOT = File.dirname(__FILE__) if !defined?(RAILS_ROOT)
RAILS_ROOT = File.dirname(__FILE__)
load File.dirname(__FILE__) + "/../dispatches/dispatch.fcgi"
end


class RailsFCGIHandler class RailsFCGIHandler
attr_reader :exit_code attr_reader :exit_code
attr_reader :restarted
attr_accessor :thread attr_accessor :thread


def trap(signal, handler, &block) def trap(signal, handler, &block)
Expand All @@ -25,6 +25,10 @@ def exit(code=0)
def send_signal(which) def send_signal(which)
@signal_handlers[which].call(which) @signal_handlers[which].call(which)
end end

def restore!
@restarted = true
end
end end


class RailsFCGIHandlerTest < Test::Unit::TestCase class RailsFCGIHandlerTest < Test::Unit::TestCase
Expand All @@ -40,32 +44,53 @@ def setup
def test_uninterrupted_processing def test_uninterrupted_processing
@handler.process! @handler.process!
assert_nil @handler.exit_code assert_nil @handler.exit_code
assert !@handler.please_exit_at_your_earliest_convenience assert_nil @handler.when_ready
assert !@handler.i_am_currently_processing_a_request assert !@handler.processing
end end


%w(HUP USR1).each do |signal| def test_interrupted_via_HUP_when_not_in_request
define_method("test_interrupted_via_#{signal}_when_not_in_request") do FCGI.time_to_sleep = 1
FCGI.time_to_sleep = 1 @handler.thread = Thread.new { @handler.process! }
@handler.thread = Thread.new { @handler.process! } sleep 0.1 # let the thread get started
sleep 0.1 # let the thread get started @handler.send_signal("HUP")
@handler.send_signal(signal) @handler.thread.join
@handler.thread.join assert_nil @handler.exit_code
assert_equal 0, @handler.exit_code assert_nil @handler.when_ready
assert !@handler.please_exit_at_your_earliest_convenience assert !@handler.processing
assert !@handler.i_am_currently_processing_a_request assert @handler.restarted
end end


define_method("test_interrupted_via_#{signal}_when_in_request") do def test_interrupted_via_HUP_when_in_request
Dispatcher.time_to_sleep = 1 Dispatcher.time_to_sleep = 1
@handler.thread = Thread.new { @handler.process! } @handler.thread = Thread.new { @handler.process! }
sleep 0.1 # let the thread get started sleep 0.1 # let the thread get started
@handler.send_signal(signal) @handler.send_signal("HUP")
@handler.thread.join @handler.thread.join
assert_nil @handler.exit_code assert_nil @handler.exit_code
assert @handler.please_exit_at_your_earliest_convenience assert_equal :restart, @handler.when_ready
assert !@handler.i_am_currently_processing_a_request assert !@handler.processing
end 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 end


%w(RuntimeError SignalException).each do |exception| %w(RuntimeError SignalException).each do |exception|
Expand All @@ -77,7 +102,7 @@ def test_uninterrupted_processing
when "RuntimeError" when "RuntimeError"
assert_match %r{almost killed}, @log.string assert_match %r{almost killed}, @log.string
when "SignalException" when "SignalException"
assert_match %r{\d killed}, @log.string assert_match %r{^killed}, @log.string
end end
end end


Expand All @@ -89,7 +114,7 @@ def test_uninterrupted_processing
when "RuntimeError" when "RuntimeError"
assert_no_match %r{killed}, @log.string assert_no_match %r{killed}, @log.string
when "SignalException" when "SignalException"
assert_match %r{\d killed}, @log.string assert_match %r{^killed}, @log.string
end end
end end
end end
Expand Down

0 comments on commit 3cc47a4

Please sign in to comment.