Changeset 7593
- Timestamp:
- 09/23/07 11:20:25 (2 years ago)
- Files:
-
- trunk/railties/lib/fcgi_handler.rb (modified) (6 diffs)
- trunk/railties/test/abstract_unit.rb (modified) (2 diffs)
- trunk/railties/test/fcgi_dispatcher_test.rb (modified) (8 diffs)
- trunk/railties/test/mocks/fcgi.rb (deleted)
- trunk/railties/test/mocks/stubbed_breakpoint.rb (deleted)
- trunk/railties/test/mocks/stubbed_kernel.rb (deleted)
- trunk/railties/test/rails_generator_test.rb (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/railties/lib/fcgi_handler.rb
r7095 r7593 42 42 # Start error timestamp at 11 seconds ago. 43 43 @last_error_on = Time.now - 11 44 45 dispatcher_log :info, "starting"46 44 end 47 45 48 46 def process!(provider = FCGI) 49 # Make a note of $" so we can safely reload this instance. 50 mark! 51 52 run_gc! if gc_request_period 53 54 process_each_request!(provider) 55 56 GC.enable 57 dispatcher_log :info, "terminated gracefully" 58 59 rescue SystemExit => exit_error 60 dispatcher_log :info, "terminated by explicit exit" 61 62 rescue Exception => fcgi_error # FCGI errors 63 # retry on errors that would otherwise have terminated the FCGI process, 64 # but only if they occur more than 10 seconds apart. 65 if !(SignalException === fcgi_error) && Time.now - @last_error_on > 10 66 @last_error_on = Time.now 67 dispatcher_error(fcgi_error, "almost killed by this error") 68 retry 47 mark_features! 48 49 dispatcher_log :info, 'starting' 50 process_each_request provider 51 dispatcher_log :info, 'stopping gracefully' 52 53 rescue Exception => error 54 case error 55 when SystemExit 56 dispatcher_log :info, 'stopping after explicit exit' 57 when SignalException 58 dispatcher_error error, 'stopping after unhandled signal' 69 59 else 70 dispatcher_error(fcgi_error, "killed by this error") 60 # Retry if exceptions occur more than 10 seconds apart. 61 if Time.now - @last_error_on > 10 62 @last_error_on = Time.now 63 dispatcher_error error, 'retrying after unhandled exception' 64 retry 65 else 66 dispatcher_error error, 'stopping after unhandled exception within 10 seconds of the last' 67 end 71 68 end 72 69 end … … 74 71 75 72 protected 76 def logger 77 @logger ||= Logger.new(@log_file_path) 78 end 79 80 def dispatcher_log(level, msg) 81 time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S") 82 logger.send(level, "[#{time_str} :: #{$$}] #{msg}") 83 rescue Exception => log_error # Logger errors 84 STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n" 85 STDERR << " #{log_error.class}: #{log_error.message}\n" 86 end 87 88 def dispatcher_error(e, msg = "") 89 error_message = 90 "Dispatcher failed to catch: #{e} (#{e.class})\n" + 91 " #{e.backtrace.join("\n ")}\n#{msg}" 92 dispatcher_log(:error, error_message) 93 end 94 95 def install_signal_handlers 96 GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) } 97 end 98 99 def install_signal_handler(signal, handler = nil) 100 handler ||= method("#{SIGNALS[signal]}_handler").to_proc 101 trap(signal, handler) 102 rescue ArgumentError 103 dispatcher_log :warn, "Ignoring unsupported signal #{signal}." 104 end 105 106 def with_signal_handler(signal) 107 install_signal_handler(signal) 108 yield 109 ensure 110 install_signal_handler(signal, 'DEFAULT') 111 end 112 113 def exit_now_handler(signal) 114 dispatcher_log :info, "asked to terminate immediately" 115 exit 116 end 117 118 def exit_handler(signal) 119 dispatcher_log :info, "asked to terminate ASAP" 120 @when_ready = :exit 121 end 122 123 def reload_handler(signal) 124 dispatcher_log :info, "asked to reload ASAP" 125 @when_ready = :reload 126 end 127 128 def restart_handler(signal) 129 dispatcher_log :info, "asked to restart ASAP" 130 @when_ready = :restart 131 end 132 133 def process_each_request!(provider) 73 def process_each_request(provider) 134 74 cgi = nil 75 135 76 provider.each_cgi do |cgi| 136 with_signal_handler 'USR1' do 137 process_request(cgi) 138 end 77 process_request(cgi) 139 78 140 79 case when_ready … … 148 87 break 149 88 end 150 151 gc_countdown152 89 end 153 90 rescue SignalException => signal 154 91 raise unless signal.message == 'SIGUSR1' 155 close_connection(cgi) if cgi92 close_connection(cgi) 156 93 end 157 94 158 95 def process_request(cgi) 159 Dispatcher.dispatch(cgi) 160 rescue Exception => e # errors from CGI dispatch 161 raise if SignalException === e 162 dispatcher_error(e) 96 @when_ready = nil 97 gc_countdown 98 99 with_signal_handler 'USR1' do 100 begin 101 Dispatcher.dispatch(cgi) 102 rescue SignalException, SystemExit 103 raise 104 rescue Exception => error 105 dispatcher_error error, 'unhandled dispatch error' 106 end 107 end 108 end 109 110 def logger 111 @logger ||= Logger.new(@log_file_path) 112 end 113 114 def dispatcher_log(level, msg) 115 time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S") 116 logger.send(level, "[#{time_str} :: #{$$}] #{msg}") 117 rescue Exception => log_error # Logger errors 118 STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n" 119 STDERR << " #{log_error.class}: #{log_error.message}\n" 120 end 121 122 def dispatcher_error(e, msg = "") 123 error_message = 124 "Dispatcher failed to catch: #{e} (#{e.class})\n" + 125 " #{e.backtrace.join("\n ")}\n#{msg}" 126 dispatcher_log(:error, error_message) 127 end 128 129 def install_signal_handlers 130 GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) } 131 end 132 133 def install_signal_handler(signal, handler = nil) 134 if SIGNALS.include?(signal) && self.class.method_defined?(name = "#{SIGNALS[signal]}_handler") 135 handler ||= method(name).to_proc 136 trap(signal, handler) 137 else 138 dispatcher_log :warn, "Ignoring unsupported signal #{signal}." 139 end 140 end 141 142 def with_signal_handler(signal) 143 install_signal_handler(signal) 144 yield 145 ensure 146 install_signal_handler(signal, 'DEFAULT') 147 end 148 149 def exit_now_handler(signal) 150 dispatcher_log :info, "asked to stop immediately" 151 exit 152 end 153 154 def exit_handler(signal) 155 dispatcher_log :info, "asked to stop ASAP" 156 @when_ready = :exit 157 end 158 159 def reload_handler(signal) 160 dispatcher_log :info, "asked to reload ASAP" 161 @when_ready = :reload 162 end 163 164 def restart_handler(signal) 165 dispatcher_log :info, "asked to restart ASAP" 166 @when_ready = :restart 163 167 end 164 168 … … 185 189 end 186 190 187 def mark! 191 # Make a note of $" so we can safely reload this instance. 192 def mark_features! 188 193 @features = $".clone 189 194 end … … 202 207 def gc_countdown 203 208 if gc_request_period 209 @gc_request_countdown ||= gc_request_period 204 210 @gc_request_countdown -= 1 205 211 run_gc! if @gc_request_countdown <= 0 … … 208 214 209 215 def close_connection(cgi) 210 cgi.instance_variable_get("@request").finish 216 cgi.instance_variable_get("@request").finish if cgi 211 217 end 212 218 end trunk/railties/test/abstract_unit.rb
r7591 r7593 5 5 6 6 require 'test/unit' 7 require 'stringio' 7 8 require 'active_support' 8 9 if defined?(RAILS_ROOT)10 RAILS_ROOT.replace File.dirname(__FILE__)11 else12 RAILS_ROOT = File.dirname(__FILE__)13 end14 15 class Test::Unit::TestCase16 # Add stuff here if you need it17 end18 9 19 10 # Wrap tests that use Mocha and skip if unavailable. … … 26 17 $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again." 27 18 end 19 20 if defined?(RAILS_ROOT) 21 RAILS_ROOT.replace File.dirname(__FILE__) 22 else 23 RAILS_ROOT = File.dirname(__FILE__) 24 end trunk/railties/test/fcgi_dispatcher_test.rb
r7552 r7593 1 1 require File.dirname(__FILE__) + "/abstract_unit" 2 2 3 begin # rescue LoadError 4 5 require_library_or_gem 'mocha' 6 7 $:.unshift File.dirname(__FILE__) + "/mocks" 8 9 require 'stringio' 10 11 # Stubs 3 uses_mocha 'fcgi dispatcher tests' do 4 12 5 require 'fcgi_handler' 13 require 'routes' 14 require 'stubbed_kernel' 15 16 class RailsFCGIHandler 17 attr_reader :exit_code 18 attr_reader :reloaded 19 attr_accessor :thread 20 attr_reader :gc_runs 21 22 def trap(signal, handler, &block) 23 handler ||= block 24 (@signal_handlers ||= Hash.new)[signal] = handler 25 end 26 27 def exit(code=0) 28 @exit_code = code 29 (thread || Thread.current).exit 30 end 31 32 def send_signal(which) 33 @signal_handlers[which].call(which) 34 end 35 36 alias_method :old_run_gc!, :run_gc! 37 def run_gc! 38 @gc_runs ||= 0 39 @gc_runs += 1 40 old_run_gc! 41 end 42 end 6 7 module ActionController; module Routing; module Routes; end end end 43 8 44 9 class RailsFCGIHandlerTest < Test::Unit::TestCase … … 46 11 @log = StringIO.new 47 12 @handler = RailsFCGIHandler.new(@log) 48 FCGI.time_to_sleep = nil49 FCGI.raise_exception = nil50 Dispatcher.time_to_sleep = nil51 Dispatcher.raise_exception = nil52 13 end 53 14 54 15 def test_process_restart 55 @handler.stubs(:when_ready).returns(:restart) 56 57 @handler.expects(:close_connection) 16 cgi = mock 17 FCGI.stubs(:each_cgi).yields(cgi) 18 19 @handler.expects(:process_request).once 20 @handler.expects(:dispatcher_error).never 21 22 @handler.expects(:when_ready).returns(:restart) 23 @handler.expects(:close_connection).with(cgi) 24 @handler.expects(:reload!).never 58 25 @handler.expects(:restart!) 59 @handler.process! 60 end 61 26 27 @handler.process! 28 end 29 62 30 def test_process_exit 63 @handler.stubs(:when_ready).returns(:exit) 64 65 @handler.expects(:close_connection) 66 @handler.process! 67 end 68 31 cgi = mock 32 FCGI.stubs(:each_cgi).yields(cgi) 33 34 @handler.expects(:process_request).once 35 @handler.expects(:dispatcher_error).never 36 37 @handler.expects(:when_ready).returns(:exit) 38 @handler.expects(:close_connection).with(cgi) 39 @handler.expects(:reload!).never 40 @handler.expects(:restart!).never 41 42 @handler.process! 43 end 44 69 45 def test_process_with_system_exit_exception 70 @handler.stubs(:process_request).raises(SystemExit) 71 72 @handler.expects(:dispatcher_log).with(:info, "terminated by explicit exit") 73 @handler.process! 74 end 75 46 cgi = mock 47 FCGI.stubs(:each_cgi).yields(cgi) 48 49 @handler.expects(:process_request).once.raises(SystemExit) 50 @handler.stubs(:dispatcher_log) 51 @handler.expects(:dispatcher_log).with(:info, regexp_matches(/^stopping/)) 52 @handler.expects(:dispatcher_error).never 53 54 @handler.expects(:when_ready).never 55 @handler.expects(:close_connection).never 56 @handler.expects(:reload!).never 57 @handler.expects(:restart!).never 58 59 @handler.process! 60 end 61 76 62 def test_restart_handler 77 63 @handler.expects(:dispatcher_log).with(:info, "asked to restart ASAP") 78 64 79 65 @handler.send(:restart_handler, nil) 80 66 assert_equal :restart, @handler.when_ready 81 67 end 82 68 83 69 def test_install_signal_handler_should_log_on_bad_signal 84 70 @handler.stubs(:trap).raises(ArgumentError) … … 87 73 @handler.send(:install_signal_handler, "CHEESECAKE", nil) 88 74 end 89 75 90 76 def test_reload 91 77 @handler.expects(:restore!) … … 95 81 assert_nil @handler.when_ready 96 82 end 97 98 83 84 99 85 def test_reload_runs_gc_when_gc_request_period_set 100 86 @handler.expects(:run_gc!) … … 104 90 @handler.send(:reload!) 105 91 end 106 92 107 93 def test_reload_doesnt_run_gc_if_gc_request_period_isnt_set 108 94 @handler.expects(:run_gc!).never … … 111 97 @handler.send(:reload!) 112 98 end 113 99 114 100 def test_restart! 115 101 @handler.expects(:dispatcher_log).with(:info, "restarted") 116 assert_equal true, @handler.send(:restart!), "Exec wasn't run" 117 end 118 102 @handler.expects(:exec).returns('restarted') 103 assert_equal 'restarted', @handler.send(:restart!) 104 end 105 119 106 def test_restore! 120 107 $".expects(:replace) … … 125 112 126 113 def test_uninterrupted_processing 127 @handler.process! 128 assert_nil @handler.exit_code 129 assert_nil @handler.when_ready 114 cgi = mock 115 FCGI.expects(:each_cgi).yields(cgi) 116 @handler.expects(:process_request).with(cgi) 117 118 @handler.process! 119 120 assert_nil @handler.when_ready 121 end 122 end 123 124 125 class RailsFCGIHandlerSignalsTest < Test::Unit::TestCase 126 def setup 127 @log = StringIO.new 128 @handler = RailsFCGIHandler.new(@log) 130 129 end 131 130 132 131 def test_interrupted_via_HUP_when_not_in_request 133 @handler.expects(:reload!) 134 FCGI.time_to_sleep = 1 135 @handler.thread = Thread.new { @handler.process! } 136 sleep 0.1 # let the thread get started 137 @handler.send_signal("HUP") 138 @handler.thread.join 139 assert_nil @handler.exit_code 132 cgi = mock 133 FCGI.expects(:each_cgi).once.yields(cgi) 134 @handler.expects(:gc_countdown).returns { Process.kill 'HUP', $$ } 135 136 @handler.expects(:reload!).once 137 @handler.expects(:close_connection).never 138 @handler.expects(:exit).never 139 140 @handler.process! 140 141 assert_equal :reload, @handler.when_ready 141 142 end 142 143 143 144 def test_interrupted_via_HUP_when_in_request 144 @handler.expects(:reload!) 145 146 Dispatcher.time_to_sleep = 1 147 @handler.thread = Thread.new { @handler.process! } 148 sleep 0.1 # let the thread get started 149 @handler.send_signal("HUP") 150 @handler.thread.join 151 assert_nil @handler.exit_code 145 cgi = mock 146 FCGI.expects(:each_cgi).once.yields(cgi) 147 Dispatcher.expects(:dispatch).with(cgi).returns { Process.kill 'HUP', $$ } 148 149 @handler.expects(:reload!).once 150 @handler.expects(:close_connection).never 151 @handler.expects(:exit).never 152 153 @handler.process! 152 154 assert_equal :reload, @handler.when_ready 153 155 end 154 156 155 157 def test_interrupted_via_USR1_when_not_in_request 156 FCGI.time_to_sleep = 1 157 @handler.thread = Thread.new { @handler.process! } 158 sleep 0.1 # let the thread get started 159 @handler.send_signal("USR1") 160 @handler.thread.join 161 assert_nil @handler.exit_code 158 cgi = mock 159 FCGI.expects(:each_cgi).once.yields(cgi) 160 @handler.expects(:gc_countdown).returns { Process.kill 'USR1', $$ } 161 @handler.expects(:exit_handler).never 162 163 @handler.expects(:reload!).never 164 @handler.expects(:close_connection).with(cgi).once 165 @handler.expects(:exit).never 166 167 @handler.process! 168 assert_nil @handler.when_ready 169 end 170 171 def test_interrupted_via_USR1_when_in_request 172 cgi = mock 173 FCGI.expects(:each_cgi).once.yields(cgi) 174 Dispatcher.expects(:dispatch).with(cgi).returns { Process.kill 'USR1', $$ } 175 176 @handler.expects(:reload!).never 177 @handler.expects(:close_connection).with(cgi).once 178 @handler.expects(:exit).never 179 180 @handler.process! 162 181 assert_equal :exit, @handler.when_ready 163 182 end 164 183 165 def test_interrupted_via_USR1_when_in_request166 Dispatcher.time_to_sleep = 1167 @handler.thread = Thread.new { @handler.process! }168 sleep 0.1 # let the thread get started169 @handler.send_signal("USR1")170 @handler.thread.join171 assert_nil @handler.exit_code172 assert_equal :exit, @handler.when_ready173 end174 175 184 def test_interrupted_via_TERM 176 Dispatcher.time_to_sleep = 1 177 @handler.thread = Thread.new { @handler.process! } 178 sleep 0.1 # let the thread get started 179 @handler.send_signal("TERM") 180 @handler.thread.join 181 assert_equal 0, @handler.exit_code 182 assert_nil @handler.when_ready 183 end 184 185 %w(RuntimeError SignalException).each do |exception| 186 define_method("test_#{exception}_in_fcgi") do 187 FCGI.raise_exception = Object.const_get(exception) 188 @handler.process! 189 assert_match %r{Dispatcher failed to catch}, @log.string 190 case exception 191 when "RuntimeError" 192 assert_match %r{almost killed}, @log.string 193 when "SignalException" 194 assert_match %r{^killed}, @log.string 195 end 196 end 197 198 define_method("test_#{exception}_in_dispatcher") do 199 Dispatcher.raise_exception = Object.const_get(exception) 200 @handler.process! 201 assert_match %r{Dispatcher failed to catch}, @log.string 202 case exception 203 when "RuntimeError" 204 assert_no_match %r{killed}, @log.string 205 when "SignalException" 206 assert_match %r{^killed}, @log.string 207 end 208 end 185 cgi = mock 186 FCGI.expects(:each_cgi).once.yields(cgi) 187 Dispatcher.expects(:dispatch).with(cgi).returns { Process.kill 'TERM', $$ } 188 189 @handler.expects(:reload!).never 190 @handler.expects(:close_connection).never 191 192 @handler.process! 193 assert_nil @handler.when_ready 194 end 195 196 def test_runtime_exception_in_fcgi 197 error = RuntimeError.new('foo') 198 FCGI.expects(:each_cgi).times(2).raises(error) 199 @handler.expects(:dispatcher_error).with(error, regexp_matches(/^retrying/)) 200 @handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/)) 201 @handler.process! 202 end 203 204 def test_runtime_error_in_dispatcher 205 cgi = mock 206 error = RuntimeError.new('foo') 207 FCGI.expects(:each_cgi).once.yields(cgi) 208 Dispatcher.expects(:dispatch).once.with(cgi).raises(error) 209 @handler.expects(:dispatcher_error).with(error, regexp_matches(/^unhandled/)) 210 @handler.process! 211 end 212 213 def test_signal_exception_in_fcgi 214 error = SignalException.new('USR2') 215 FCGI.expects(:each_cgi).once.raises(error) 216 @handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/)) 217 @handler.process! 218 end 219 220 def test_signal_exception_in_dispatcher 221 cgi = mock 222 error = SignalException.new('USR2') 223 FCGI.expects(:each_cgi).once.yields(cgi) 224 Dispatcher.expects(:dispatch).once.with(cgi).raises(error) 225 @handler.expects(:dispatcher_error).with(error, regexp_matches(/^stopping/)) 226 @handler.process! 209 227 end 210 228 end 229 211 230 212 231 class RailsFCGIHandlerPeriodicGCTest < Test::Unit::TestCase 213 232 def setup 214 233 @log = StringIO.new 215 FCGI.time_to_sleep = nil216 FCGI.raise_exception = nil217 FCGI.each_cgi_count = nil218 Dispatcher.time_to_sleep = nil219 Dispatcher.raise_exception = nil220 Dispatcher.dispatch_hook = nil221 234 end 222 235 223 236 def teardown 224 FCGI.each_cgi_count = nil225 Dispatcher.dispatch_hook = nil226 237 GC.enable 227 238 end … … 236 247 237 248 def test_periodic_gc 238 Dispatcher.dispatch_hook = lambda do |cgi|239 # When GC is disabled, GC.enable enables and returns true.240 assert_equal true, GC.enable241 GC.disable242 end243 244 249 @handler = RailsFCGIHandler.new(@log, 10) 245 250 assert_equal 10, @handler.gc_request_period 246 FCGI.each_cgi_count = 1 247 @handler.process! 248 assert_equal 1, @handler.gc_runs 249 250 FCGI.each_cgi_count = 10 251 @handler.process! 252 assert_equal 3, @handler.gc_runs 253 254 FCGI.each_cgi_count = 25 255 @handler.process! 256 assert_equal 6, @handler.gc_runs 257 258 assert_nil @handler.exit_code 251 252 cgi = mock 253 FCGI.expects(:each_cgi).times(10).yields(cgi) 254 Dispatcher.expects(:dispatch).times(10).with(cgi) 255 256 @handler.expects(:run_gc!).never 257 9.times { @handler.process! } 258 @handler.expects(:run_gc!).once 259 @handler.process! 260 259 261 assert_nil @handler.when_ready 260 262 end 261 263 end 262 264 263 rescue LoadError => e 264 $stderr.puts "Skipping dispatcher tests. `gem install mocha` and try again. (#{e})" 265 end 265 end # uses_mocha trunk/railties/test/rails_generator_test.rb
r7551 r7593 38 38 39 39 class RailsGeneratorTest < Test::Unit::TestCase 40 BUILTINS = %w(controller integration_test mailer migration model observer plugin resource scaffold session_migration web_service)40 BUILTINS = %w(controller integration_test mailer migration model observer plugin resource scaffold session_migration) 41 41 CAPITALIZED_BUILTINS = BUILTINS.map { |b| b.capitalize } 42 42