Add new middleware builtin: Lock

This will do a process lock by flocking a file. If it fails, it will
raise an exception of choice.
This commit is contained in:
Mitchell Hashimoto 2013-01-28 17:08:37 -08:00
parent eed24a08e3
commit 9ae3a373c8
3 changed files with 115 additions and 0 deletions

View File

@ -15,6 +15,7 @@ module Vagrant
autoload :ConfigValidate, "vagrant/action/builtin/config_validate"
autoload :EnvSet, "vagrant/action/builtin/env_set"
autoload :GracefulHalt, "vagrant/action/builtin/graceful_halt"
autoload :Lock, "vagrant/action/builtin/lock"
autoload :Provision, "vagrant/action/builtin/provision"
autoload :SSHExec, "vagrant/action/builtin/ssh_exec"
autoload :SSHRun, "vagrant/action/builtin/ssh_run"

View File

@ -0,0 +1,47 @@
module Vagrant
module Action
module Builtin
# This class creates a multi-process lock using `flock`. The lock
# is active for the remainder of the middleware stack.
class Lock
def initialize(app, env, options=nil)
@app = app
@options ||= options || {}
raise ArgumentError, "Please specify a lock path" if !@options[:path]
raise ArgumentError, "Please specify an exception." if !@options[:exception]
end
def call(env)
lock_path = @options[:path]
env_key = "has_lock_#{lock_path}"
if !env[env_key]
# If we already have the key in our environment we assume the
# lock is held by our middleware stack already and we allow
# nesting.
File.open(lock_path, "w+") do |f|
# The file locking fails only if it returns "false." If it
# succeeds it returns a 0, so we must explicitly check for
# the proper error case.
if f.flock(File::LOCK_EX | File::LOCK_NB) === false
raise @options[:exception]
end
# Set that we gained the lock and call deeper into the
# middleware, but make sure we UNSET the lock when we leave.
begin
env[env_key] = true
@app.call(env)
ensure
env[env_key] = false
end
end
else
# Just call up the middleware because we already hold the lock
@app.call(env)
end
end
end
end
end
end

View File

@ -0,0 +1,67 @@
require File.expand_path("../../../../base", __FILE__)
describe Vagrant::Action::Builtin::Lock do
let(:app) { lambda { |env| } }
let(:env) { {} }
let(:lock_path) do
@__lock_path = Tempfile.new("vagrant-test-lock")
@__lock_path.path.to_s
end
let(:options) do
{
:exception => Class.new(StandardError),
:path => lock_path
}
end
it "should require a path" do
expect { described_class.new(app, env) }.
to raise_error(ArgumentError)
expect { described_class.new(app, env, :path => "foo") }.
to raise_error(ArgumentError)
expect { described_class.new(app, env, :exception => "foo") }.
to raise_error(ArgumentError)
expect { described_class.new(app, env, :path => "bar", :exception => "foo") }.
to_not raise_error
end
it "should call the middleware with the lock held" do
inner_acquire = true
app = lambda do |env|
File.open(lock_path, "w+") do |f|
inner_acquire = f.flock(File::LOCK_EX | File::LOCK_NB)
end
end
instance = described_class.new(app, env, options)
instance.call(env)
inner_acquire.should == false
end
it "should raise an exception if the lock is already held" do
File.open(lock_path, "w+") do |f|
# Acquire lock
f.flock(File::LOCK_EX | File::LOCK_NB).should == 0
# Test!
instance = described_class.new(app, env, options)
expect { instance.call(env) }.
to raise_error(options[:exception])
end
end
it "should allow nesting locks within the same middleware sequence" do
called = false
app = lambda { |env| called = true }
inner = described_class.new(app, env, options)
outer = described_class.new(inner, env, options)
outer.call(env)
called.should == true
end
end