Skip to content
This repository
Browse code

Introduce ActionDispatch::Reloader

Based on the implementation on the 2-3-stable branch, patches by Hongli
Lai <hongli@phusion.nl>, and helpful suggestions from José Valim.

Hongli Lai's patches included locking around the request cycle; this is
now handled by Rack::Lock (https://github.com/rack/rack/issues/issue/87/).

[#2873]

Signed-off-by: José Valim <jose.valim@gmail.com>
  • Loading branch information...
commit 0f7c970e4f1cf0f3bcc01c22a6a3038cb3e34668 1 parent d4f9953
authored November 23, 2010 josevalim committed December 20, 2010
1  actionpack/lib/action_dispatch.rb
@@ -53,6 +53,7 @@ module ActionDispatch
53 53
     autoload :Flash
54 54
     autoload :Head
55 55
     autoload :ParamsParser
  56
+    autoload :Reloader
56 57
     autoload :RemoteIp
57 58
     autoload :Rescue
58 59
     autoload :ShowExceptions
82  actionpack/lib/action_dispatch/middleware/reloader.rb
... ...
@@ -0,0 +1,82 @@
  1
+module ActionDispatch
  2
+  # ActionDispatch::Reloader provides to_prepare and to_cleanup callbacks.
  3
+  # These are analogs of ActionDispatch::Callback's before and after
  4
+  # callbacks, with the difference that to_cleanup is not called until the
  5
+  # request is fully complete -- that is, after #close has been called on
  6
+  # the request body. This is important for streaming responses such as the
  7
+  # following:
  8
+  #
  9
+  #     self.response_body = lambda { |response, output|
  10
+  #       # code here which refers to application models
  11
+  #     }
  12
+  #
  13
+  # Cleanup callbacks will not be called until after the response_body lambda
  14
+  # is evaluated, ensuring that it can refer to application models and other
  15
+  # classes before they are unloaded.
  16
+  #
  17
+  # By default, ActionDispatch::Reloader is included in the middleware stack
  18
+  # only in the development environment.
  19
+  #
  20
+  class Reloader
  21
+    include ActiveSupport::Callbacks
  22
+
  23
+    define_callbacks :prepare, :scope => :name
  24
+    define_callbacks :cleanup, :scope => :name
  25
+
  26
+    # Add a preparation callback. Preparation callbacks are run before each
  27
+    # request.
  28
+    #
  29
+    # If a symbol with a block is given, the symbol is used as an identifier.
  30
+    # That allows to_prepare to be called again with the same identifier to
  31
+    # replace the existing callback. Passing an identifier is a suggested
  32
+    # practice if the code adding a preparation block may be reloaded.
  33
+    def self.to_prepare(*args, &block)
  34
+      first_arg = args.first
  35
+      if first_arg.is_a?(Symbol) && block_given?
  36
+        remove_method :"__#{first_arg}" if method_defined?(:"__#{first_arg}")
  37
+        define_method :"__#{first_arg}", &block
  38
+        set_callback(:prepare, :"__#{first_arg}")
  39
+      else
  40
+        set_callback(:prepare, *args, &block)
  41
+      end
  42
+    end
  43
+
  44
+    # Add a cleanup callback. Cleanup callbacks are run after each request is
  45
+    # complete (after #close is called on the response body).
  46
+    def self.to_cleanup(&block)
  47
+      set_callback(:cleanup, &block)
  48
+    end
  49
+
  50
+    def self.prepare!
  51
+      new(nil).send(:_run_prepare_callbacks)
  52
+    end
  53
+
  54
+    def self.cleanup!
  55
+      new(nil).send(:_run_cleanup_callbacks)
  56
+    end
  57
+
  58
+    def self.reload!
  59
+      prepare!
  60
+      cleanup!
  61
+    end
  62
+
  63
+    def initialize(app)
  64
+      @app = app
  65
+    end
  66
+
  67
+    module CleanupOnClose
  68
+      def close
  69
+        super if defined?(super)
  70
+      ensure
  71
+        ActionDispatch::Reloader.cleanup!
  72
+      end
  73
+    end
  74
+
  75
+    def call(env)
  76
+      _run_prepare_callbacks
  77
+      response = @app.call(env)
  78
+      response[2].extend(CleanupOnClose)
  79
+      response
  80
+    end
  81
+  end
  82
+end
139  actionpack/test/dispatch/reloader_test.rb
... ...
@@ -0,0 +1,139 @@
  1
+require 'abstract_unit'
  2
+
  3
+class ReloaderTest < Test::Unit::TestCase
  4
+  Reloader = ActionDispatch::Reloader
  5
+
  6
+  def test_prepare_callbacks
  7
+    a = b = c = nil
  8
+    Reloader.to_prepare { |*args| a = b = c = 1 }
  9
+    Reloader.to_prepare { |*args| b = c = 2 }
  10
+    Reloader.to_prepare { |*args| c = 3 }
  11
+
  12
+    # Ensure to_prepare callbacks are not run when defined
  13
+    assert_nil a || b || c
  14
+
  15
+    # Run callbacks
  16
+    call_and_return_body
  17
+
  18
+    assert_equal 1, a
  19
+    assert_equal 2, b
  20
+    assert_equal 3, c
  21
+  end
  22
+
  23
+  def test_to_prepare_with_identifier_replaces
  24
+    a = b = 0
  25
+    Reloader.to_prepare(:unique_id) { |*args| a = b = 1 }
  26
+    Reloader.to_prepare(:unique_id) { |*args| a = 2 }
  27
+
  28
+    call_and_return_body
  29
+    assert_equal 2, a
  30
+    assert_equal 0, b
  31
+  end
  32
+
  33
+  class MyBody < Array
  34
+    def initialize(&block)
  35
+      @on_close = block
  36
+    end
  37
+
  38
+    def foo
  39
+      "foo"
  40
+    end
  41
+
  42
+    def bar
  43
+      "bar"
  44
+    end
  45
+
  46
+    def close
  47
+      @on_close.call if @on_close
  48
+    end
  49
+  end
  50
+
  51
+  def test_returned_body_object_always_responds_to_close
  52
+    body = call_and_return_body
  53
+    assert body.respond_to?(:close)
  54
+  end
  55
+
  56
+  def test_returned_body_object_behaves_like_underlying_object
  57
+    body = call_and_return_body do
  58
+      b = MyBody.new
  59
+      b << "hello"
  60
+      b << "world"
  61
+      [200, { "Content-Type" => "text/html" }, b]
  62
+    end
  63
+    assert_equal 2, body.size
  64
+    assert_equal "hello", body[0]
  65
+    assert_equal "world", body[1]
  66
+    assert_equal "foo", body.foo
  67
+    assert_equal "bar", body.bar
  68
+  end
  69
+
  70
+  def test_it_calls_close_on_underlying_object_when_close_is_called_on_body
  71
+    close_called = false
  72
+    body = call_and_return_body do
  73
+      b = MyBody.new do
  74
+        close_called = true
  75
+      end
  76
+      [200, { "Content-Type" => "text/html" }, b]
  77
+    end
  78
+    body.close
  79
+    assert close_called
  80
+  end
  81
+
  82
+  def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object
  83
+    body = call_and_return_body do
  84
+      [200, { "Content-Type" => "text/html" }, MyBody.new]
  85
+    end
  86
+    assert body.respond_to?(:size)
  87
+    assert body.respond_to?(:each)
  88
+    assert body.respond_to?(:foo)
  89
+    assert body.respond_to?(:bar)
  90
+  end
  91
+
  92
+  def test_cleanup_callbacks_are_called_when_body_is_closed
  93
+    cleaned = false
  94
+    Reloader.to_cleanup { cleaned = true }
  95
+
  96
+    body = call_and_return_body
  97
+    assert !cleaned
  98
+
  99
+    body.close
  100
+    assert cleaned
  101
+  end
  102
+
  103
+  def test_prepare_callbacks_arent_called_when_body_is_closed
  104
+    prepared = false
  105
+    Reloader.to_prepare { prepared = true }
  106
+
  107
+    body = call_and_return_body
  108
+    prepared = false
  109
+
  110
+    body.close
  111
+    assert !prepared
  112
+  end
  113
+
  114
+  def test_manual_reloading
  115
+    prepared = cleaned = false
  116
+    Reloader.to_prepare { prepared = true }
  117
+    Reloader.to_cleanup { cleaned  = true }
  118
+
  119
+    Reloader.prepare!
  120
+    assert prepared
  121
+    assert !cleaned
  122
+
  123
+    prepared = cleaned = false
  124
+    Reloader.cleanup!
  125
+    assert !prepared
  126
+    assert cleaned
  127
+
  128
+    prepared = cleaned = false
  129
+    Reloader.reload!
  130
+    assert prepared
  131
+    assert cleaned
  132
+  end
  133
+
  134
+  private
  135
+    def call_and_return_body(&block)
  136
+      @reloader ||= Reloader.new(block || proc {[200, {}, 'response']})
  137
+      @reloader.call({'rack.input' => StringIO.new('')})[2]
  138
+    end
  139
+end
1  railties/lib/rails/application.rb
@@ -156,6 +156,7 @@ def default_middleware_stack
156 156
         middleware.use ::ActionDispatch::ShowExceptions, config.consider_all_requests_local if config.action_dispatch.show_exceptions
157 157
         middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
158 158
         middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
  159
+        middleware.use ::ActionDispatch::Reloader unless config.cache_classes
159 160
         middleware.use ::ActionDispatch::Callbacks, !config.cache_classes
160 161
         middleware.use ::ActionDispatch::Cookies
161 162
 
7  railties/test/application/middleware_test.rb
@@ -27,6 +27,7 @@ def app
27 27
         "ActionDispatch::ShowExceptions",
28 28
         "ActionDispatch::RemoteIp",
29 29
         "Rack::Sendfile",
  30
+        "ActionDispatch::Reloader",
30 31
         "ActionDispatch::Callbacks",
31 32
         "ActiveRecord::ConnectionAdapters::ConnectionManagement",
32 33
         "ActiveRecord::QueryCache",
@@ -81,6 +82,12 @@ def app
81 82
       assert !middleware.include?("ActionDispatch::ShowExceptions")
82 83
     end
83 84
 
  85
+    test "removes ActionDispatch::Reloader if cache_classes is true" do
  86
+      add_to_config "config.cache_classes = true"
  87
+      boot!
  88
+      assert !middleware.include?("ActionDispatch::Reloader")
  89
+    end
  90
+
84 91
     test "use middleware" do
85 92
       use_frameworks []
86 93
       add_to_config "config.middleware.use Rack::Config"

0 notes on commit 0f7c970

Xavier Noria

This preamble focuses on to_cleanup, I think it would be good to balance it saying something about to_prepare.

Xavier Noria

"before each request" is a bit ambiguous, when exactly? The reader should now which is the situation within the callback, have before filters been run? How much does the callback know about the request context?

Xavier Noria

Devil's advocate again :). Why is this included only in development? There are no use cases for these callbacks in production? A little explanation of the rationale behind this restriction would also be nice I believe.

In addition, the checked bit is not really development mode, but cache_classes.

John Firebaugh

Is this merged to docrails? I can expand the docs a bit.

Xavier Noria

Excellent :), I just merged it.

Please sign in to comment.
Something went wrong with that request. Please try again.