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:
parent
b829493220
commit
5650bc9094
6 changed files with 161 additions and 21 deletions
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue