From 547447ad2abf72f6f5eb57a76b1a830feca34d90 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Sun, 23 Sep 2007 11:20:25 +0000 Subject: RailsFCGIHandler tests. Closes #9630. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7593 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/lib/fcgi_handler.rb | 130 ++++++------- railties/test/abstract_unit.rb | 17 +- railties/test/fcgi_dispatcher_test.rb | 296 +++++++++++++++--------------- railties/test/mocks/fcgi.rb | 15 -- railties/test/mocks/stubbed_breakpoint.rb | 2 - railties/test/mocks/stubbed_kernel.rb | 5 - railties/test/rails_generator_test.rb | 2 +- 7 files changed, 224 insertions(+), 243 deletions(-) delete mode 100644 railties/test/mocks/fcgi.rb delete mode 100644 railties/test/mocks/stubbed_breakpoint.rb delete mode 100644 railties/test/mocks/stubbed_kernel.rb (limited to 'railties') diff --git a/railties/lib/fcgi_handler.rb b/railties/lib/fcgi_handler.rb index 5bb26e4598..2b0d7b1916 100644 --- a/railties/lib/fcgi_handler.rb +++ b/railties/lib/fcgi_handler.rb @@ -41,38 +41,72 @@ class RailsFCGIHandler # 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 @@ class RailsFCGIHandler 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 @@ class RailsFCGIHandler 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 @@ class RailsFCGIHandler @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 @@ class RailsFCGIHandler 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 @@ class RailsFCGIHandler 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 diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 7b773531c6..e1ce32da65 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -4,18 +4,9 @@ $:.unshift File.dirname(__FILE__) + "/../lib" $:.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 diff --git a/railties/test/fcgi_dispatcher_test.rb b/railties/test/fcgi_dispatcher_test.rb index 37bf4c8641..7949cb6525 100644 --- a/railties/test/fcgi_dispatcher_test.rb +++ b/railties/test/fcgi_dispatcher_test.rb @@ -1,92 +1,78 @@ 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 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase @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 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase @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 @@ class RailsFCGIHandlerTest < Test::Unit::TestCase 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 @@ class RailsFCGIHandlerPeriodicGCTest < Test::Unit::TestCase 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 diff --git a/railties/test/mocks/fcgi.rb b/railties/test/mocks/fcgi.rb deleted file mode 100644 index 59260a684f..0000000000 --- a/railties/test/mocks/fcgi.rb +++ /dev/null @@ -1,15 +0,0 @@ -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 diff --git a/railties/test/mocks/stubbed_breakpoint.rb b/railties/test/mocks/stubbed_breakpoint.rb deleted file mode 100644 index 15558282b1..0000000000 --- a/railties/test/mocks/stubbed_breakpoint.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Breakpoint -end diff --git a/railties/test/mocks/stubbed_kernel.rb b/railties/test/mocks/stubbed_kernel.rb deleted file mode 100644 index ef9864867c..0000000000 --- a/railties/test/mocks/stubbed_kernel.rb +++ /dev/null @@ -1,5 +0,0 @@ -module Kernel - def exec(*args) - true - end -end diff --git a/railties/test/rails_generator_test.rb b/railties/test/rails_generator_test.rb index eac3a3d176..51d02312bc 100644 --- a/railties/test/rails_generator_test.rb +++ b/railties/test/rails_generator_test.rb @@ -37,7 +37,7 @@ require 'rails_generator' 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 -- cgit v1.2.3