Uses Postgres advisory locks to enable you to syncronize actions across processes and machines.
This gem requires Ruby 2.1+
Add this line to your application's Gemfile:
gem 'pg_lock'
And then execute:
$ bundle
Or install it yourself as:
$ gem install pg_lock
Create a PgLock.new
instance and call the lock
method to ensure exclusive execution of a block of code.
PgLock.new(name: "all_your_base").lock do
# stuff
end
Now no matter how many times this code is executed across any number of machines, one block of code will be allowed to execute at a time.
The postgres lock is unique across different database sessions, if the same session tries to aquire the same lock it will succeed. So while PgLock
will guarantee unique execution across machines and processes, it will not block the same process (sharing the same connection session) from running. For example while you would think the middle block would not run in this example:
key = "all_your_base"
PgLock.new(name: key).lock do
puts "First block called"
PgLock.new(name: key).lock do
puts "Second block called because it's sharing the same session"
end
end
The result will be:
First block called
Second block called because it's sharing the same session
If you need to syncronize code execution inside of the same process you should use a mutex.
Transaction level lock can be acquired inside of a transaction using PgLock#lock_for_transaction
. If you try to acquire the same lock inside the same transaction multiple times it will not block.
Transaction level locks cannot be manually released. They are released when transactions ends.
connection = ActiveRecord::Base.connection.raw_connection
lock = PgLock.new(name: 'resource_lock', connection: connection)
connection.exec('BEGIN')
lock.lock_for_transaction
puts 'Do some work while resource is locked.'
connection.exec('COMMIT')
By default, locked blocks will timeout after 60 seconds of execution, the lock will be released and any code executing will be terminated by a Timeout::Error
will be raised. You can lower or raise this value by passing in a ttl
(time to live) argument:
begin
PgLock.new(name: "all_your_base", ttl: 30).lock do
# stuff
end
rescue Timeout::Error
puts "Took longer than 30 seconds to execute"
end
To disable the timeout pass in a falsey value:
PgLock.new(name: "all_your_base", ttl: false).lock do
# stuff
end
By default if a lock cannot be aquired, PgLock
will try 3 times with a 1 second delay between tries. You can configure this behavior using attempts
and attempt_interval
arguments:
PgLock.new(name: "all_your_base", attempts: 10, attempt_interval: 5).lock do
# stuff
end
To run once use attempts: 1
.
You can optionally raise an error if a block cannot be executed in the given number of attempts by using the lock!
method:
begin
PgLock.new(name: "all_your_base").lock! do
# stuff
end
rescue PgLock::UnableToLockError
# do stuff
end
The create
method will return the PgLock
instance if a lock object was created, or false
if no lock was aquired. You should manually delete
a successfully created lock object:
begin
lock = PgLock.new(name: "all_your_base")
lock.create
# do stuff
ensure
lock.delete
end
You can check on the status of a lock with the acquired?
method:
begin
lock = PgLock.new(name: "all_your_base")
lock.create
if lock.acquired?
# do stuff
end
ensure
lock.delete
end
By default there is no logging, if you want you can provide a logging block:
PgLock.new(name: "all_your_base", log: ->(data) { puts data.inspect }).lock do
# stuff
end
One argument will be passed to the block, a hash. You can optionally define a default log for all instances:
PgLock::DEFAULT_LOG = ->(data) { puts data.inspect }
Note: When you enable logging exceptions raised when deleting a lock will be swallowed. To re-raise you can use the exception in data[:exception]
.
This library defaults to use Active Record. If you want to use another library, or spin up a dedicated connection you can use the connection
argument:
my_connection = MyCustomConnectionObject.new
PgLock.new(name: "all_your_base", connection: my_connection).lock do
# stuff
end
The object needs to respond to the exec
method where the first argument is a query string, and the second is an array of bind arguments. For example to use with sequel you could do something like this:
connection = Module do
def self.exec(sql, bind)
DB.fetch(sql, bind)
end
end
PgLock.new(name: "all_your_base", connection: my_connection).lock do
# stuff
end
Where DB
is to be your database connection.
After checking out the repo, run bin/setup
to install dependencies. Then, run bin/console
for an interactive prompt that will allow you to experiment.
To run tests you'll need a database:
$ createdb pg_lock_test
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
to create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
- Fork it ( https://github.com/[my-github-username]/pg_lock/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Originally written by @mikehale