Defer reloading until #close is called on request body #137

Closed
wants to merge 3 commits into
from
@@ -53,6 +53,7 @@ module ActionDispatch
autoload :Flash
autoload :Head
autoload :ParamsParser
+ autoload :Reloader
autoload :RemoteIp
autoload :Rescue
autoload :ShowExceptions
@@ -1,32 +1,14 @@
module ActionDispatch
# Provide callbacks to be executed before and after the request dispatch.
- #
- # It also provides a to_prepare callback, which is performed in all requests
- # in development by only once in production and notification callback for async
- # operations.
- #
class Callbacks
include ActiveSupport::Callbacks
define_callbacks :call, :rescuable => true
- define_callbacks :prepare, :scope => :name
- # Add a preparation callback. Preparation callbacks are run before every
- # request in development mode, and before the first request in production mode.
- #
- # If a symbol with a block is given, the symbol is used as an identifier.
- # That allows to_prepare to 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 self.to_prepare(*args, &block)
- first_arg = args.first
- if first_arg.is_a?(Symbol) && block_given?
- remove_method :"__#{first_arg}" if method_defined?(:"__#{first_arg}")
- define_method :"__#{first_arg}", &block
- set_callback(:prepare, :"__#{first_arg}")
- else
- set_callback(:prepare, *args, &block)
- end
+ ActiveSupport::Deprecation.warn "ActionDispatch::Callbacks.to_prepare is deprecated. " <<
+ "Please use ActionDispatch::Reloader.to_prepare instead."
+ ActionDispatch::Reloader.to_prepare(*args, &block)
end
def self.before(*args, &block)
@@ -37,14 +19,13 @@ def self.after(*args, &block)
set_callback(:call, :after, *args, &block)
end
- def initialize(app, prepare_each_request = false)
- @app, @prepare_each_request = app, prepare_each_request
- _run_prepare_callbacks
+ def initialize(app, unused = nil)
+ ActiveSupport::Deprecation.warn "Passing a second argument to ActionDispatch::Callbacks.new is deprecated." unless unused.nil?
+ @app = app
end
def call(env)
_run_call_callbacks do
- _run_prepare_callbacks if @prepare_each_request
@app.call(env)
end
end
@@ -0,0 +1,82 @@
+module ActionDispatch
+ # ActionDispatch::Reloader provides to_prepare and to_cleanup callbacks.
+ # These are analogs of ActionDispatch::Callback's before and after
+ # callbacks, with the difference that to_cleanup is not called until the
+ # request is fully complete -- that is, after #close has been called on
+ # the request body. This is important for streaming responses such as the
+ # following:
+ #
+ # self.response_body = lambda { |response, output|
+ # # code here which refers to application models
+ # }
+ #
+ # Cleanup callbacks will not be called until after the response_body lambda
+ # is evaluated, ensuring that it can refer to application models and other
+ # classes before they are unloaded.
+ #
+ # By default, ActionDispatch::Reloader is included in the middleware stack
+ # only in the development environment.
+ #
+ class Reloader
+ include ActiveSupport::Callbacks
+
+ define_callbacks :prepare, :scope => :name
+ define_callbacks :cleanup, :scope => :name
+
+ # Add a preparation callback. Preparation callbacks are run before each
+ # request.
+ #
+ # If a symbol with a block is given, the symbol is used as an identifier.
+ # That allows to_prepare to 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 self.to_prepare(*args, &block)
+ first_arg = args.first
+ if first_arg.is_a?(Symbol) && block_given?
+ remove_method :"__#{first_arg}" if method_defined?(:"__#{first_arg}")
+ define_method :"__#{first_arg}", &block
+ set_callback(:prepare, :"__#{first_arg}")
+ else
+ set_callback(:prepare, *args, &block)
+ end
+ end
+
+ # Add a cleanup callback. Cleanup callbacks are run after each request is
+ # complete (after #close is called on the response body).
+ def self.to_cleanup(&block)
+ set_callback(:cleanup, &block)
+ end
+
+ def self.prepare!
+ new(nil).send(:_run_prepare_callbacks)
+ end
+
+ def self.cleanup!
+ new(nil).send(:_run_cleanup_callbacks)
+ end
+
+ def self.reload!
+ prepare!
+ cleanup!
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ module CleanupOnClose
+ def close
+ super if defined?(super)
+ ensure
+ ActionDispatch::Reloader.cleanup!
+ end
+ end
+
+ def call(env)
+ _run_prepare_callbacks
+ response = @app.call(env)
+ response[2].extend(CleanupOnClose)
+ response
+ end
+ end
+end
@@ -1,6 +1,6 @@
require 'abstract_unit'
-class DispatcherTest < Test::Unit::TestCase
+class DispatcherTest < ActiveSupport::TestCase
class Foo
cattr_accessor :a, :b
end
@@ -13,65 +13,9 @@ def call(env)
def setup
Foo.a, Foo.b = 0, 0
- ActionDispatch::Callbacks.reset_callbacks(:prepare)
ActionDispatch::Callbacks.reset_callbacks(:call)
end
- def test_prepare_callbacks_with_cache_classes
- a = b = c = nil
- ActionDispatch::Callbacks.to_prepare { |*args| a = b = c = 1 }
- ActionDispatch::Callbacks.to_prepare { |*args| b = c = 2 }
- ActionDispatch::Callbacks.to_prepare { |*args| c = 3 }
-
- # Ensure to_prepare callbacks are not run when defined
- assert_nil a || b || c
-
- # Run callbacks
- dispatch
-
- assert_equal 1, a
- assert_equal 2, b
- assert_equal 3, c
-
- # Make sure they are only run once
- a = b = c = nil
- dispatch
- assert_nil a || b || c
- end
-
- def test_prepare_callbacks_without_cache_classes
- a = b = c = nil
- ActionDispatch::Callbacks.to_prepare { |*args| a = b = c = 1 }
- ActionDispatch::Callbacks.to_prepare { |*args| b = c = 2 }
- ActionDispatch::Callbacks.to_prepare { |*args| c = 3 }
-
- # Ensure to_prepare callbacks are not run when defined
- assert_nil a || b || c
-
- # Run callbacks
- dispatch(false)
-
- assert_equal 1, a
- assert_equal 2, b
- assert_equal 3, c
-
- # Make sure they are run again
- a = b = c = nil
- dispatch(false)
- assert_equal 1, a
- assert_equal 2, b
- assert_equal 3, c
- end
-
- def test_to_prepare_with_identifier_replaces
- ActionDispatch::Callbacks.to_prepare(:unique_id) { |*args| Foo.a, Foo.b = 1, 1 }
- ActionDispatch::Callbacks.to_prepare(:unique_id) { |*args| Foo.a = 2 }
-
- dispatch
- assert_equal 2, Foo.a
- assert_equal 0, Foo.b
- end
-
def test_before_and_after_callbacks
ActionDispatch::Callbacks.before { |*args| Foo.a += 1; Foo.b += 1 }
ActionDispatch::Callbacks.after { |*args| Foo.a += 1; Foo.b += 1 }
@@ -85,10 +29,20 @@ def test_before_and_after_callbacks
assert_equal 4, Foo.b
end
+ def test_to_prepare_deprecation
+ prepared = false
+ assert_deprecated do
+ ActionDispatch::Callbacks.to_prepare { prepared = true }
+ end
+
+ ActionDispatch::Reloader.prepare!
+ assert prepared
+ end
+
private
- def dispatch(cache_classes = true, &block)
- @dispatcher ||= ActionDispatch::Callbacks.new(block || DummyApp.new, !cache_classes)
+ def dispatch(&block)
+ @dispatcher ||= ActionDispatch::Callbacks.new(block || DummyApp.new)
@dispatcher.call({'rack.input' => StringIO.new('')})
end
@@ -0,0 +1,139 @@
+require 'abstract_unit'
+
+class ReloaderTest < Test::Unit::TestCase
+ Reloader = ActionDispatch::Reloader
+
+ def test_prepare_callbacks
+ a = b = c = nil
+ Reloader.to_prepare { |*args| a = b = c = 1 }
+ Reloader.to_prepare { |*args| b = c = 2 }
+ Reloader.to_prepare { |*args| c = 3 }
+
+ # Ensure to_prepare callbacks are not run when defined
+ assert_nil a || b || c
+
+ # Run callbacks
+ call_and_return_body
+
+ assert_equal 1, a
+ assert_equal 2, b
+ assert_equal 3, c
+ end
+
+ def test_to_prepare_with_identifier_replaces
+ a = b = 0
+ Reloader.to_prepare(:unique_id) { |*args| a = b = 1 }
+ Reloader.to_prepare(:unique_id) { |*args| a = 2 }
+
+ call_and_return_body
+ assert_equal 2, a
+ assert_equal 0, b
+ end
+
+ class MyBody < Array
+ def initialize(&block)
+ @on_close = block
+ end
+
+ def foo
+ "foo"
+ end
+
+ def bar
+ "bar"
+ end
+
+ def close
+ @on_close.call if @on_close
+ end
+ end
+
+ def test_returned_body_object_always_responds_to_close
+ body = call_and_return_body
+ assert body.respond_to?(:close)
+ end
+
+ def test_returned_body_object_behaves_like_underlying_object
+ body = call_and_return_body do
+ b = MyBody.new
+ b << "hello"
+ b << "world"
+ [200, { "Content-Type" => "text/html" }, b]
+ end
+ assert_equal 2, body.size
+ assert_equal "hello", body[0]
+ assert_equal "world", body[1]
+ assert_equal "foo", body.foo
+ assert_equal "bar", body.bar
+ end
+
+ def test_it_calls_close_on_underlying_object_when_close_is_called_on_body
+ close_called = false
+ body = call_and_return_body do
+ b = MyBody.new do
+ close_called = true
+ end
+ [200, { "Content-Type" => "text/html" }, b]
+ end
+ body.close
+ assert close_called
+ end
+
+ def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object
+ body = call_and_return_body do
+ [200, { "Content-Type" => "text/html" }, MyBody.new]
+ end
+ assert body.respond_to?(:size)
+ assert body.respond_to?(:each)
+ assert body.respond_to?(:foo)
+ assert body.respond_to?(:bar)
+ end
+
+ def test_cleanup_callbacks_are_called_when_body_is_closed
+ cleaned = false
+ Reloader.to_cleanup { cleaned = true }
+
+ body = call_and_return_body
+ assert !cleaned
+
+ body.close
+ assert cleaned
+ end
+
+ def test_prepare_callbacks_arent_called_when_body_is_closed
+ prepared = false
+ Reloader.to_prepare { prepared = true }
+
+ body = call_and_return_body
+ prepared = false
+
+ body.close
+ assert !prepared
+ end
+
+ def test_manual_reloading
+ prepared = cleaned = false
+ Reloader.to_prepare { prepared = true }
+ Reloader.to_cleanup { cleaned = true }
+
+ Reloader.prepare!
+ assert prepared
+ assert !cleaned
+
+ prepared = cleaned = false
+ Reloader.cleanup!
+ assert !prepared
+ assert cleaned
+
+ prepared = cleaned = false
+ Reloader.reload!
+ assert prepared
+ assert cleaned
+ end
+
+ private
+ def call_and_return_body(&block)
+ @reloader ||= Reloader.new(block || proc {[200, {}, 'response']})
+ @reloader.call({'rack.input' => StringIO.new('')})[2]
+ end
+end
Oops, something went wrong.