提交 547447ad 编写于 作者: J Jeremy Kemper

RailsFCGIHandler tests. Closes #9630.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7593 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
上级 4e3ed5bc
......@@ -41,38 +41,72 @@ def initialize(log_file_path = nil, gc_request_period = nil)
# Start error timestamp at 11 seconds ago.
@last_error_on = Time.now - 11
dispatcher_log :info, "starting"
end
def process!(provider = FCGI)
# Make a note of $" so we can safely reload this instance.
mark!
run_gc! if gc_request_period
mark_features!
dispatcher_log :info, 'starting'
process_each_request provider
dispatcher_log :info, 'stopping gracefully'
rescue Exception => error
case error
when SystemExit
dispatcher_log :info, 'stopping after explicit exit'
when SignalException
dispatcher_error error, 'stopping after unhandled signal'
else
# Retry if exceptions occur more than 10 seconds apart.
if Time.now - @last_error_on > 10
@last_error_on = Time.now
dispatcher_error error, 'retrying after unhandled exception'
retry
else
dispatcher_error error, 'stopping after unhandled exception within 10 seconds of the last'
end
end
end
process_each_request!(provider)
GC.enable
dispatcher_log :info, "terminated gracefully"
protected
def process_each_request(provider)
cgi = nil
rescue SystemExit => exit_error
dispatcher_log :info, "terminated by explicit exit"
provider.each_cgi do |cgi|
process_request(cgi)
rescue Exception => fcgi_error # FCGI errors
# 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")
case when_ready
when :reload
reload!
when :restart
close_connection(cgi)
restart!
when :exit
close_connection(cgi)
break
end
end
rescue SignalException => signal
raise unless signal.message == 'SIGUSR1'
close_connection(cgi)
end
end
def process_request(cgi)
@when_ready = nil
gc_countdown
with_signal_handler 'USR1' do
begin
Dispatcher.dispatch(cgi)
rescue SignalException, SystemExit
raise
rescue Exception => error
dispatcher_error error, 'unhandled dispatch error'
end
end
end
protected
def logger
@logger ||= Logger.new(@log_file_path)
end
......@@ -97,10 +131,12 @@ def install_signal_handlers
end
def install_signal_handler(signal, handler = nil)
handler ||= method("#{SIGNALS[signal]}_handler").to_proc
trap(signal, handler)
rescue ArgumentError
dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
if SIGNALS.include?(signal) && self.class.method_defined?(name = "#{SIGNALS[signal]}_handler")
handler ||= method(name).to_proc
trap(signal, handler)
else
dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
end
end
def with_signal_handler(signal)
......@@ -111,12 +147,12 @@ def with_signal_handler(signal)
end
def exit_now_handler(signal)
dispatcher_log :info, "asked to terminate immediately"
dispatcher_log :info, "asked to stop immediately"
exit
end
def exit_handler(signal)
dispatcher_log :info, "asked to terminate ASAP"
dispatcher_log :info, "asked to stop ASAP"
@when_ready = :exit
end
......@@ -130,38 +166,6 @@ def restart_handler(signal)
@when_ready = :restart
end
def process_each_request!(provider)
cgi = nil
provider.each_cgi do |cgi|
with_signal_handler 'USR1' do
process_request(cgi)
end
case when_ready
when :reload
reload!
when :restart
close_connection(cgi)
restart!
when :exit
close_connection(cgi)
break
end
gc_countdown
end
rescue SignalException => signal
raise unless signal.message == 'SIGUSR1'
close_connection(cgi) if cgi
end
def process_request(cgi)
Dispatcher.dispatch(cgi)
rescue Exception => e # errors from CGI dispatch
raise if SignalException === e
dispatcher_error(e)
end
def restart!
config = ::Config::CONFIG
ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT']
......@@ -184,7 +188,8 @@ def reload!
dispatcher_log :info, "reloaded"
end
def mark!
# Make a note of $" so we can safely reload this instance.
def mark_features!
@features = $".clone
end
......@@ -201,12 +206,13 @@ def run_gc!
def gc_countdown
if gc_request_period
@gc_request_countdown ||= gc_request_period
@gc_request_countdown -= 1
run_gc! if @gc_request_countdown <= 0
end
end
def close_connection(cgi)
cgi.instance_variable_get("@request").finish
cgi.instance_variable_get("@request").finish if cgi
end
end
......@@ -4,18 +4,9 @@
$:.unshift File.dirname(__FILE__) + "/../builtin/rails_info"
require 'test/unit'
require 'stringio'
require 'active_support'
if defined?(RAILS_ROOT)
RAILS_ROOT.replace File.dirname(__FILE__)
else
RAILS_ROOT = File.dirname(__FILE__)
end
class Test::Unit::TestCase
# Add stuff here if you need it
end
# Wrap tests that use Mocha and skip if unavailable.
def uses_mocha(test_name)
require 'rubygems'
......@@ -25,3 +16,9 @@ def uses_mocha(test_name)
rescue LoadError
$stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
end
if defined?(RAILS_ROOT)
RAILS_ROOT.replace File.dirname(__FILE__)
else
RAILS_ROOT = File.dirname(__FILE__)
end
require File.dirname(__FILE__) + "/abstract_unit"
begin # rescue LoadError
uses_mocha 'fcgi dispatcher tests' do
require_library_or_gem 'mocha'
$:.unshift File.dirname(__FILE__) + "/mocks"
require 'stringio'
# Stubs
require 'fcgi_handler'
require 'routes'
require 'stubbed_kernel'
class RailsFCGIHandler
attr_reader :exit_code
attr_reader :reloaded
attr_accessor :thread
attr_reader :gc_runs
def trap(signal, handler, &block)
handler ||= block
(@signal_handlers ||= Hash.new)[signal] = handler
end
def exit(code=0)
@exit_code = code
(thread || Thread.current).exit
end
def send_signal(which)
@signal_handlers[which].call(which)
end
alias_method :old_run_gc!, :run_gc!
def run_gc!
@gc_runs ||= 0
@gc_runs += 1
old_run_gc!
end
end
module ActionController; module Routing; module Routes; end end end
class RailsFCGIHandlerTest < Test::Unit::TestCase
def setup
@log = StringIO.new
@handler = RailsFCGIHandler.new(@log)
FCGI.time_to_sleep = nil
FCGI.raise_exception = nil
Dispatcher.time_to_sleep = nil
Dispatcher.raise_exception = nil
end
def test_process_restart
@handler.stubs(:when_ready).returns(:restart)
@handler.expects(:close_connection)
cgi = mock
FCGI.stubs(:each_cgi).yields(cgi)
@handler.expects(:process_request).once
@handler.expects(:dispatcher_error).never
@handler.expects(:when_ready).returns(:restart)
@handler.expects(:close_connection).with(cgi)
@handler.expects(:reload!).never
@handler.expects(:restart!)
@handler.process!
end
def test_process_exit
@handler.stubs(:when_ready).returns(:exit)
@handler.expects(:close_connection)
cgi = mock
FCGI.stubs(:each_cgi).yields(cgi)
@handler.expects(:process_request).once
@handler.expects(:dispatcher_error).never
@handler.expects(:when_ready).returns(:exit)
@handler.expects(:close_connection).with(cgi)
@handler.expects(:reload!).never
@handler.expects(:restart!).never
@handler.process!
end
def test_process_with_system_exit_exception
@handler.stubs(:process_request).raises(SystemExit)
@handler.expects(:dispatcher_log).with(:info, "terminated by explicit exit")
cgi = mock
FCGI.stubs(:each_cgi).yields(cgi)
@handler.expects(:process_request).once.raises(SystemExit)
@handler.stubs(:dispatcher_log)
@handler.expects(:dispatcher_log).with(:info, regexp_matches(/^stopping/))
@handler.expects(:dispatcher_error).never
@handler.expects(:when_ready).never
@handler.expects(:close_connection).never
@handler.expects(:reload!).never
@handler.expects(:restart!).never
@handler.process!
end
def test_restart_handler
@handler.expects(:dispatcher_log).with(:info, "asked to restart ASAP")
@handler.send(:restart_handler, nil)
assert_equal :restart, @handler.when_ready
end
def test_install_signal_handler_should_log_on_bad_signal
@handler.stubs(:trap).raises(ArgumentError)
@handler.expects(:dispatcher_log).with(:warn, "Ignoring unsupported signal CHEESECAKE.")
@handler.send(:install_signal_handler, "CHEESECAKE", nil)
end
def test_reload
@handler.expects(:restore!)
@handler.expects(:dispatcher_log).with(:info, "reloaded")
......@@ -94,8 +80,8 @@ def test_reload
@handler.send(:reload!)
assert_nil @handler.when_ready
end
def test_reload_runs_gc_when_gc_request_period_set
@handler.expects(:run_gc!)
@handler.expects(:restore!)
......@@ -103,19 +89,20 @@ def test_reload_runs_gc_when_gc_request_period_set
@handler.gc_request_period = 10
@handler.send(:reload!)
end
def test_reload_doesnt_run_gc_if_gc_request_period_isnt_set
@handler.expects(:run_gc!).never
@handler.expects(:restore!)
@handler.expects(:dispatcher_log).with(:info, "reloaded")
@handler.send(:reload!)
end
def test_restart!
@handler.expects(:dispatcher_log).with(:info, "restarted")
assert_equal true, @handler.send(:restart!), "Exec wasn't run"
@handler.expects(:exec).returns('restarted')
assert_equal 'restarted', @handler.send(:restart!)
end
def test_restore!
$".expects(:replace)
Dispatcher.expects(:reset_application!)
......@@ -124,105 +111,129 @@ def test_restore!
end
def test_uninterrupted_processing
cgi = mock
FCGI.expects(:each_cgi).yields(cgi)
@handler.expects(:process_request).with(cgi)
@handler.process!
assert_nil @handler.exit_code
assert_nil @handler.when_ready
end
end
class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase
def setup
@log = StringIO.new
@handler = RailsFCGIHandler.new(@log)
end
def test_interrupted_via_HUP_when_not_in_request
@handler.expects(:reload!)
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
cgi = mock
FCGI.expects(:each_cgi).once.yields(cgi)
@handler.expects(:gc_countdown).returns { Process.kill 'HUP', $$ }
@handler.expects(:reload!).once
@handler.expects(:close_connection).never
@handler.expects(:exit).never
@handler.process!
assert_equal :reload, @handler.when_ready
end
def test_interrupted_via_HUP_when_in_request
@handler.expects(:reload!)
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
cgi = mock
FCGI.expects(:each_cgi).once.yields(cgi)
Dispatcher.expects(:dispatch).with(cgi).returns { Process.kill 'HUP', $$ }
@handler.expects(:reload!).once
@handler.expects(:close_connection).never
@handler.expects(:exit).never
@handler.process!
assert_equal :reload, @handler.when_ready
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_nil @handler.exit_code
assert_equal :exit, @handler.when_ready
cgi = mock
FCGI.expects(:each_cgi).once.yields(cgi)
@handler.expects(:gc_countdown).returns { Process.kill 'USR1', $$ }
@handler.expects(:exit_handler).never
@handler.expects(:reload!).never
@handler.expects(:close_connection).with(cgi).once
@handler.expects(:exit).never
@handler.process!
assert_nil @handler.when_ready
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
cgi = mock
FCGI.expects(:each_cgi).once.yields(cgi)
Dispatcher.expects(:dispatch).with(cgi).returns { Process.kill 'USR1', $$ }
@handler.expects(:reload!).never
@handler.expects(:close_connection).with(cgi).once
@handler.expects(:exit).never
@handler.process!
assert_equal :exit, @handler.when_ready
end
def test_interrupted_via_TERM
Dispatcher.time_to_sleep = 1
@handler.thread = Thread.new { @handler.process! }
sleep 0.1 # let the thread get started
@handler.send_signal("TERM")
@handler.thread.join
assert_equal 0, @handler.exit_code
cgi = mock
FCGI.expects(:each_cgi).once.yields(cgi)
Dispatcher.expects(:dispatch).with(cgi).returns { Process.kill 'TERM', $$ }
@handler.expects(:reload!).never
@handler.expects(:close_connection).never
@handler.process!
assert_nil @handler.when_ready
end
%w(RuntimeError SignalException).each do |exception|
define_method("test_#{exception}_in_fcgi") do
FCGI.raise_exception = Object.const_get(exception)
@handler.process!
assert_match %r{Dispatcher failed to catch}, @log.string
case exception
when "RuntimeError"
assert_match %r{almost killed}, @log.string
when "SignalException"
assert_match %r{^killed}, @log.string
end
end
define_method("test_#{exception}_in_dispatcher") do
Dispatcher.raise_exception = Object.const_get(exception)
@handler.process!
assert_match %r{Dispatcher failed to catch}, @log.string
case exception
when "RuntimeError"
assert_no_match %r{killed}, @log.string
when "SignalException"
assert_match %r{^killed}, @log.string
end
end
def test_runtime_exception_in_fcgi
error = RuntimeError.new('foo')
FCGI.expects(:each_cgi).times(2).raises(error)
@handler.expects(:dispatcher_error).with(error, regexp_matches(/^retrying/))
@handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/))
@handler.process!
end
def test_runtime_error_in_dispatcher
cgi = mock
error = RuntimeError.new('foo')
FCGI.expects(:each_cgi).once.yields(cgi)
Dispatcher.expects(:dispatch).once.with(cgi).raises(error)
@handler.expects(:dispatcher_error).with(error, regexp_matches(/^unhandled/))
@handler.process!
end
def test_signal_exception_in_fcgi
error = SignalException.new('USR2')
FCGI.expects(:each_cgi).once.raises(error)
@handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/))
@handler.process!
end
def test_signal_exception_in_dispatcher
cgi = mock
error = SignalException.new('USR2')
FCGI.expects(:each_cgi).once.yields(cgi)
Dispatcher.expects(:dispatch).once.with(cgi).raises(error)
@handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/))
@handler.process!
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
......@@ -235,31 +246,20 @@ def test_normal_gc
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
cgi = mock
FCGI.expects(:each_cgi).times(10).yields(cgi)
Dispatcher.expects(:dispatch).times(10).with(cgi)
FCGI.each_cgi_count = 25
@handler.expects(:run_gc!).never
9.times { @handler.process! }
@handler.expects(:run_gc!).once
@handler.process!
assert_equal 6, @handler.gc_runs
assert_nil @handler.exit_code
assert_nil @handler.when_ready
end
end
rescue LoadError => e
$stderr.puts "Skipping dispatcher tests. `gem install mocha` and try again. (#{e})"
end
\ No newline at end of file
end # uses_mocha
class FCGI
class << self
attr_accessor :time_to_sleep
attr_accessor :raise_exception
attr_accessor :each_cgi_count
def each_cgi
(each_cgi_count || 1).times do
sleep(time_to_sleep || 0)
raise raise_exception, "Something died" if raise_exception
yield "mock cgi value"
end
end
end
end
module Kernel
def exec(*args)
true
end
end
......@@ -37,7 +37,7 @@ class InstanceTag; end
class RailsGeneratorTest < Test::Unit::TestCase
BUILTINS = %w(controller integration_test mailer migration model observer plugin resource scaffold session_migration web_service)
BUILTINS = %w(controller integration_test mailer migration model observer plugin resource scaffold session_migration)
CAPITALIZED_BUILTINS = BUILTINS.map { |b| b.capitalize }
def setup
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册