aboutsummaryrefslogtreecommitdiffstats
path: root/railties
diff options
context:
space:
mode:
Diffstat (limited to 'railties')
-rw-r--r--railties/lib/fcgi_handler.rb130
-rw-r--r--railties/test/abstract_unit.rb17
-rw-r--r--railties/test/fcgi_dispatcher_test.rb296
-rw-r--r--railties/test/mocks/fcgi.rb15
-rw-r--r--railties/test/mocks/stubbed_breakpoint.rb2
-rw-r--r--railties/test/mocks/stubbed_kernel.rb5
-rw-r--r--railties/test/rails_generator_test.rb2
7 files changed, 224 insertions, 243 deletions
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