Move Railties' Dispatcher to ActionController::Dispatcher, introduce before_ and after_dispatch callbacks, and warm up to non-CGI requests.
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7640 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
6b2226aae9
commit
f08da31a4f
|
@ -1,5 +1,7 @@
|
|||
*SVN*
|
||||
|
||||
* Move Railties' Dispatcher to ActionController::Dispatcher, introduce before_ and after_dispatch callbacks, and warm up to non-CGI requests. [Jeremy Kemper]
|
||||
|
||||
* The tag helper may bypass escaping. [Jeremy Kemper]
|
||||
|
||||
* Cache asset ids. [Jeremy Kemper]
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
module ActionController
|
||||
# Dispatches requests to the appropriate controller and takes care of
|
||||
# reloading the app after each request when Dependencies.load? is true.
|
||||
class Dispatcher
|
||||
class << self
|
||||
# Backward-compatible class method takes CGI-specific args. Deprecated
|
||||
# in favor of Dispatcher.new(output, request, response).dispatch!
|
||||
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
|
||||
new(output).dispatch_cgi(cgi, session_options)
|
||||
end
|
||||
|
||||
# Declare a block to be called before each dispatch.
|
||||
# Run in the order declared.
|
||||
def before_dispatch(*method_names, &block)
|
||||
callbacks[:before].concat method_names
|
||||
callbacks[:before] << block if block_given?
|
||||
end
|
||||
|
||||
# Declare a block to be called after each dispatch.
|
||||
# Run in reverse of the order declared.
|
||||
def after_dispatch(*method_names, &block)
|
||||
callbacks[:after].concat method_names
|
||||
callbacks[:after] << block if block_given?
|
||||
end
|
||||
|
||||
# Add a preparation callback. Preparation callbacks are run before every
|
||||
# request in development mode, and before the first request in production
|
||||
# mode.
|
||||
#
|
||||
# An optional identifier may be supplied for the callback. If provided,
|
||||
# to_prepare may be called again with the same identifier to replace the
|
||||
# existing callback. Passing an identifier is a suggested practice if the
|
||||
# code adding a preparation block may be reloaded.
|
||||
def to_prepare(identifier = nil, &block)
|
||||
# Already registered: update the existing callback
|
||||
if identifier
|
||||
if callback = callbacks[:prepare].assoc(identifier)
|
||||
callback[1] = block
|
||||
else
|
||||
callbacks[:prepare] << [identifier, block]
|
||||
end
|
||||
else
|
||||
callbacks[:prepare] << block
|
||||
end
|
||||
end
|
||||
|
||||
# If the block raises, send status code as a last-ditch response.
|
||||
def failsafe_response(fallback_output, status, originating_exception = nil)
|
||||
yield
|
||||
rescue Exception => exception
|
||||
begin
|
||||
log_failsafe_exception(status, originating_exception || exception)
|
||||
body = failsafe_response_body(status)
|
||||
fallback_output.write "Status: #{status}\r\nContent-Type: text/html\r\n\r\n#{body}"
|
||||
nil
|
||||
rescue Exception => failsafe_error # Logger or IO errors
|
||||
$stderr.puts "Error during failsafe response: #{failsafe_error}"
|
||||
$stderr.puts "(originally #{originating_exception})" if originating_exception
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def failsafe_response_body(status)
|
||||
error_path = "#{error_file_path}/#{status.to_s[0..3]}.html"
|
||||
|
||||
if File.exist?(error_path)
|
||||
File.read(error_path)
|
||||
else
|
||||
"<html><body><h1>#{status}</h1></body></html>"
|
||||
end
|
||||
end
|
||||
|
||||
def log_failsafe_exception(status, exception)
|
||||
message = "/!\ FAILSAFE /!\ #{Time.now}\n Status: #{status}\n"
|
||||
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
|
||||
failsafe_logger.fatal message
|
||||
end
|
||||
|
||||
def failsafe_logger
|
||||
if defined?(::RAILS_DEFAULT_LOGGER) && !::RAILS_DEFAULT_LOGGER.nil?
|
||||
::RAILS_DEFAULT_LOGGER
|
||||
else
|
||||
Logger.new($stderr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cattr_accessor :error_file_path
|
||||
self.error_file_path = "#{::RAILS_ROOT}/public" if defined? ::RAILS_ROOT
|
||||
|
||||
cattr_accessor :callbacks
|
||||
self.callbacks = Hash.new { |h, k| h[k] = [] }
|
||||
|
||||
attr_accessor_with_default :unprepared, true
|
||||
|
||||
|
||||
before_dispatch :reload_application
|
||||
before_dispatch :prepare_application
|
||||
after_dispatch :flush_logger
|
||||
after_dispatch :cleanup_application
|
||||
|
||||
def initialize(output, request = nil, response = nil)
|
||||
@output, @request, @response = output, request, response
|
||||
end
|
||||
|
||||
def dispatch
|
||||
run_callbacks :before
|
||||
handle_request
|
||||
rescue Exception => exception
|
||||
failsafe_rescue exception
|
||||
ensure
|
||||
run_callbacks :after, :reverse_each
|
||||
end
|
||||
|
||||
def dispatch_cgi(cgi, session_options)
|
||||
if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new }
|
||||
@request = CgiRequest.new(cgi, session_options)
|
||||
@response = CgiResponse.new(cgi)
|
||||
dispatch
|
||||
end
|
||||
rescue Exception => exception
|
||||
failsafe_rescue exception
|
||||
end
|
||||
|
||||
def reload_application
|
||||
if Dependencies.load?
|
||||
Routing::Routes.reload
|
||||
self.unprepared = true
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_application(force = false)
|
||||
require_dependency 'application' unless defined?(::ApplicationController)
|
||||
ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)
|
||||
|
||||
if unprepared || force
|
||||
run_callbacks :prepare
|
||||
self.unprepared = false
|
||||
end
|
||||
end
|
||||
|
||||
# Cleanup the application by clearing out loaded classes so they can
|
||||
# be reloaded on the next request without restarting the server.
|
||||
def cleanup_application(force = false)
|
||||
if Dependencies.load? || force
|
||||
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
|
||||
Dependencies.clear
|
||||
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
|
||||
end
|
||||
end
|
||||
|
||||
def flush_logger
|
||||
RAILS_DEFAULT_LOGGER.flush if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:flush)
|
||||
end
|
||||
|
||||
protected
|
||||
def handle_request
|
||||
@controller = Routing::Routes.recognize(@request)
|
||||
@controller.process(@request, @response).out(@output)
|
||||
end
|
||||
|
||||
def run_callbacks(kind, enumerator = :each)
|
||||
callbacks[kind].send(enumerator) do |callback|
|
||||
case callback
|
||||
when Proc; callback.call(self)
|
||||
when String, Symbol; send(callback)
|
||||
when Array; callback[1].call(self)
|
||||
else raise ArgumentError, "Unrecognized callback #{callback.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def failsafe_rescue(exception)
|
||||
self.class.failsafe_response(@output, '500 Internal Server Error', exception) do
|
||||
if @controller ||= defined?(::ApplicationController) ? ::ApplicationController : Base
|
||||
@controller.process_with_exception(@request, @response, exception).out(@output)
|
||||
else
|
||||
raise exception
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,6 +30,7 @@ def uses_mocha(test_name)
|
|||
require 'stubba'
|
||||
end
|
||||
yield
|
||||
rescue LoadError
|
||||
rescue LoadError => load_error
|
||||
raise unless load_error.message =~ /mocha/i
|
||||
$stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
|
||||
end
|
||||
|
|
|
@ -1,31 +1,22 @@
|
|||
require "#{File.dirname(__FILE__)}/abstract_unit"
|
||||
require "#{File.dirname(__FILE__)}/../abstract_unit"
|
||||
|
||||
uses_mocha 'dispatcher tests' do
|
||||
|
||||
$:.unshift File.dirname(__FILE__) + "/../../actionmailer/lib"
|
||||
|
||||
require 'stringio'
|
||||
require 'cgi'
|
||||
|
||||
require 'dispatcher'
|
||||
require 'action_controller'
|
||||
require 'action_mailer'
|
||||
|
||||
require 'action_controller/dispatcher'
|
||||
|
||||
class DispatcherTest < Test::Unit::TestCase
|
||||
Dispatcher = ActionController::Dispatcher
|
||||
|
||||
def setup
|
||||
@output = StringIO.new
|
||||
ENV['REQUEST_METHOD'] = "GET"
|
||||
ENV['REQUEST_METHOD'] = 'GET'
|
||||
|
||||
Dispatcher.send(:preparation_callbacks).clear
|
||||
Dispatcher.send(:preparation_callbacks_run=, false)
|
||||
|
||||
Object.const_set 'ApplicationController', nil
|
||||
Dispatcher.callbacks[:prepare].clear
|
||||
@dispatcher = Dispatcher.new(@output)
|
||||
end
|
||||
|
||||
def teardown
|
||||
ENV['REQUEST_METHOD'] = nil
|
||||
Object.send :remove_const, 'ApplicationController'
|
||||
end
|
||||
|
||||
def test_clears_dependencies_after_dispatch_if_in_loading_mode
|
||||
|
@ -37,7 +28,7 @@ class DispatcherTest < Test::Unit::TestCase
|
|||
dispatch
|
||||
end
|
||||
|
||||
def test_clears_dependencies_after_dispatch_if_not_in_loading_mode
|
||||
def test_leaves_dependencies_after_dispatch_if_not_in_loading_mode
|
||||
Dependencies.stubs(:load?).returns(false)
|
||||
|
||||
ActionController::Routing::Routes.expects(:reload).never
|
||||
|
@ -57,47 +48,50 @@ class DispatcherTest < Test::Unit::TestCase
|
|||
assert_equal "Status: 400 Bad Request\r\nContent-Type: text/html\r\n\r\n<html><body><h1>400 Bad Request</h1></body></html>", @output.string
|
||||
end
|
||||
|
||||
def test_preparation_callbacks
|
||||
ActionController::Routing::Routes.stubs(:reload)
|
||||
def test_reload_application_sets_unprepared_if_loading_dependencies
|
||||
Dependencies.stubs(:load?).returns(false)
|
||||
ActionController::Routing::Routes.expects(:reload).never
|
||||
@dispatcher.unprepared = false
|
||||
@dispatcher.send(:reload_application)
|
||||
assert !@dispatcher.unprepared
|
||||
|
||||
old_mechanism = Dependencies.mechanism
|
||||
|
||||
Dependencies.stubs(:load?).returns(true)
|
||||
ActionController::Routing::Routes.expects(:reload).once
|
||||
@dispatcher.send(:reload_application)
|
||||
assert @dispatcher.unprepared
|
||||
end
|
||||
|
||||
def test_prepare_application_runs_callbacks_if_unprepared
|
||||
a = b = c = nil
|
||||
Dispatcher.to_prepare { a = b = c = 1 }
|
||||
Dispatcher.to_prepare { b = c = 2 }
|
||||
Dispatcher.to_prepare { c = 3 }
|
||||
|
||||
Dispatcher.send :prepare_application
|
||||
|
||||
|
||||
# Skip the callbacks when already prepared.
|
||||
@dispatcher.unprepared = false
|
||||
@dispatcher.send :prepare_application
|
||||
assert_nil a || b || c
|
||||
|
||||
# Perform the callbacks when unprepared.
|
||||
@dispatcher.unprepared = true
|
||||
@dispatcher.send :prepare_application
|
||||
assert_equal 1, a
|
||||
assert_equal 2, b
|
||||
assert_equal 3, c
|
||||
|
||||
# When mechanism is :load, perform the callbacks each request:
|
||||
Dependencies.mechanism = :load
|
||||
a = b = c = nil
|
||||
Dispatcher.send :prepare_application
|
||||
assert_equal 1, a
|
||||
assert_equal 2, b
|
||||
assert_equal 3, c
|
||||
|
||||
|
||||
# But when not :load, make sure they are only run once
|
||||
a = b = c = nil
|
||||
Dependencies.mechanism = :not_load
|
||||
Dispatcher.send :prepare_application
|
||||
assert_equal nil, a || b || c
|
||||
ensure
|
||||
Dependencies.mechanism = old_mechanism
|
||||
@dispatcher.send :prepare_application
|
||||
assert_nil a || b || c
|
||||
end
|
||||
|
||||
def test_to_prepare_with_identifier_replaces
|
||||
ActionController::Routing::Routes.stubs(:reload)
|
||||
|
||||
def test_to_prepare_with_identifier_replaces
|
||||
a = b = nil
|
||||
Dispatcher.to_prepare(:unique_id) { a = b = 1 }
|
||||
Dispatcher.to_prepare(:unique_id) { a = 2 }
|
||||
|
||||
Dispatcher.send :prepare_application
|
||||
|
||||
@dispatcher.unprepared = true
|
||||
@dispatcher.send :prepare_application
|
||||
assert_equal 2, a
|
||||
assert_equal nil, b
|
||||
end
|
||||
|
@ -118,4 +112,4 @@ class DispatcherTest < Test::Unit::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
end # uses_mocha
|
||||
end
|
|
@ -1,5 +1,7 @@
|
|||
*SVN*
|
||||
|
||||
* Moved Dispatcher to ActionController::Dispatcher. [Jeremy Kemper]
|
||||
|
||||
* Changed the default logger from Ruby's own Logger with the clean_logger extensions to ActiveSupport::BufferedLogger for performance reasons [DHH]. (You can change it back with config.logger = Logger.new("/path/to/log", level).)
|
||||
|
||||
* Added a default 422.html page to be rendered when ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved, or ActionController::InvalidAuthenticityToken is raised [DHH]
|
||||
|
|
|
@ -21,6 +21,8 @@ RUBY_FORGE_PROJECT = "rails"
|
|||
RUBY_FORGE_USER = "webster132"
|
||||
|
||||
|
||||
task :default => :test
|
||||
|
||||
## This is required until the regular test task
|
||||
## below passes. It's not ideal, but at least
|
||||
## we can see the failures
|
||||
|
@ -353,4 +355,4 @@ task :release => [ :package ] do
|
|||
rubyforge = RubyForge.new
|
||||
rubyforge.login
|
||||
rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,8 @@ end
|
|||
#reloads the environment
|
||||
def reload!
|
||||
puts "Reloading..."
|
||||
returning Dispatcher.reset_application! do
|
||||
Dispatcher.send :run_preparation_callbacks
|
||||
end
|
||||
dispatcher = ActionController::Dispatcher.new($stdout)
|
||||
dispatcher.cleanup_application(true)
|
||||
dispatcher.prepare_application(true)
|
||||
true
|
||||
end
|
||||
|
|
|
@ -20,162 +20,5 @@
|
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
# This class provides an interface for dispatching a CGI (or CGI-like) request
|
||||
# to the appropriate controller and action. It also takes care of resetting
|
||||
# the environment (when Dependencies.load? is true) after each request.
|
||||
class Dispatcher
|
||||
class << self
|
||||
# Dispatch the given CGI request, using the given session options, and
|
||||
# emitting the output via the given output. If you dispatch with your
|
||||
# own CGI object be sure to handle the exceptions it raises on multipart
|
||||
# requests (EOFError and ArgumentError).
|
||||
def dispatch(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
|
||||
controller = nil
|
||||
if cgi ||= new_cgi(output)
|
||||
request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
|
||||
prepare_application
|
||||
controller = ActionController::Routing::Routes.recognize(request)
|
||||
controller.process(request, response).out(output)
|
||||
end
|
||||
rescue Exception => exception # errors from CGI dispatch
|
||||
failsafe_response(cgi, output, '500 Internal Server Error', exception) do
|
||||
controller ||= (ApplicationController rescue ActionController::Base)
|
||||
controller.process_with_exception(request, response, exception).out(output)
|
||||
end
|
||||
ensure
|
||||
# Do not give a failsafe response here
|
||||
flush_logger
|
||||
reset_after_dispatch
|
||||
end
|
||||
|
||||
# Reset the application by clearing out loaded controllers, views, actions,
|
||||
# mailers, and so forth. This allows them to be loaded again without having
|
||||
# to restart the server (WEBrick, FastCGI, etc.).
|
||||
def reset_application!
|
||||
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
|
||||
|
||||
Dependencies.clear
|
||||
|
||||
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
|
||||
end
|
||||
|
||||
# Add a preparation callback. Preparation callbacks are run before every
|
||||
# request in development mode, and before the first request in production
|
||||
# mode.
|
||||
#
|
||||
# An optional identifier may be supplied for the callback. If provided,
|
||||
# to_prepare may be called again with the same identifier to replace the
|
||||
# existing callback. Passing an identifier is a suggested practice if the
|
||||
# code adding a preparation block may be reloaded.
|
||||
def to_prepare(identifier = nil, &block)
|
||||
unless identifier.nil?
|
||||
callback = preparation_callbacks.detect { |ident, _| ident == identifier }
|
||||
|
||||
if callback # Already registered: update the existing callback
|
||||
callback[-1] = block
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
preparation_callbacks << [identifier, block]
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
private
|
||||
attr_accessor_with_default :preparation_callbacks, []
|
||||
attr_accessor_with_default :preparation_callbacks_run, false
|
||||
|
||||
# CGI.new plus exception handling. CGI#read_multipart raises EOFError
|
||||
# if body.empty? or body.size != Content-Length and raises ArgumentError
|
||||
# if Content-Length is non-integer.
|
||||
def new_cgi(output)
|
||||
failsafe_response(nil, output, '400 Bad Request') { CGI.new }
|
||||
end
|
||||
|
||||
def prepare_application
|
||||
if Dependencies.load?
|
||||
ActionController::Routing::Routes.reload
|
||||
self.preparation_callbacks_run = false
|
||||
end
|
||||
|
||||
require_dependency 'application' unless Object.const_defined?(:ApplicationController)
|
||||
ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)
|
||||
run_preparation_callbacks
|
||||
end
|
||||
|
||||
def reset_after_dispatch
|
||||
reset_application! if Dependencies.load?
|
||||
end
|
||||
|
||||
def run_preparation_callbacks
|
||||
return if preparation_callbacks_run
|
||||
preparation_callbacks.each { |_, callback| callback.call }
|
||||
self.preparation_callbacks_run = true
|
||||
end
|
||||
|
||||
# If the block raises, send status code as a last-ditch response.
|
||||
def failsafe_response(cgi, fallback_output, status, exception = nil)
|
||||
yield
|
||||
rescue Exception
|
||||
begin
|
||||
log_failsafe_exception(cgi, status, exception)
|
||||
|
||||
body = failsafe_response_body(status)
|
||||
if cgi
|
||||
head = { 'status' => status, 'type' => 'text/html' }
|
||||
|
||||
# FIXME: using CGI differently than CGIResponse does breaks
|
||||
# the Mongrel CGI wrapper.
|
||||
if defined?(Mongrel) && cgi.is_a?(Mongrel::CGIWrapper)
|
||||
# FIXME: set a dummy cookie so the Mongrel CGI wrapper will
|
||||
# also consider @output_cookies (used for session cookies.)
|
||||
head['cookie'] = []
|
||||
cgi.header(head)
|
||||
fallback_output << body
|
||||
else
|
||||
cgi.out(head) { body }
|
||||
end
|
||||
else
|
||||
fallback_output.write "Status: #{status}\r\nContent-Type: text/html\r\n\r\n#{body}"
|
||||
end
|
||||
nil
|
||||
rescue Exception # Logger or IO errors
|
||||
end
|
||||
end
|
||||
|
||||
def failsafe_response_body(status)
|
||||
error_path = "#{RAILS_ROOT}/public/#{status[0..3]}.html"
|
||||
|
||||
if File.exists?(error_path)
|
||||
File.read(error_path)
|
||||
else
|
||||
"<html><body><h1>#{status}</h1></body></html>"
|
||||
end
|
||||
end
|
||||
|
||||
def log_failsafe_exception(cgi, status, exception)
|
||||
fell_back = cgi ? 'has cgi' : 'no cgi, fallback ouput'
|
||||
message = "DISPATCHER FAILSAFE RESPONSE (#{fell_back}) #{Time.now}\n Status: #{status}\n"
|
||||
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
|
||||
failsafe_logger.fatal message
|
||||
end
|
||||
|
||||
def failsafe_logger
|
||||
if defined?(RAILS_DEFAULT_LOGGER) && !RAILS_DEFAULT_LOGGER.nil?
|
||||
RAILS_DEFAULT_LOGGER
|
||||
else
|
||||
ActiveSupport::BufferedLogger.new($stderr)
|
||||
end
|
||||
end
|
||||
|
||||
def flush_logger
|
||||
RAILS_DEFAULT_LOGGER.flush if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:flush)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Dispatcher.to_prepare :activerecord_instantiate_observers do
|
||||
ActiveRecord::Base.instantiate_observers
|
||||
end if defined?(ActiveRecord)
|
||||
require 'action_controller/dispatcher'
|
||||
Dispatcher = ActionController::Dispatcher
|
||||
|
|
|
@ -2,6 +2,10 @@ require File.dirname(__FILE__) + '/abstract_unit'
|
|||
|
||||
require 'action_controller' # console_app uses 'action_controller/integration'
|
||||
|
||||
unless defined? ApplicationController
|
||||
class ApplicationController < ActionController::Base; end
|
||||
end
|
||||
|
||||
require 'dispatcher'
|
||||
require 'console_app'
|
||||
|
||||
|
|
Loading…
Reference in New Issue