Skip to content

Commit

Permalink
Publish AS::Executor and AS::Reloader APIs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matthewd committed Mar 1, 2016
1 parent 664a13e commit d3c9d80
Show file tree
Hide file tree
Showing 33 changed files with 782 additions and 307 deletions.
2 changes: 1 addition & 1 deletion actionpack/lib/action_dispatch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class IllegalStateError < StandardError
autoload :Cookies
autoload :DebugExceptions
autoload :ExceptionWrapper
autoload :Executor
autoload :Flash
autoload :LoadInterlock
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
Expand Down
11 changes: 10 additions & 1 deletion actionpack/lib/action_dispatch/middleware/callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ class Callbacks
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)
Expand Down
19 changes: 19 additions & 0 deletions actionpack/lib/action_dispatch/middleware/executor.rb
Original file line number Diff line number Diff line change
@@ -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
21 changes: 0 additions & 21 deletions actionpack/lib/action_dispatch/middleware/load_interlock.rb

This file was deleted.

74 changes: 18 additions & 56 deletions actionpack/lib/action_dispatch/middleware/reloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!
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
if defined? Rails.application.reloader
Rails.application.reloader.reload!
else
ActiveSupport::Reloader.reload!
end
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
13 changes: 11 additions & 2 deletions actionpack/lib/action_dispatch/testing/integration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -455,17 +455,24 @@ module Runner
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)
Expand All @@ -481,6 +488,8 @@ def create_session(app)
end

def remove! # :nodoc:
@execution_context.complete! if @execution_context
@execution_context = nil
@integration_session = nil
end

Expand Down
14 changes: 10 additions & 4 deletions actionpack/test/dispatch/callbacks_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,19 @@ def test_before_and_after_callbacks

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

Expand Down
134 changes: 134 additions & 0 deletions actionpack/test/dispatch/executor_test.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d3c9d80

Please sign in to comment.