spec/redis_mutex_spec.rb in redis-mutex-1.3.5 vs spec/redis_mutex_spec.rb in redis-mutex-2.0.0

- old
+ new

@@ -1,51 +1,81 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') +require 'spec_helper' +SHORT_MUTEX_OPTIONS = { :block => 0.1, :sleep => 0.02 } + +class C + include Redis::Mutex::Macro + auto_mutex :run_singularly, :block => 0, :after_failure => lambda {|id| return "failure: #{id}" } + + def run_singularly(id) + sleep 0.1 + return "success: #{id}" + end +end + describe Redis::Mutex do before do Redis::Classy.flushdb - @short_mutex_options = { :block => 0.1, :sleep => 0.02 } end - after do + after :all do Redis::Classy.flushdb + Redis::Classy.quit end - it "should set the value to the expiration" do + it 'locks the universe' do + mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + mutex1.lock.should be_true + + mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + mutex2.lock.should be_false + end + + it 'fails to lock when the lock is taken' do + mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + + mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + mutex2.lock.should be_true # mutex2 beats us to it + + mutex1.lock.should be_false # fail + end + + it 'unlocks only once' do + mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + mutex.lock.should be_true + + mutex.unlock.should be_true # successfully released the lock + mutex.unlock.should be_false # the lock no longer exists + end + + it 'prevents accidental unlock from outside' do + mutex1 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + mutex1.lock.should be_true + + mutex2 = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) + mutex2.unlock.should be_false + end + + it 'sets expiration' do start = Time.now expires_in = 10 mutex = Redis::Mutex.new(:test_lock, :expire => expires_in) - mutex.lock do + mutex.with_lock do mutex.get.to_f.should be_within(1.0).of((start + expires_in).to_f) end - # key should have been cleaned up - mutex.get.should be_nil + mutex.get.should be_nil # key should have been cleaned up end - it "should get a lock when existing lock is expired" do - mutex = Redis::Mutex.new(:test_lock) - # locked in the far past + it 'overwrites a lock when existing lock is expired' do + # stale lock from the far past Redis::Mutex.set(:test_lock, Time.now - 60) + mutex = Redis::Mutex.new(:test_lock) mutex.lock.should be_true - mutex.get.should_not be_nil - mutex.unlock - mutex.get.should be_nil end - it "should not get a lock when existing lock is still effective" do - mutex = Redis::Mutex.new(:test_lock, @short_mutex_options) - - # someone beats us to it - mutex2 = Redis::Mutex.new(:test_lock, @short_mutex_options) - mutex2.lock - - mutex.lock.should be_false # should not have the lock - mutex.get.should_not be_nil # lock value should still be set - end - - it "should not remove the key if lock is held past expiration" do + it 'fails to unlock the key if it took too long past expiration' do mutex = Redis::Mutex.new(:test_lock, :expire => 0.1, :block => 0) mutex.lock.should be_true sleep 0.2 # lock expired # someone overwrites the expired lock @@ -54,41 +84,70 @@ mutex.unlock mutex.get.should_not be_nil # lock should still be there end - it "should ensure unlock when something goes wrong in the block" do + it 'ensures unlocking when something goes wrong in the block' do mutex = Redis::Mutex.new(:test_lock) begin - mutex.lock do + mutex.with_lock do raise "Something went wrong!" end - rescue - mutex.locking.should be_false + rescue RuntimeError + mutex.get.should be_nil end end - it "should reset locking state on reuse" do - mutex = Redis::Mutex.new(:test_lock, @short_mutex_options) + it 'resets locking state on reuse' do + mutex = Redis::Mutex.new(:test_lock, SHORT_MUTEX_OPTIONS) mutex.lock.should be_true mutex.lock.should be_false end - describe Redis::Mutex::Macro do - it "should add auto_mutex" do + it 'returns value of block' do + Redis::Mutex.with_lock(:test_lock) { :test_result }.should == :test_result + end - class C - include Redis::Mutex::Macro - auto_mutex :run_singularly, :block => 0, :after_failure => lambda {|id| return "failure: #{id}" } + it 'requires block for #with_lock' do + expect { Redis::Mutex.with_lock(:test_lock) }.to raise_error(LocalJumpError) #=> no block given (yield) + end - def run_singularly(id) - sleep 0.1 - return "success: #{id}" - end - end + it 'raises LockError if lock not obtained' do + expect { Redis::Mutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to_not raise_error + expect { Redis::Mutex.lock!(:test_lock, SHORT_MUTEX_OPTIONS) }.to raise_error(Redis::Mutex::LockError) + end + it 'raises UnlockError if lock not obtained' do + mutex = Redis::Mutex.new(:test_lock) + mutex.lock.should be_true + mutex.unlock.should be_true + expect { mutex.unlock! }.to raise_error(Redis::Mutex::UnlockError) + end + + it 'raises AssertionError when block is given to #lock' do + # instance method + mutex = Redis::Mutex.new(:test_lock) + expect { mutex.lock {} }.to raise_error(Redis::Mutex::AssertionError) + + # class method + expect { Redis::Mutex.lock(:test_lock) {} }.to raise_error(Redis::Mutex::AssertionError) + end + + it 'sweeps expired locks' do + Redis::Mutex.set(:past, Time.now.to_f - 60) + Redis::Mutex.set(:present, Time.now.to_f) + Redis::Mutex.set(:future, Time.now.to_f + 60) + Redis::Mutex.keys.size.should == 3 + Redis::Mutex.sweep.should == 2 + Redis::Mutex.keys.size.should == 1 + end + + describe Redis::Mutex::Macro do + it 'adds auto_mutex' do t1 = Thread.new { C.new.run_singularly(1).should == "success: 1" } - sleep 0.01 # In most cases t1 wins, but make sure to give it a head start, not exceeding the sleep inside the method + # In most cases t1 wins, but make sure to give it a head start, + # not exceeding the sleep inside the method. + sleep 0.01 t2 = Thread.new { C.new.run_singularly(2).should == "failure: 2" } t1.join t2.join end end