initial commit of the uncomplicated mutex
commit
443f78f545
|
@ -0,0 +1,2 @@
|
|||
vendor/
|
||||
.bundle
|
|
@ -0,0 +1,3 @@
|
|||
source 'https://rubygems.org'
|
||||
gem 'redis', '~> 3.0'
|
||||
gem 'minitest', '~> 5.0'
|
|
@ -0,0 +1,12 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
minitest (5.5.0)
|
||||
redis (3.2.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (~> 5.0)
|
||||
redis (~> 3.0)
|
|
@ -0,0 +1,8 @@
|
|||
require 'rake/testtask'
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << 'test'
|
||||
end
|
||||
|
||||
desc "Run tests"
|
||||
task :default => :test
|
|
@ -0,0 +1,72 @@
|
|||
require 'digest/md5'
|
||||
require 'redis'
|
||||
|
||||
class UncomplicatedMutex
|
||||
attr_reader :lock_name
|
||||
|
||||
MutexTimeout = Class.new(StandardError)
|
||||
|
||||
LUA_ACQUIRE = "return redis.call('SET', KEYS[1], ARGV[2], 'NX', 'EX', ARGV[1]) and redis.call('expire', KEYS[1], ARGV[1]) and 1 or 0"
|
||||
LUA_RELEASE = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
|
||||
|
||||
def initialize(obj, opts = {})
|
||||
@verbose = opts[:verbose]
|
||||
@timeout = opts[:timeout] || 300
|
||||
@fail_on_timeout = opts[:fail_on_timeout]
|
||||
@ticks = opts[:ticks] || 100
|
||||
@wait_tick = @timeout.to_f / @ticks.to_f
|
||||
@redis = opts[:redis] || Redis.new
|
||||
@lock_name = "lock:#{obj.class.name}:#{obj.id}".squeeze(":")
|
||||
@token = Digest::MD5.new.hexdigest("#{@lock_name}_#{Time.now.to_f}")
|
||||
end
|
||||
|
||||
def acquire_mutex
|
||||
puts("Running transaction to acquire the lock #{@lock_name}") if @verbose
|
||||
@redis.eval(LUA_ACQUIRE, [ @lock_name ], [ @timeout, @token ]) == 1
|
||||
end
|
||||
|
||||
def destroy_mutex
|
||||
puts("Destroying the lock #{@lock_name}") if @verbose
|
||||
@redis.del(@lock_name)
|
||||
end
|
||||
|
||||
def lock(&block)
|
||||
begin
|
||||
wait_for_mutex
|
||||
yield block
|
||||
ensure
|
||||
release_mutex
|
||||
end
|
||||
end
|
||||
|
||||
def overwrite_mutex
|
||||
puts("Replacing the lock #{@lock_name} with #{@token}") if @verbose
|
||||
@redis.set(@lock_name, @token)
|
||||
end
|
||||
|
||||
def recurse_until_ready(depth = 1)
|
||||
return false if depth == @ticks
|
||||
wait_a_tick if depth > 1
|
||||
acquire_mutex || recurse_until_ready(depth + 1)
|
||||
end
|
||||
|
||||
def release_mutex
|
||||
puts("Releasing the lock #{@lock_name} if it still holds the value '#{@token}'") if @verbose
|
||||
@redis.eval(LUA_RELEASE, [ @lock_name ], [ @token ])
|
||||
end
|
||||
|
||||
def wait_a_tick
|
||||
puts("Sleeping #{@wait_tick} for the lock #{@lock_name} to become available") if @verbose
|
||||
sleep(@wait_tick)
|
||||
end
|
||||
|
||||
def wait_for_mutex
|
||||
if recurse_until_ready
|
||||
puts("Acquired lock #{@lock_name}") if @verbose
|
||||
else
|
||||
puts("Failed to acquire the lock") if @verbose
|
||||
raise MutexTimeout.new("Failed to acquire the lock") if @fail_on_timeout
|
||||
overwrite_mutex
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
require 'redis'
|
||||
require 'uncomplicated_mutex'
|
||||
require 'minitest'
|
||||
require 'minitest/autorun'
|
||||
|
||||
class TestUncomplicatedMutex < Minitest::Test
|
||||
class SlowObject
|
||||
attr_accessor :id
|
||||
def initialize
|
||||
@id = Time.now.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def setup
|
||||
@obj = SlowObject.new
|
||||
@redis = Redis.new
|
||||
default_opts = { timeout: 1, ticks: 10, redis: @redis }
|
||||
@mutex1 = UncomplicatedMutex.new(@obj, default_opts)
|
||||
@mutex2 = UncomplicatedMutex.new(@obj, default_opts)
|
||||
@lock_name = @mutex1.lock_name
|
||||
end
|
||||
|
||||
def test_mutex_works
|
||||
@mutex1.lock do
|
||||
assert_equal @redis.exists(@lock_name), true
|
||||
end
|
||||
assert_equal @redis.exists(@lock_name), false
|
||||
end
|
||||
|
||||
def test_sequential_access
|
||||
@redis.set('lock:testvalue', 1)
|
||||
@mutex1.lock do
|
||||
@redis.set('lock:testvalue', 2)
|
||||
end
|
||||
@mutex2.lock do
|
||||
assert_equal(@redis.get('lock:testvalue'), '2')
|
||||
@redis.del('lock:testvalue')
|
||||
end
|
||||
assert_equal @redis.exists('lock:testvalue'), false
|
||||
end
|
||||
|
||||
def test_exception_is_thrown
|
||||
begin
|
||||
@redis.set(@lock_name, 1)
|
||||
UncomplicatedMutex.new(@obj, { timeout: 1, fail_on_timeout: true, ticks: 10 }).lock do
|
||||
sleep 2
|
||||
end
|
||||
rescue UncomplicatedMutex::MutexTimeout
|
||||
pass "Exception thrown"
|
||||
else
|
||||
flunk "Exception was not thrown"
|
||||
ensure
|
||||
@redis.del(@lock_name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_exception_is_not_thrown
|
||||
begin
|
||||
@redis.set(@lock_name, 1)
|
||||
@mutex2.lock do
|
||||
sleep 1.05
|
||||
end
|
||||
rescue UncomplicatedMutex::MutexTimeout
|
||||
flunk "Exception thrown"
|
||||
else
|
||||
pass "Exception was not thrown"
|
||||
ensure
|
||||
@redis.del(@lock_name)
|
||||
end
|
||||
end
|
||||
|
||||
def test_lock_is_not_overwritten
|
||||
@mutex1.lock do
|
||||
@redis.set(@lock_name, 'abc123')
|
||||
end
|
||||
assert_equal(@redis.get(@lock_name), 'abc123')
|
||||
@redis.del(@lock_name)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
Gem::Specification.new do |s|
|
||||
s.name = 'uncomplicated-mutex'
|
||||
s.version = '1.0.0'
|
||||
s.date = '2014-12-22'
|
||||
s.summary = 'Redis. Lua. Mutex.'
|
||||
s.description = 'A mutex that uses Redis that is also not complicated.'
|
||||
s.authors = [ 'Andrew Coleman' ]
|
||||
s.email = 'penguincoder@gmail.com'
|
||||
s.files = [ 'lib/uncomplicated_mutex.rb' ]
|
||||
s.homepage = 'https://github.com/penguincoder/uncomplicated_mutex'
|
||||
s.license = 'MIT'
|
||||
|
||||
s.add_runtime_dependency 'redis', '~> 3.0'
|
||||
s.add_development_dependency 'minitest', '~> 5.0'
|
||||
end
|
Loading…
Reference in New Issue