Suo Build Status Code Climate Test Coverage Gem Version

:lock: 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

Time To Live

Suo::Client::Redis.new("bar_resource", ttl: 60) #ttl in seconds

A key representing a set of lockable resources is removed once the last resource lock is released and the ttl time runs out. When another lock is acquired and the key has been removed the key has to be recreated.

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: