#initial_set which is causing a double attempt and delay on lock acquisition
The call to `#initial_set` in `#retry` and `#acquire_lock` is followed by `next` which leads to a second pass through the `#retry_with_timeout` loop and a sleep call for up to `:acquisition_delay`. This delay isn't necessary if the value can be set without a race condition. Removing the `next` call causes the client to continue to retry because the transaction has been changed outside the transaction boundary: In Redis, calling `SET` within a `WATCH`/`UNWATCH` block but not inside a `MULTI`/`EXEC` block will [cause the EXEC to fail the transaction](https://github.com/antirez/redis-doc/issues/734), so the first `#set` call fails and it requires a second pass. To resolve this I changed `#initial_set` to call `#set` within a `MULTI` block so that it would be inside the transaction. In Memcache the call to `SET` without the `CAS` during `#initial_set` is going to cause the `SET` with `CAS` to fail (return `EXISTS`), and resulting in a second pass. To resolve this I changed `#initial_set` to use `SET` with `CAS` and return the CAS value to be used in the subsequent `#set` call that stores the lock token.
Suo

🔒 Distributed semaphores using Memcached or Redis in Ruby.
Suo provides a very performant distributed lock solution using Compare-And-Set (CAS) commands in Memcached, and WATCH/MULTI in Redis. It allows locking both single exclusion (like a mutex - sharing one resource), as well as multiple resources.
Installation
Add this line to your application’s Gemfile:
gem 'suo'
Usage
Basic
# Memcached
suo = Suo::Client::Memcached.new("foo_resource", connection: "127.0.0.1:11211")
# Redis
suo = Suo::Client::Redis.new("baz_resource", connection: {host: "10.0.1.1"})
# Pre-existing client
suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client)
suo.lock do
# critical code here
@puppies.pet!
end
# The resources argument is the number of resources the semaphore will allow to lock (defaulting to one - a mutex)
suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client, resources: 2)
Thread.new { suo.lock { puts "One"; sleep 2 } }
Thread.new { suo.lock { puts "Two"; sleep 2 } }
Thread.new { suo.lock { puts "Three" } }
# will print "One" "Two", but not "Three", as there are only 2 resources
# custom acquisition timeouts (time to acquire)
suo = Suo::Client::Memcached.new("protected_key", client: some_dalli_client, acquisition_timeout: 1) # in seconds
# manually locking/unlocking
# the return value from lock without a block is a unique token valid only for the current lock
# which must be unlocked manually
token = suo.lock
foo.baz!
suo.unlock(token)
# custom stale lock expiration (cleaning of dead locks)
suo = Suo::Client::Redis.new("other_key", client: some_redis_client, stale_lock_expiration: 60*5)
Stale locks
"Stale locks" - those acquired more than stale_lock_expiration (defaulting to 3600 or one hour) ago - are automatically cleared during any operation on the key (lock, unlock, refresh). The locked? method will not return true if only stale locks exist, but will not modify the key itself.
To re-acquire a lock in the middle of a block, you can use the refresh method on client.
suo = Suo::Client::Redis.new("foo")
# lock is the same token as seen in the manual example, above
suo.lock do |token|
5.times do
baz.bar!
suo.refresh(token)
end
end
TODO
- more race condition tests
History
View the changelog.
Contributing
Everyone is encouraged to help improve this project. Here are a few ways you can help:
- Report bugs
- Fix bugs and submit pull requests
- Write, clarify, or fix documentation
- Suggest or add new features