What's an "Advisory Lock"?
An advisory lock is a mutex used to ensure no two processes run some process at the same time. When the advisory lock is powered by your database server, as long as it isn't SQLite, your mutex spans hosts.
User is an ActiveRecord model, and
lock_name is some string:
User.with_advisory_lock(lock_name) do do_something_that_needs_locking end
- The thread will wait indefinitely until the lock is acquired.
- While inside the block, you will exclusively own the advisory lock.
- The lock will be released after your block ends, even if an exception is raised in the block.
Lock wait timeouts
with_advisory_lock takes an options hash as the second parameter.
timeout_seconds option defaults to
nil, which means wait indefinitely for the lock.
A value of zero will try the lock only once. If the lock is acquired, the block will be yielded to. If the lock is currently being held, the block will not be called.
Note that if a non-nil value is provided for
timeout_seconds, the block will not be invoked if
the lock cannot be acquired within that time-frame.
For backwards compatability, the timeout value can be specified directly as the second parameter.
shared option defaults to
false which means an exclusive lock will be obtained.
true will allow locks to be obtained by multiple actors
as long as they are all shared locks.
Note: MySQL does not support shared locks.
PostgreSQL supports transaction-level locks which remain held until the transaction completes.
You can enable this by setting the
transaction option to
Note: transaction-level locks will not be reflected by
.current_advisory_lock when the block has returned.
The return value of
with_advisory_lock_result is a
which has a
lock_was_acquired? method and a
result accessor method, which is
the returned value of the given block. If your block may validly return false, you should use
The return value of
with_advisory_lock will be the result of the yielded block,
if the lock was able to be acquired and the block yielded, or
false, if you provided
a timeout_seconds value and the lock was not able to be acquired in time.
Testing for the current lock status
If you needed to check if the advisory lock is currently being held, you can call
Tag.advisory_lock_exists?("foo"), but realize the lock can be acquired between the time you
test for the lock, and the time you try to acquire the lock.
If you want to see if the current Thread is holding a lock, you can call
which will return the name of the current lock. If no lock is currently held,
Add this line to your application's Gemfile:
And then execute:
First off, know that there are lots of different kinds of locks available to you. Pick the finest-grain lock that ensures correctness. If you choose a lock that is too coarse, you are unnecessarily blocking other processes.
These are named mutexes that are inherently "application level"—it is up to the application to acquire, run a critical code section, and release the advisory lock.
If you're building a CRUD application, this will be your most commonly used lock.
Provided through something like the monogamy gem, these prevent concurrent access to any instance of a model. Their coarseness means they aren't going to be commonly applicable, and they can be a source of deadlocks.
Transactions and Advisory Locks
Advisory locks with MySQL and PostgreSQL ignore database transaction boundaries.
You will want to wrap your block within a transaction to ensure consistency.
MySQL doesn't support nesting
With MySQL (at least <= v5.5), if you ask for a different advisory lock within a
you will be releasing the parent lock (!!!). A
NestedAdvisoryLockErrorwill be raised
in this case. If you ask for the same lock name,
with_advisory_lock won't ask for the
lock again, and the block given will be yielded to.
Is clustered MySQL supported?
There are many
lock-* files in my project directory after test runs
This is expected if you aren't using MySQL or Postgresql for your tests. See issue 3.
SQLite doesn't have advisory locks, so we resort to file locking, which will only work
FLOCK_DIR is set consistently for all ruby processes.
minitest_helper.rb, add a
before do ENV['FLOCK_DIR'] = Dir.mktmpdir end after do FileUtils.remove_entry_secure ENV['FLOCK_DIR'] end
- Joshua Flanagan added a SQL comment to the lock query for PostgreSQL. Thanks!
- Fernando Luizão found a spurious requirement for
thread_safe. Thanks for the fix!
- Joel Turkel added
require 'active_support'(it was required, but relied on downstream gems to pull in active_support before pulling in with_advisory_lock). Thanks!
- Jason Weathered Added new shared and transaction-level lock options (Pull request 21). Thanks!
- Added ActiveRecord 5.0 to build matrix. Dropped 3.2, 4.0, and 4.1 (which no longer get security updates: http://rubyonrails.org/security/)
- Replaced ruby 1.9 and 2.0 (both EOL) with ruby 2.2 and 2.3 (see https://www.ruby-lang.org/en/downloads/)
- Added jruby/PostgreSQL support for Rails 4.x
- Reworked threaded tests to allow jruby tests to pass
yield_with_locknow return instances of
WithAdvisoryLock::Result, so blocks that return
falseare not misinterpreted as a failure to lock. As this changes the interface (albeit internal methods), the major version number was incremented.
with_advisory_lock_resultwas introduced, which clarifies whether the lock was acquired versus the yielded block returned false.
- Lock timeouts of 0 now attempt the lock once, as per suggested by Jon Leighton and implemented by Abdelkader Boudih. Thanks to both of you!
- Pull request 11 fixed a downstream issue with jruby support! Thanks, Aaron Todd!
- Added Travis tests for jruby
- Dropped support for Rails 3.0, 3.1, and Ruby 1.8.7, as they are no longer receiving security patches. See http://rubyonrails.org/security/ for more information. This required the major version bump.
advisory_lock_exists?to use existing functionality
- Fixed sqlite's implementation so parallel tests could be run against it
- Releasing 1.0.0. The interface will be stable.
advisory_lock_exists?. Thanks, Sean Devine, for the great pull request!
- Added Travis test for Rails 4.1
- Explicitly added MIT licensing to the gemspec.
- Merged in Postgis Adapter Support to address issue 7 Thanks for the pull request, Abdelkader Boudih!
- The database switching code had to be duplicated by Closure Tree,
so I extracted a new
- Builds were failing on Travis, so I introduced a global lock prefix that can be set with the
WITH_ADVISORY_LOCK_PREFIXenvironment variable. I'm not going to advertise this feature yet. It's a secret. Only you and I know, now. shhh
- Addressed issue 5 by using a deterministic hash for Postgresql + MRI >= 1.9. Thanks for the pull request, Joel Turkel!
- Addressed issue 2 by using a cache-busting query for MySQL and Postgres to deal with AR value caching bug. Thanks for the pull request, Jaime Giraldo!
- Addressed issue 4 by
adding support for
em-postgresql-adapter. Thanks, lestercsp!
(Hey, github—your notifications are WAY too easy to ignore!)
- Added Travis tests for Rails 3.0, 3.1, 3.2, and 4.0
- Fixed MySQL bug with select_value returning a string instead of an integer when using AR 3.0.x
- Only require ActiveRecord >= 3.0.x
- Fixed MySQL error reporting
- Asking for the currently acquired advisory lock doesn't re-ask for the lock now.
- Introduced NestedAdvisoryLockError when asking for different, nested advisory locksMySQL
- Moved require into on_load, which should speed loading when AR doesn't have to spin up
- Fought with ActiveRecord 3.0.x and 3.1.x. You don't want them if you use threads—they fail predictably.
- Added warning log message for nested MySQL lock calls
- Randomized lock wait time, which can help ameliorate lock contention
- First whack