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:
Jeremy Kemper 2007-09-26 01:24:07 +00:00
parent 6b2226aae9
commit f08da31a4f
9 changed files with 240 additions and 208 deletions

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'