1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Publish AS::Executor and AS::Reloader APIs

These should allow external code to run blocks of user code to do
"work", at a similar unit size to a web request, without needing to get
intimate with ActionDipatch.
This commit is contained in:
Matthew Draper 2016-02-22 11:55:52 +10:30
parent 664a13e6fb
commit d3c9d808e3
33 changed files with 782 additions and 307 deletions

View file

@ -51,8 +51,8 @@ module ActionDispatch
autoload :Cookies
autoload :DebugExceptions
autoload :ExceptionWrapper
autoload :Executor
autoload :Flash
autoload :LoadInterlock
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader

View file

@ -7,7 +7,16 @@ module ActionDispatch
define_callbacks :call
class << self
delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
def to_prepare(*args, &block)
ActiveSupport::Reloader.to_prepare(*args, &block)
end
def to_cleanup(*args, &block)
ActiveSupport::Reloader.to_complete(*args, &block)
end
deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
to_cleanup: 'use ActiveSupport::Reloader.to_complete instead'
def before(*args, &block)
set_callback(:call, :before, *args, &block)

View file

@ -0,0 +1,19 @@
require 'rack/body_proxy'
module ActionDispatch
class Executor
def initialize(app, executor)
@app, @executor = app, executor
end
def call(env)
state = @executor.run!
begin
response = @app.call(env)
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
ensure
state.complete! unless returned
end
end
end
end

View file

@ -1,21 +0,0 @@
require 'active_support/dependencies'
require 'rack/body_proxy'
module ActionDispatch
class LoadInterlock
def initialize(app)
@app = app
end
def call(env)
interlock = ActiveSupport::Dependencies.interlock
interlock.start_running
response = @app.call(env)
body = Rack::BodyProxy.new(response[2]) { interlock.done_running }
response[2] = body
response
ensure
interlock.done_running unless body
end
end
end

View file

@ -23,74 +23,36 @@ module ActionDispatch
# middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
# or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
#
class Reloader
include ActiveSupport::Callbacks
include ActiveSupport::Deprecation::Reporting
define_callbacks :prepare
define_callbacks :cleanup
# Add a prepare callback. Prepare callbacks are run before each request, prior
# to ActionDispatch::Callback's before callbacks.
class Reloader < Executor
def self.to_prepare(*args, &block)
unless block_given?
warn "to_prepare without a block is deprecated. Please use a block"
end
set_callback(:prepare, *args, &block)
ActiveSupport::Reloader.to_prepare(*args, &block)
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(*args, &block)
unless block_given?
warn "to_cleanup without a block is deprecated. Please use a block"
end
set_callback(:cleanup, *args, &block)
ActiveSupport::Reloader.to_complete(*args, &block)
end
# Execute all prepare callbacks.
def self.prepare!
new(nil).prepare!
if defined? Rails.application.reloader
Rails.application.reloader.prepare!
else
ActiveSupport::Reloader.prepare!
end
end
# Execute all cleanup callbacks.
def self.cleanup!
new(nil).cleanup!
if defined? Rails.application.reloader
Rails.application.reloader.reload!
else
ActiveSupport::Reloader.reload!
end
end
def initialize(app, condition=nil)
@app = app
@condition = condition || lambda { true }
@validated = true
end
def call(env)
@validated = @condition.call
prepare!
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
response
rescue Exception
cleanup!
raise
end
def prepare! #:nodoc:
run_callbacks :prepare if validated?
end
def cleanup! #:nodoc:
run_callbacks :cleanup if validated?
ensure
@validated = true
end
private
def validated? #:nodoc:
@validated
class << self
deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
to_cleanup: 'use ActiveSupport::Reloader.to_complete instead',
prepare!: 'use Rails.application.reloader.prepare! instead',
cleanup!: 'use Rails.application.reloader.reload! instead of cleanup + prepare'
end
end
end

View file

@ -455,17 +455,24 @@ module ActionDispatch
def before_setup # :nodoc:
@app = nil
@integration_session = nil
@execution_context = nil
super
end
def after_teardown # :nodoc:
remove!
super
end
def integration_session
@integration_session ||= create_session(app)
@integration_session ||= create_session(app).tap { @execution_context = app.respond_to?(:executor) && app.executor.run! }
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
@integration_session = create_session(app)
remove!
integration_session
end
def create_session(app)
@ -481,6 +488,8 @@ module ActionDispatch
end
def remove! # :nodoc:
@execution_context.complete! if @execution_context
@execution_context = nil
@integration_session = nil
end

View file

@ -37,13 +37,19 @@ class DispatcherTest < ActiveSupport::TestCase
def test_to_prepare_and_cleanup_delegation
prepared = cleaned = false
ActionDispatch::Callbacks.to_prepare { prepared = true }
ActionDispatch::Callbacks.to_prepare { cleaned = true }
assert_deprecated do
ActionDispatch::Callbacks.to_prepare { prepared = true }
ActionDispatch::Callbacks.to_prepare { cleaned = true }
end
ActionDispatch::Reloader.prepare!
assert_deprecated do
ActionDispatch::Reloader.prepare!
end
assert prepared
ActionDispatch::Reloader.cleanup!
assert_deprecated do
ActionDispatch::Reloader.cleanup!
end
assert cleaned
end

View file

@ -0,0 +1,134 @@
require 'abstract_unit'
class ExecutorTest < ActiveSupport::TestCase
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_respond_to body, :close
end
def test_returned_body_object_always_responds_to_close_even_if_called_twice
body = call_and_return_body
assert_respond_to body, :close
body.close
body = call_and_return_body
assert_respond_to body, :close
body.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_respond_to body, :size
assert_respond_to body, :each
assert_respond_to body, :foo
assert_respond_to body, :bar
end
def test_run_callbacks_are_called_before_close
running = false
executor.to_run { running = true }
body = call_and_return_body
assert running
running = false
body.close
assert !running
end
def test_complete_callbacks_are_called_on_close
completed = false
executor.to_complete { completed = true }
body = call_and_return_body
assert !completed
body.close
assert completed
end
def test_complete_callbacks_are_called_on_exceptions
completed = false
executor.to_complete { completed = true }
begin
call_and_return_body do
raise "error"
end
rescue
end
assert completed
end
def test_callbacks_execute_in_shared_context
result = false
executor.to_run { @in_shared_context = true }
executor.to_complete { result = @in_shared_context }
call_and_return_body.close
assert result
assert !defined?(@in_shared_context) # it's not in the test itself
end
private
def call_and_return_body(&block)
app = middleware(block || proc { [200, {}, 'response'] })
_, _, body = app.call({'rack.input' => StringIO.new('')})
body
end
def middleware(inner_app)
ActionDispatch::Executor.new(inner_app, executor)
end
def executor
@executor ||= Class.new(ActiveSupport::Executor)
end
end

View file

@ -4,15 +4,17 @@ class ReloaderTest < ActiveSupport::TestCase
Reloader = ActionDispatch::Reloader
teardown do
Reloader.reset_callbacks :prepare
Reloader.reset_callbacks :cleanup
ActiveSupport::Reloader.reset_callbacks :prepare
ActiveSupport::Reloader.reset_callbacks :complete
end
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 }
assert_deprecated do
Reloader.to_prepare { |*args| a = b = c = 1 }
Reloader.to_prepare { |*args| b = c = 2 }
Reloader.to_prepare { |*args| c = 3 }
end
# Ensure to_prepare callbacks are not run when defined
assert_nil a || b || c
@ -60,9 +62,15 @@ class ReloaderTest < ActiveSupport::TestCase
def test_condition_specifies_when_to_reload
i, j = 0, 0, 0, 0
Reloader.to_prepare { |*args| i += 1 }
Reloader.to_cleanup { |*args| j += 1 }
app = Reloader.new(lambda { |env| [200, {}, []] }, lambda { i < 3 })
assert_deprecated do
Reloader.to_prepare { |*args| i += 1 }
Reloader.to_cleanup { |*args| j += 1 }
end
x = Class.new(ActiveSupport::Reloader)
x.check = lambda { i < 3 }
app = Reloader.new(lambda { |env| [200, {}, []] }, x)
5.times do
resp = app.call({})
resp[2].close
@ -109,7 +117,9 @@ class ReloaderTest < ActiveSupport::TestCase
def test_cleanup_callbacks_are_called_when_body_is_closed
cleaned = false
Reloader.to_cleanup { cleaned = true }
assert_deprecated do
Reloader.to_cleanup { cleaned = true }
end
body = call_and_return_body
assert !cleaned
@ -120,7 +130,9 @@ class ReloaderTest < ActiveSupport::TestCase
def test_prepare_callbacks_arent_called_when_body_is_closed
prepared = false
Reloader.to_prepare { prepared = true }
assert_deprecated do
Reloader.to_prepare { prepared = true }
end
body = call_and_return_body
prepared = false
@ -131,31 +143,43 @@ class ReloaderTest < ActiveSupport::TestCase
def test_manual_reloading
prepared = cleaned = false
Reloader.to_prepare { prepared = true }
Reloader.to_cleanup { cleaned = true }
assert_deprecated do
Reloader.to_prepare { prepared = true }
Reloader.to_cleanup { cleaned = true }
end
Reloader.prepare!
assert_deprecated do
Reloader.prepare!
end
assert prepared
assert !cleaned
prepared = cleaned = false
Reloader.cleanup!
assert !prepared
assert_deprecated do
Reloader.cleanup!
end
assert prepared
assert cleaned
end
def test_prepend_prepare_callback
i = 10
Reloader.to_prepare { i += 1 }
Reloader.to_prepare(:prepend => true) { i = 0 }
assert_deprecated do
Reloader.to_prepare { i += 1 }
Reloader.to_prepare(:prepend => true) { i = 0 }
end
Reloader.prepare!
assert_deprecated do
Reloader.prepare!
end
assert_equal 1, i
end
def test_cleanup_callbacks_are_called_on_exceptions
cleaned = false
Reloader.to_cleanup { cleaned = true }
assert_deprecated do
Reloader.to_cleanup { cleaned = true }
end
begin
call_and_return_body do
@ -169,8 +193,11 @@ class ReloaderTest < ActiveSupport::TestCase
private
def call_and_return_body(&block)
x = Class.new(ActiveSupport::Reloader)
x.check = lambda { true }
@response ||= 'response'
@reloader ||= Reloader.new(block || proc {[200, {}, @response]})
@reloader ||= Reloader.new(block || proc {[200, {}, @response]}, x)
@reloader.call({'rack.input' => StringIO.new('')})[2]
end
end

View file

@ -137,7 +137,6 @@ module ActiveRecord
eager_autoload do
autoload :AbstractAdapter
autoload :ConnectionManagement, "active_record/connection_adapters/abstract/connection_pool"
end
end

View file

@ -951,24 +951,5 @@ module ActiveRecord
owner_to_pool && owner_to_pool[owner.name]
end
end
class ConnectionManagement
def initialize(app)
@app = app
end
def call(env)
testing = env['rack.test']
status, headers, body = @app.call(env)
proxy = ::Rack::BodyProxy.new(body) do
ActiveRecord::Base.clear_active_connections! unless testing
end
[status, headers, proxy]
rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
end
end
end
end

View file

@ -27,7 +27,6 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
autoload :ConnectionHandler
autoload :ConnectionManagement
end
autoload_under 'abstract' do

View file

@ -23,34 +23,26 @@ module ActiveRecord
end
end
def initialize(app)
@app = app
end
def self.install_executor_hooks(executor = ActiveSupport::Executor)
executor.to_run do
connection = ActiveRecord::Base.connection
enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
connection.enable_query_cache!
def call(env)
connection = ActiveRecord::Base.connection
enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
restore_query_cache_settings(connection_id, enabled)
@restore_query_cache_settings = lambda do
ActiveRecord::Base.connection_id = connection_id
ActiveRecord::Base.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache! unless enabled
end
end
response
rescue Exception => e
restore_query_cache_settings(connection_id, enabled)
raise e
executor.to_complete do
@restore_query_cache_settings.call if defined?(@restore_query_cache_settings)
# FIXME: This should be skipped when env['rack.test']
ActiveRecord::Base.clear_active_connections!
end
end
private
def restore_query_cache_settings(connection_id, enabled)
ActiveRecord::Base.connection_id = connection_id
ActiveRecord::Base.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache! unless enabled
end
end
end

View file

@ -16,12 +16,6 @@ module ActiveRecord
config.app_generators.orm :active_record, :migration => true,
:timestamps => true
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
ActiveRecord::QueryCache
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
ActiveRecord::ConnectionAdapters::ConnectionManagement
config.action_dispatch.rescue_responses.merge!(
'ActiveRecord::RecordNotFound' => :not_found,
'ActiveRecord::StaleObjectError' => :conflict,
@ -153,11 +147,9 @@ end_warning
end
end
initializer "active_record.set_reloader_hooks" do |app|
hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup
initializer "active_record.set_reloader_hooks" do
ActiveSupport.on_load(:active_record) do
ActionDispatch::Reloader.send(hook) do
ActiveSupport::Reloader.before_class_unload do
if ActiveRecord::Base.connected?
ActiveRecord::Base.clear_cache!
ActiveRecord::Base.clear_reloadable_connections!
@ -166,6 +158,12 @@ end_warning
end
end
initializer "active_record.set_executor_hooks" do
ActiveSupport.on_load(:active_record) do
ActiveRecord::QueryCache.install_executor_hooks
end
end
initializer "active_record.add_watchable_files" do |app|
path = app.paths["db"].first
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]

View file

@ -19,7 +19,7 @@ module ActiveRecord
def setup
@env = {}
@app = App.new
@management = ConnectionManagement.new(@app)
@management = middleware(@app)
# make sure we have an active connection
assert ActiveRecord::Base.connection
@ -27,17 +27,12 @@ module ActiveRecord
end
def test_app_delegation
manager = ConnectionManagement.new(@app)
manager = middleware(@app)
manager.call @env
assert_equal [@env], @app.calls
end
def test_connections_are_active_after_call
@management.call(@env)
assert ActiveRecord::Base.connection_handler.active_connections?
end
def test_body_responds_to_each
_, _, body = @management.call(@env)
bits = []
@ -52,45 +47,40 @@ module ActiveRecord
end
def test_active_connections_are_not_cleared_on_body_close_during_test
@env['rack.test'] = true
_, _, body = @management.call(@env)
body.close
assert ActiveRecord::Base.connection_handler.active_connections?
executor.wrap do
_, _, body = @management.call(@env)
body.close
assert ActiveRecord::Base.connection_handler.active_connections?
end
end
def test_connections_closed_if_exception
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = ConnectionManagement.new(app)
explosive = middleware(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
end
def test_connections_not_closed_if_exception_and_test
@env['rack.test'] = true
app = Class.new(App) { def call(env); raise; end }.new
explosive = ConnectionManagement.new(app)
assert_raises(RuntimeError) { explosive.call(@env) }
assert ActiveRecord::Base.connection_handler.active_connections?
end
def test_connections_closed_if_exception_and_explicitly_not_test
@env['rack.test'] = false
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = ConnectionManagement.new(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
executor.wrap do
app = Class.new(App) { def call(env); raise; end }.new
explosive = middleware(app)
assert_raises(RuntimeError) { explosive.call(@env) }
assert ActiveRecord::Base.connection_handler.active_connections?
end
end
test "doesn't clear active connections when running in a test case" do
@env['rack.test'] = true
@management.call(@env)
assert ActiveRecord::Base.connection_handler.active_connections?
executor.wrap do
@management.call(@env)
assert ActiveRecord::Base.connection_handler.active_connections?
end
end
test "proxy is polite to its body and responds to it" do
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
response_body = ConnectionManagement.new(app).call(@env)[2]
response_body = middleware(app).call(@env)[2]
assert response_body.respond_to?(:to_path)
assert_equal "/path", response_body.to_path
end
@ -98,9 +88,23 @@ module ActiveRecord
test "doesn't mutate the original response" do
original_response = [200, {}, 'hi']
app = lambda { |_| original_response }
ConnectionManagement.new(app).call(@env)[2]
middleware(app).call(@env)[2]
assert_equal 'hi', original_response.last
end
private
def executor
@executor ||= Class.new(ActiveSupport::Executor).tap do |exe|
ActiveRecord::QueryCache.install_executor_hooks(exe)
end
end
def middleware(app)
lambda do |env|
a, b, c = executor.wrap { app.call(env) }
[a, b, Rack::BodyProxy.new(c) { }]
end
end
end
end
end

View file

@ -16,7 +16,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_exceptional_middleware_clears_and_disables_cache_on_error
assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
mw = ActiveRecord::QueryCache.new lambda { |env|
mw = middleware { |env|
Task.find 1
Task.find 1
assert_equal 1, ActiveRecord::Base.connection.query_cache.length
@ -31,7 +31,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_exceptional_middleware_leaves_enabled_cache_alone
ActiveRecord::Base.connection.enable_query_cache!
mw = ActiveRecord::QueryCache.new lambda { |env|
mw = middleware { |env|
raise "lol borked"
}
assert_raises(RuntimeError) { mw.call({}) }
@ -42,7 +42,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_exceptional_middleware_assigns_original_connection_id_on_error
connection_id = ActiveRecord::Base.connection_id
mw = ActiveRecord::QueryCache.new lambda { |env|
mw = middleware { |env|
ActiveRecord::Base.connection_id = self.object_id
raise "lol borked"
}
@ -53,7 +53,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_middleware_delegates
called = false
mw = ActiveRecord::QueryCache.new lambda { |env|
mw = middleware { |env|
called = true
[200, {}, nil]
}
@ -62,7 +62,7 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_middleware_caches
mw = ActiveRecord::QueryCache.new lambda { |env|
mw = middleware { |env|
Task.find 1
Task.find 1
assert_equal 1, ActiveRecord::Base.connection.query_cache.length
@ -74,50 +74,13 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_cache_enabled_during_call
assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
mw = ActiveRecord::QueryCache.new lambda { |env|
mw = middleware { |env|
assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
[200, {}, nil]
}
mw.call({})
end
def test_cache_on_during_body_write
streaming = Class.new do
def each
yield ActiveRecord::Base.connection.query_cache_enabled
end
end
mw = ActiveRecord::QueryCache.new lambda { |env|
[200, {}, streaming.new]
}
body = mw.call({}).last
body.each { |x| assert x, 'cache should be on' }
body.close
assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
end
def test_cache_off_after_close
mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] }
body = mw.call({}).last
assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled'
body.close
assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
end
def test_cache_clear_after_close
mw = ActiveRecord::QueryCache.new lambda { |env|
Post.first
[200, {}, nil]
}
body = mw.call({}).last
assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty'
body.close
assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty'
end
def test_cache_passing_a_relation
post = Post.first
Post.cache do
@ -244,6 +207,13 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_equal 0, Post.where(title: 'rollback').to_a.count
end
end
private
def middleware(&app)
executor = Class.new(ActiveSupport::Executor)
ActiveRecord::QueryCache.install_executor_hooks executor
lambda { |env| executor.wrap { app.call(env) } }
end
end
class QueryCacheExpiryTest < ActiveRecord::TestCase

View file

@ -1,3 +1,10 @@
* Publish ActiveSupport::Executor and ActiveSupport::Reloader APIs to allow
components and libraries to manage, and participate in, the execution of
application code, and the application reloading process.
*Matthew Draper*
## Rails 5.0.0.beta3 (February 24, 2016) ##
* Deprecate arguments on `assert_nothing_raised`.

View file

@ -33,10 +33,13 @@ module ActiveSupport
autoload :Concern
autoload :Dependencies
autoload :DescendantsTracker
autoload :ExecutionWrapper
autoload :Executor
autoload :FileUpdateChecker
autoload :EventedFileUpdateChecker
autoload :LogSubscriber
autoload :Notifications
autoload :Reloader
eager_autoload do
autoload :BacktraceCleaner

View file

@ -19,14 +19,12 @@ module ActiveSupport #:nodoc:
end
end
# Attempt to obtain an "unloading" (exclusive) lock. If possible,
# execute the supplied block while holding the lock. If there is
# concurrent activity, return immediately (without executing the
# block) instead of waiting.
def attempt_unloading
@lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload], no_wait: true) do
yield
end
def start_unloading
@lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
end
def done_unloading
@lock.stop_exclusive(compatible: [:load, :unload])
end
def start_running

View file

@ -37,6 +37,7 @@ module ActiveSupport
def execute_if_updated
if updated?
yield if block_given?
execute
true
end

View file

@ -0,0 +1,69 @@
require 'active_support/callbacks'
module ActiveSupport
class ExecutionWrapper
include ActiveSupport::Callbacks
define_callbacks :run
define_callbacks :complete
def self.to_run(*args, &block)
set_callback(:run, *args, &block)
end
def self.to_complete(*args, &block)
set_callback(:complete, *args, &block)
end
# Run this execution.
#
# Returns an instance, whose +complete!+ method *must* be invoked
# after the work has been performed.
#
# Where possible, prefer +wrap+.
def self.run!
new.tap(&:run!)
end
# Perform the work in the supplied block as an execution.
def self.wrap
return yield if active?
state = run!
begin
yield
ensure
state.complete!
end
end
class << self # :nodoc:
attr_accessor :active
end
def self.inherited(other) # :nodoc:
super
other.active = Concurrent::Hash.new(0)
end
self.active = Concurrent::Hash.new(0)
def self.active? # :nodoc:
@active[Thread.current] > 0
end
def run! # :nodoc:
self.class.active[Thread.current] += 1
run_callbacks(:run)
end
# Complete this in-flight execution. This method *must* be called
# exactly once on the result of any call to +run!+.
#
# Where possible, prefer +wrap+.
def complete!
run_callbacks(:complete)
self.class.active.delete Thread.current if (self.class.active[Thread.current] -= 1) == 0
end
end
end

View file

@ -0,0 +1,6 @@
require 'active_support/execution_wrapper'
module ActiveSupport
class Executor < ExecutionWrapper
end
end

View file

@ -81,6 +81,7 @@ module ActiveSupport
# Execute the block given if updated.
def execute_if_updated
if updated?
yield if block_given?
execute
true
else

View file

@ -64,8 +64,8 @@ module I18n
end
app.reloaders << reloader
ActionDispatch::Reloader.to_prepare do
reloader.execute_if_updated
app.reloader.to_run do
reloader.execute_if_updated { require_unload_lock! }
# TODO: remove the following line as soon as the return value of
# callbacks is ignored, that is, returning `false` does not
# display a deprecation warning or halts the callback chain.

View file

@ -0,0 +1,126 @@
require 'active_support/execution_wrapper'
module ActiveSupport
#--
# This class defines several callbacks:
#
# to_prepare -- Run once at application startup, and also from
# +to_run+.
#
# to_run -- Run before a work run that is reloading. If
# +reload_classes_only_on_change+ is true (the default), the class
# unload will have already occurred.
#
# to_complete -- Run after a work run that has reloaded. If
# +reload_classes_only_on_change+ is false, the class unload will
# have occurred after the work run, but before this callback.
#
# before_class_unload -- Run immediately before the classes are
# unloaded.
#
# after_class_unload -- Run immediately after the classes are
# unloaded.
#
class Reloader < ExecutionWrapper
Null = Class.new(ExecutionWrapper) # :nodoc:
define_callbacks :prepare
define_callbacks :class_unload
def self.to_prepare(*args, &block)
set_callback(:prepare, *args, &block)
end
def self.before_class_unload(*args, &block)
set_callback(:class_unload, *args, &block)
end
def self.after_class_unload(*args, &block)
set_callback(:class_unload, :after, *args, &block)
end
to_run(:after) { self.class.prepare! }
# Initiate a manual reload
def self.reload!
executor.wrap do
new.tap(&:run!).complete!
end
prepare!
end
def self.run! # :nodoc:
if check!
super
else
Null.run!
end
end
# Run the supplied block as a work unit, reloading code as needed
def self.wrap
executor.wrap do
super
end
end
class << self
attr_accessor :executor
attr_accessor :check
end
self.executor = Executor
self.check = lambda { false }
def self.check! # :nodoc:
@should_reload ||= check.call
end
def self.reloaded! # :nodoc:
@should_reload = false
end
def self.prepare! # :nodoc:
new.run_callbacks(:prepare)
end
def initialize
super
@locked = false
end
# Acquire the ActiveSupport::Dependencies::Interlock unload lock,
# ensuring it will be released automatically
def require_unload_lock!
unless @locked
ActiveSupport::Dependencies.interlock.start_unloading
@locked = true
end
end
# Release the unload lock if it has been previously obtained
def release_unload_lock!
if @locked
@locked = false
ActiveSupport::Dependencies.interlock.done_unloading
end
end
def run! # :nodoc:
super
release_unload_lock!
end
def class_unload!(&block) # :nodoc:
require_unload_lock!
run_callbacks(:class_unload, &block)
end
def complete! # :nodoc:
super
self.class.reloaded!
ensure
release_unload_lock!
end
end
end

View file

@ -0,0 +1,76 @@
require 'abstract_unit'
class ExecutorTest < ActiveSupport::TestCase
def test_wrap_invokes_callbacks
called = []
executor.to_run { called << :run }
executor.to_complete { called << :complete }
executor.wrap do
called << :body
end
assert_equal [:run, :body, :complete], called
end
def test_callbacks_share_state
result = false
executor.to_run { @foo = true }
executor.to_complete { result = @foo }
executor.wrap { }
assert result
end
def test_separated_calls_invoke_callbacks
called = []
executor.to_run { called << :run }
executor.to_complete { called << :complete }
state = executor.run!
called << :body
state.complete!
assert_equal [:run, :body, :complete], called
end
def test_avoids_double_wrapping
called = []
executor.to_run { called << :run }
executor.to_complete { called << :complete }
executor.wrap do
called << :early
executor.wrap do
called << :body
end
called << :late
end
assert_equal [:run, :early, :body, :late, :complete], called
end
def test_separate_classes_can_wrap
other_executor = Class.new(ActiveSupport::Executor)
called = []
executor.to_run { called << :run }
executor.to_complete { called << :complete }
other_executor.to_run { called << :other_run }
other_executor.to_complete { called << :other_complete }
executor.wrap do
other_executor.wrap do
called << :body
end
end
assert_equal [:run, :other_run, :body, :other_complete, :complete], called
end
private
def executor
@executor ||= Class.new(ActiveSupport::Executor)
end
end

View file

@ -0,0 +1,85 @@
require 'abstract_unit'
class ReloaderTest < ActiveSupport::TestCase
def test_prepare_callback
prepared = false
reloader.to_prepare { prepared = true }
assert !prepared
reloader.prepare!
assert prepared
prepared = false
reloader.wrap do
assert prepared
prepared = false
end
assert !prepared
end
def test_only_run_when_check_passes
r = new_reloader { true }
invoked = false
r.to_run { invoked = true }
r.wrap { }
assert invoked
r = new_reloader { false }
invoked = false
r.to_run { invoked = true }
r.wrap { }
assert !invoked
end
def test_full_reload_sequence
called = []
reloader.to_prepare { called << :prepare }
reloader.to_run { called << :reloader_run }
reloader.to_complete { called << :reloader_complete }
reloader.executor.to_run { called << :executor_run }
reloader.executor.to_complete { called << :executor_complete }
reloader.wrap { }
assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete], called
called = []
reloader.reload!
assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called
reloader.check = lambda { false }
called = []
reloader.wrap { }
assert_equal [:executor_run, :executor_complete], called
called = []
reloader.reload!
assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called
end
def test_class_unload_block
called = []
reloader.before_class_unload { called << :before_unload }
reloader.after_class_unload { called << :after_unload }
reloader.to_run do
class_unload! do
called << :unload
end
end
reloader.wrap { called << :body }
assert_equal [:before_unload, :unload, :after_unload, :body], called
end
private
def new_reloader(&check)
Class.new(ActiveSupport::Reloader).tap do |r|
r.check = check
r.executor = Class.new(ActiveSupport::Executor)
end
end
def reloader
@reloader ||= new_reloader { true }
end
end

View file

@ -113,7 +113,7 @@ module Rails
attr_accessor :assets, :sandbox
alias_method :sandbox?, :sandbox
attr_reader :reloaders
attr_reader :reloaders, :reloader, :executor
delegate :default_url_options, :default_url_options=, to: :routes
@ -131,6 +131,10 @@ module Rails
@message_verifiers = {}
@ran_load_hooks = false
@executor = Class.new(ActiveSupport::Executor)
@reloader = Class.new(ActiveSupport::Reloader)
@reloader.executor = @executor
# are these actually used?
@initial_variable_values = initial_variable_values
@block = block

View file

@ -34,22 +34,10 @@ module Rails
# handling: presumably their code is not threadsafe
middleware.use ::Rack::Lock
elsif config.allow_concurrency == :unsafe
# Do nothing, even if we know this is dangerous. This is the
# historical behaviour for true.
else
# Default concurrency setting: enabled, but safe
unless config.cache_classes && config.eager_load
# Without cache_classes + eager_load, the load interlock
# is required for proper operation
middleware.use ::ActionDispatch::LoadInterlock
end
end
middleware.use ::ActionDispatch::Executor, app.executor
middleware.use ::Rack::Runtime
middleware.use ::Rack::MethodOverride unless config.api_only
middleware.use ::ActionDispatch::RequestId
@ -61,7 +49,7 @@ module Rails
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
unless config.cache_classes
middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? }
middleware.use ::ActionDispatch::Reloader, app.reloader
end
middleware.use ::ActionDispatch::Callbacks
@ -83,10 +71,6 @@ module Rails
private
def reload_dependencies?
config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
end
def load_rack_cache
rack_cache = config.action_dispatch.rack_cache
return unless rack_cache

View file

@ -38,16 +38,16 @@ module Rails
app.routes.define_mounted_helper(:main_app)
end
initializer :add_to_prepare_blocks do
initializer :add_to_prepare_blocks do |app|
config.to_prepare_blocks.each do |block|
ActionDispatch::Reloader.to_prepare(&block)
app.reloader.to_prepare(&block)
end
end
# This needs to happen before eager load so it happens
# in exactly the same point regardless of config.cache_classes
initializer :run_prepare_callbacks do
ActionDispatch::Reloader.prepare!
initializer :run_prepare_callbacks do |app|
app.reloader.prepare!
end
initializer :eager_load! do
@ -62,13 +62,47 @@ module Rails
ActiveSupport.run_load_hooks(:after_initialize, self)
end
initializer :configure_executor_for_concurrency do |app|
if config.allow_concurrency == false
# User has explicitly opted out of concurrent request
# handling: presumably their code is not threadsafe
mutex = Mutex.new
app.executor.to_run(prepend: true) do
mutex.lock
end
app.executor.to_complete(:after) do
mutex.unlock
end
elsif config.allow_concurrency == :unsafe
# Do nothing, even if we know this is dangerous. This is the
# historical behaviour for true.
else
# Default concurrency setting: enabled, but safe
unless config.cache_classes && config.eager_load
# Without cache_classes + eager_load, the load interlock
# is required for proper operation
app.executor.to_run(prepend: true) do
ActiveSupport::Dependencies.interlock.start_running
end
app.executor.to_complete(:after) do
ActiveSupport::Dependencies.interlock.done_running
end
end
end
end
# Set routes reload after the finisher hook to ensure routes added in
# the hook are taken into account.
initializer :set_routes_reloader_hook do
initializer :set_routes_reloader_hook do |app|
reloader = routes_reloader
reloader.execute_if_updated
self.reloaders << reloader
ActionDispatch::Reloader.to_prepare do
app.reloader.to_run do
# We configure #execute rather than #execute_if_updated because if
# autoloaded constants are cleared we need to reload routes also in
# case any was used there, as in
@ -78,18 +112,27 @@ module Rails
# This means routes are also reloaded if i18n is updated, which
# might not be necessary, but in order to be more precise we need
# some sort of reloaders dependency support, to be added.
require_unload_lock!
reloader.execute
end
end
# Set clearing dependencies after the finisher hook to ensure paths
# added in the hook are taken into account.
initializer :set_clear_dependencies_hook, group: :all do
initializer :set_clear_dependencies_hook, group: :all do |app|
callback = lambda do
ActiveSupport::Dependencies.interlock.unloading do
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
ActiveSupport::DescendantsTracker.clear
ActiveSupport::Dependencies.clear
end
if config.cache_classes
app.reloader.check = lambda { false }
elsif config.reload_classes_only_on_change
app.reloader.check = lambda do
app.reloaders.map(&:updated?).any?
end
else
app.reloader.check = lambda { true }
end
if config.reload_classes_only_on_change
@ -99,15 +142,19 @@ module Rails
# Prepend this callback to have autoloaded constants cleared before
# any other possible reloading, in case they need to autoload fresh
# constants.
ActionDispatch::Reloader.to_prepare(prepend: true) do
app.reloader.to_run(prepend: true) do
# In addition to changes detected by the file watcher, if routes
# or i18n have been updated we also need to clear constants,
# that's why we run #execute rather than #execute_if_updated, this
# callback has to clear autoloaded constants after any update.
reloader.execute
class_unload! do
reloader.execute
end
end
else
ActionDispatch::Reloader.to_cleanup(&callback)
app.reloader.to_complete do
class_unload!(&callback)
end
end
end

View file

@ -29,8 +29,7 @@ module Rails
# reloads the environment
def reload!(print=true)
puts "Reloading..." if print
ActionDispatch::Reloader.cleanup!
ActionDispatch::Reloader.prepare!
Rails.application.reloader.reload!
true
end
end

View file

@ -52,12 +52,11 @@ class ConsoleTest < ActiveSupport::TestCase
a = b = c = nil
# TODO: These should be defined on the initializer
ActionDispatch::Reloader.to_cleanup { a = b = c = 1 }
ActionDispatch::Reloader.to_cleanup { b = c = 2 }
ActionDispatch::Reloader.to_prepare { c = 3 }
ActiveSupport::Reloader.to_complete { a = b = c = 1 }
ActiveSupport::Reloader.to_complete { b = c = 2 }
ActiveSupport::Reloader.to_prepare { c = 3 }
# Hide Reloading... output
silence_stream(STDOUT) { irb_context.reload! }
irb_context.reload!(false)
assert_equal 1, a
assert_equal 2, b
@ -81,7 +80,7 @@ class ConsoleTest < ActiveSupport::TestCase
MODEL
assert !User.new.respond_to?(:age)
silence_stream(STDOUT) { irb_context.reload! }
irb_context.reload!(false)
assert User.new.respond_to?(:age)
end

View file

@ -26,7 +26,7 @@ module ApplicationTests
assert_equal [
"Rack::Sendfile",
"ActionDispatch::Static",
"ActionDispatch::LoadInterlock",
"ActionDispatch::Executor",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"Rack::MethodOverride",
@ -38,8 +38,6 @@ module ApplicationTests
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
"ActiveRecord::ConnectionAdapters::ConnectionManagement",
"ActiveRecord::QueryCache",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
@ -57,7 +55,7 @@ module ApplicationTests
assert_equal [
"Rack::Sendfile",
"ActionDispatch::Static",
"ActionDispatch::LoadInterlock",
"ActionDispatch::Executor",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"ActionDispatch::RequestId",
@ -67,8 +65,6 @@ module ApplicationTests
"ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::ConnectionAdapters::ConnectionManagement",
"ActiveRecord::QueryCache",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag"
@ -114,23 +110,12 @@ module ApplicationTests
test "removing Active Record omits its middleware" do
use_frameworks []
boot!
assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement")
assert !middleware.include?("ActiveRecord::QueryCache")
assert !middleware.include?("ActiveRecord::Migration::CheckPending")
end
test "includes interlock if cache_classes is set but eager_load is not" do
add_to_config "config.cache_classes = true"
test "includes executor" do
boot!
assert_not_includes middleware, "Rack::Lock"
assert_includes middleware, "ActionDispatch::LoadInterlock"
end
test "includes interlock if cache_classes is off" do
add_to_config "config.cache_classes = false"
boot!
assert_not_includes middleware, "Rack::Lock"
assert_includes middleware, "ActionDispatch::LoadInterlock"
assert_includes middleware, "ActionDispatch::Executor"
end
test "does not include lock if cache_classes is set and so is eager_load" do
@ -138,21 +123,18 @@ module ApplicationTests
add_to_config "config.eager_load = true"
boot!
assert_not_includes middleware, "Rack::Lock"
assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "does not include lock if allow_concurrency is set to :unsafe" do
add_to_config "config.allow_concurrency = :unsafe"
boot!
assert_not_includes middleware, "Rack::Lock"
assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "includes lock if allow_concurrency is disabled" do
add_to_config "config.allow_concurrency = false"
boot!
assert_includes middleware, "Rack::Lock"
assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "removes static asset server if public_file_server.enabled is disabled" do