Low commit activity in last 3 years
No release in over a year
Uses atomic writes to DynamoDB to ensure you hold an exclusive lock on your records
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

>= 0
 Project Readme

DynamoidLockable

DynamoidLockable provides a interface for pessimistic locking for Dynamoid models.

Installation

Add this line to your application's Gemfile:

gem 'dynamoid_lockable'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install dynamoid_lockable

Usage

class MyModel
  include Dynamoid::Document
  include DynamoidLockable

  # Models may have more than one lock discriminated by name.
  # Creates a lock named :importing, which locks by default for 5 minutes and will refresh the lock every 10 seconds.
  # Default values:
  #  lock_for: 10 minutes
  #  relock_every: 1/3 of lock_for
  locks_with :importing, lock_for: 5 * 60, relock_every: 10
end

thing = MyModel.find('abcd')

# Most basic usage
thing.perform_with_lock(:importing) do
  # do whatever you like in here, record will remain locked as long as this block is executing and the
  # thread is able to do work
end

# Manually locking and unlocking
thing.lock(:importing)
# WARNING! Lock is not persisted, you have 5 minutes to complete your task
do_work(thing)
thing.unlock(:importing)

# You can also search by entities which are lockable (unlocked, locks expired or eleigible for reentry)
search = MyModel.lockable(:importing).first

# Note that `lockable` returns a dynamoid_advanced_where chain, so additional where clauses may be appended

# Locks support reentry from the same thread
thing.lock(:importing)
thing.lock(:importing) # No error

Thread.new do
  thing.lock(:importing) # Error: DynamoidLockable::CouldNotAcquireLock
end

A note on the options:

  • locks_for - should be a reasonably high number, setting it too small will result in excessive DynamoDB usage
  • relock_every - should be a positive number, nil or 0 will result in the relock process being disabled

Errors

  • If unable to obtain a lock, DynamoidLockable::CouldNotAcquireLock is raised
  • If unable to obtain to release a lock, DynamoidLockable::CouldNotUnlock is raised

Testing Lockable Items

By default when calling perform_with_lock, IDs used for locks are based on a unique ID assigned to each thread. If you want to write tests in your application to ensure locking is behaving how you desire, there are two ways. The easiest way is to simply manually call lock with a specified locker_name. Example:

describe 'locking works' do
  before do
    my_object.lock(:thing_in_progress, locker_name: 'tEsT_lOcKeR_nAmE')
  end

  it 'never calls some method that needs to acquire the lock' do
    expect(job).not_to receive(:my_locked_method!)
    job.perform
  end
end

The other way is to use perform_with_lock in an 'around' block, and then spawn a new thread. Example:

describe 'locking works' do
  around do |ex|
    my_object.perform_with_lock(:thing_in_progress) { ex.run }
  end

  it 'never calls some method that needs to acquire the lock' do
    expect(job).not_to receive(:my_locked_method!)
    Thread.new { job.perform }.join # spawn a thread, call your job method, then join the thread
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/GetTerminus/dynamoid_lockable.