1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

r1588@asus: jeremy | 2005-07-02 03:14:45 -0700

Optional periodic garbage collection for dispatch.fcgi.  Graceful exit on TERM also (a la Apache1).  Ignore signals the platform does not support, such as USR1 on Windows.


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1592 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jeremy Kemper 2005-07-02 04:52:14 +00:00
parent b829493220
commit 5650bc9094
6 changed files with 161 additions and 21 deletions

View file

@ -1,5 +1,9 @@
*SVN* *SVN*
* SIGTERM also gracefully exits dispatch.fcgi. Ignore SIGUSR1 on Windows.
* Add the option to manually manage garbage collection in the FastCGI dispatcher. Set the number of requests between GC runs in your public/dispatch.fcgi. [skaes@web.de]
* 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. * 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)

View file

@ -1,5 +1,23 @@
#!/usr/local/bin/ruby #!/usr/local/bin/ruby
#
# You may specify the path to the FastCGI crash log (a log of unhandled
# exceptions which forced the FastCGI instance to exit, great for debugging)
# and the number of requests to process before running garbage collection.
#
# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
# and the GC period is nil (turned off). A reasonable number of requests
# could range from 10-100 depending on the memory footprint of your app.
#
# Example:
# # Default log path, normal GC behavior.
# RailsFCGIHandler.process!
#
# # Default log path, 50 requests between GC.
# RailsFCGIHandler.process! nil, 50
#
# # Custom log path, normal GC behavior.
# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
#
require File.dirname(__FILE__) + "/../config/environment" require File.dirname(__FILE__) + "/../config/environment"
require 'fcgi_handler' require 'fcgi_handler'

View file

@ -3,41 +3,77 @@ require 'logger'
require 'dispatcher' require 'dispatcher'
class RailsFCGIHandler class RailsFCGIHandler
SIGNALS = {
'HUP' => :reload,
'TERM' => :graceful_exit,
'USR1' => :graceful_exit
}
attr_reader :when_ready attr_reader :when_ready
attr_reader :processing attr_reader :processing
def self.process! attr_accessor :log_file_path
new.process! attr_accessor :gc_request_period
# Initialize and run the FastCGI instance, passing arguments through to new.
def self.process!(*args, &block)
new(*args, &block).process!
end end
def initialize(log_file_path = "#{RAILS_ROOT}/log/fastcgi.crash.log") # Initialize the FastCGI instance with the path to a crash log
# detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log)
# and the number of requests to process between garbage collection runs
# (default nil for normal GC behavior.) Optionally, pass a block which
# takes this instance as an argument for further configuration.
def initialize(log_file_path = nil, gc_request_period = nil)
@when_ready = nil @when_ready = nil
@processing = false @processing = false
trap("HUP", method(:restart_handler).to_proc) self.log_file_path = log_file_path || "#{RAILS_ROOT}/log/fastcgi.crash.log"
trap("USR1", method(:trap_handler).to_proc) self.gc_request_period = gc_request_period
# initialize to 11 seconds ago to minimize special cases # Yield for additional configuration.
yield self if block_given?
# Safely install signal handlers.
install_signal_handlers
# Start error timestamp at 11 seconds ago.
@last_error_on = Time.now - 11 @last_error_on = Time.now - 11
@log_file_path = log_file_path
dispatcher_log(:info, "starting") dispatcher_log(:info, "starting")
end end
def process! def process!
# Make a note of $" so we can safely reload this instance.
mark! mark!
# Begin countdown to garbage collection.
run_gc! if gc_request_period
FCGI.each_cgi do |cgi| FCGI.each_cgi do |cgi|
if when_ready == :restart # Safely reload this instance if requested.
if when_ready == :reload
run_gc! if gc_request_period
restore! restore!
@when_ready = nil @when_ready = nil
dispatcher_log(:info, "restarted") dispatcher_log(:info, "reloaded")
end end
process_request(cgi) process_request(cgi)
# Break if graceful exit requested.
break if when_ready == :exit break if when_ready == :exit
# Garbage collection countdown.
if gc_request_period
@gc_request_countdown -= 1
run_gc! if @gc_request_countdown <= 0
end
end end
GC.enable
dispatcher_log(:info, "terminated gracefully") dispatcher_log(:info, "terminated gracefully")
rescue SystemExit => exit_error rescue SystemExit => exit_error
@ -75,7 +111,19 @@ class RailsFCGIHandler
dispatcher_log(:error, error_message) dispatcher_log(:error, error_message)
end end
def trap_handler(signal) def install_signal_handlers
SIGNALS.each do |signal, handler_name|
install_signal_handler signal, method("#{handler_name}_handler").to_proc
end
end
def install_signal_handler(signal, handler)
trap signal, handler
rescue ArgumentError
dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
end
def graceful_exit_handler(signal)
if processing if processing
dispatcher_log :info, "asked to terminate ASAP" dispatcher_log :info, "asked to terminate ASAP"
@when_ready = :exit @when_ready = :exit
@ -85,9 +133,9 @@ class RailsFCGIHandler
end end
end end
def restart_handler(signal) def reload_handler(signal)
@when_ready = :restart @when_ready = :reload
dispatcher_log :info, "asked to restart ASAP" dispatcher_log :info, "asked to reload ASAP"
end end
def process_request(cgi) def process_request(cgi)
@ -109,4 +157,9 @@ class RailsFCGIHandler
Dispatcher.reset_application! Dispatcher.reset_application!
ActionController::Routing::Routes.reload ActionController::Routing::Routes.reload
end end
def run_gc!
@gc_request_countdown = gc_request_period
GC.enable; GC.start; GC.disable
end
end end

View file

@ -9,8 +9,9 @@ RAILS_ROOT = File.dirname(__FILE__) if !defined?(RAILS_ROOT)
class RailsFCGIHandler class RailsFCGIHandler
attr_reader :exit_code attr_reader :exit_code
attr_reader :restarted attr_reader :reloaded
attr_accessor :thread attr_accessor :thread
attr_reader :gc_runs
def trap(signal, handler, &block) def trap(signal, handler, &block)
handler ||= block handler ||= block
@ -27,7 +28,14 @@ class RailsFCGIHandler
end end
def restore! def restore!
@restarted = true @reloaded = true
end
alias_method :old_run_gc!, :run_gc!
def run_gc!
@gc_runs ||= 0
@gc_runs += 1
old_run_gc!
end end
end end
@ -57,7 +65,7 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase
assert_nil @handler.exit_code assert_nil @handler.exit_code
assert_nil @handler.when_ready assert_nil @handler.when_ready
assert !@handler.processing assert !@handler.processing
assert @handler.restarted assert @handler.reloaded
end end
def test_interrupted_via_HUP_when_in_request def test_interrupted_via_HUP_when_in_request
@ -67,7 +75,7 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase
@handler.send_signal("HUP") @handler.send_signal("HUP")
@handler.thread.join @handler.thread.join
assert_nil @handler.exit_code assert_nil @handler.exit_code
assert_equal :restart, @handler.when_ready assert_equal :reload, @handler.when_ready
assert !@handler.processing assert !@handler.processing
end end
@ -119,3 +127,55 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase
end end
end end
end end
class RailsFCGIHandlerPeriodicGCTest < Test::Unit::TestCase
def setup
@log = StringIO.new
FCGI.time_to_sleep = nil
FCGI.raise_exception = nil
FCGI.each_cgi_count = nil
Dispatcher.time_to_sleep = nil
Dispatcher.raise_exception = nil
Dispatcher.dispatch_hook = nil
end
def teardown
FCGI.each_cgi_count = nil
Dispatcher.dispatch_hook = nil
GC.enable
end
def test_normal_gc
@handler = RailsFCGIHandler.new(@log)
assert_nil @handler.gc_request_period
# When GC is enabled, GC.disable disables and returns false.
assert_equal false, GC.disable
end
def test_periodic_gc
Dispatcher.dispatch_hook = lambda do |cgi|
# When GC is disabled, GC.enable enables and returns true.
assert_equal true, GC.enable
GC.disable
end
@handler = RailsFCGIHandler.new(@log, 10)
assert_equal 10, @handler.gc_request_period
FCGI.each_cgi_count = 1
@handler.process!
assert_equal 1, @handler.gc_runs
FCGI.each_cgi_count = 10
@handler.process!
assert_equal 3, @handler.gc_runs
FCGI.each_cgi_count = 25
@handler.process!
assert_equal 6, @handler.gc_runs
assert_nil @handler.exit_code
assert_nil @handler.when_ready
assert !@handler.processing
end
end

View file

@ -2,8 +2,10 @@ class Dispatcher
class <<self class <<self
attr_accessor :time_to_sleep attr_accessor :time_to_sleep
attr_accessor :raise_exception attr_accessor :raise_exception
attr_accessor :dispatch_hook
def dispatch(cgi) def dispatch(cgi)
dispatch_hook.call(cgi) if dispatch_hook
sleep(time_to_sleep || 0) sleep(time_to_sleep || 0)
raise raise_exception, "Something died" if raise_exception raise raise_exception, "Something died" if raise_exception
end end

View file

@ -2,11 +2,14 @@ class FCGI
class << self class << self
attr_accessor :time_to_sleep attr_accessor :time_to_sleep
attr_accessor :raise_exception attr_accessor :raise_exception
attr_accessor :each_cgi_count
def each_cgi def each_cgi
sleep(time_to_sleep || 0) (each_cgi_count || 1).times do
raise raise_exception, "Something died" if raise_exception sleep(time_to_sleep || 0)
yield "mock cgi value" raise raise_exception, "Something died" if raise_exception
yield "mock cgi value"
end
end end
end end
end end