Fix bug where Call didn't propagate recovery. Warden has no recovery.

The issue here is that when a middleware failed and a recovery sequence
started, it would halt at the "call" step because the "Call" didn't
properly recover the child sequence.

An additional issue was that a Warden had no "recover" method, meaning
embedded Wardens wouldn't recover their stacks properly.
This commit is contained in:
Mitchell Hashimoto 2012-12-22 22:16:17 -08:00
parent a6e0d3908f
commit 50d7b0aba4
4 changed files with 86 additions and 16 deletions

View File

@ -104,16 +104,6 @@ module Vagrant
nil nil
end end
protected
# Returns the current stack of middlewares. You probably won't
# need to use this directly, and it's recommended that you don't.
#
# @return [Array]
def stack
@stack ||= []
end
# Converts the builder stack to a runnable action sequence. # Converts the builder stack to a runnable action sequence.
# #
# @param [Vagrant::Action::Environment] env The action environment # @param [Vagrant::Action::Environment] env The action environment
@ -123,6 +113,16 @@ module Vagrant
# and predictable behavior upon exceptions. # and predictable behavior upon exceptions.
Warden.new(stack.dup, env) Warden.new(stack.dup, env)
end end
protected
# Returns the current stack of middlewares. You probably won't
# need to use this directly, and it's recommended that you don't.
#
# @return [Array]
def stack
@stack ||= []
end
end end
end end
end end

View File

@ -29,6 +29,7 @@ module Vagrant
@app = app @app = app
@callable = callable @callable = callable
@block = block @block = block
@child_app = nil
end end
def call(env) def call(env)
@ -42,11 +43,17 @@ module Vagrant
@block.call(new_env, builder) @block.call(new_env, builder)
# Run the result with our new environment # Run the result with our new environment
final_env = runner.run(builder, new_env) @child_app = builder.to_app(new_env)
final_env = runner.run(@child_app, new_env)
# Call the next step using our final environment # Call the next step using our final environment
@app.call(final_env) @app.call(final_env)
end end
def recover(env)
# Call back into our compiled application and recover it.
@child_app.recover(env) if @child_app
end
end end
end end
end end

View File

@ -49,15 +49,17 @@ module Vagrant
# Something went horribly wrong. Start the rescue chain then # Something went horribly wrong. Start the rescue chain then
# reraise the exception to properly kick us out of limbo here. # reraise the exception to properly kick us out of limbo here.
begin_rescue(env) recover(env)
raise raise
end end
end end
# Begins the recovery sequence for all middlewares which have run. # We implement the recover method ourselves in case a Warden is
# It does this by calling `recover` (if it exists) on each middleware # embedded within another Warden. To recover, we just do our own
# which has already run, in reverse order. # recovery process on our stack.
def begin_rescue(env) def recover(env)
@logger.info("Beginning recovery process...")
@stack.each do |act| @stack.each do |act|
if act.respond_to?(:recover) if act.respond_to?(:recover)
@logger.info("Calling recover: #{act}") @logger.info("Calling recover: #{act}")
@ -65,6 +67,8 @@ module Vagrant
end end
end end
@logger.info("Recovery complete.")
# Clear stack so that warden down the middleware chain doesn't # Clear stack so that warden down the middleware chain doesn't
# rescue again. # rescue again.
@stack.clear @stack.clear

View File

@ -52,4 +52,63 @@ describe Vagrant::Action::Builtin::Call do
received.should == :bar received.should == :bar
end end
it "should call the recover method for the sequence in an error" do
# Basic variables
callable = lambda { |env| }
# Build the steps for the test
basic_step = Class.new do
def initialize(app, env)
@app = app
@env = env
end
def call(env)
@app.call(env)
end
end
step_a = Class.new(basic_step) do
def call(env)
env[:steps] << :call_A
super
end
def recover(env)
env[:steps] << :recover_A
end
end
step_b = Class.new(basic_step) do
def call(env)
env[:steps] << :call_B
super
end
def recover(env)
env[:steps] << :recover_B
end
end
instance = described_class.new(app, env, callable) do |_env, builder|
builder.use step_a
builder.use step_b
end
env[:steps] = []
instance.call(env)
instance.recover(env)
env[:steps].should == [:call_A, :call_B, :recover_B, :recover_A]
end
it "should recover even if it failed in the callable" do
callable = lambda { |env| raise "error" }
instance = described_class.new(app, env, callable) { |_env, _builder| }
instance.call(env) rescue nil
expect { instance.recover(env) }.
to_not raise_error
end
end end