Advisory Locks
Sometimes applications need to perform custom locking that cannot be easily expressed as row or table locks. The pglock.advisory decorator and context manager solves this by using Postgres advisory locks.
Basic Examples
pglock.advisory is a decorator and context manager that can be used to acquire an advisory lock. Once the lock is acquired, all other code requesting the lock will wait until the lock is released.
For example, below we've ensured there's only one instance of my_exclusive_function
running at a time:
import pglock
@pglock.advisory("my_module.my_exclusive_function")
def my_exclusive_function():
# All other calls of my_exclusive_function will wait for this one to finish
Tip
When used as a decorator, the lock ID isn't required and will default to <module_name>.<function_name>
.
When creating an advisory lock, remember that the lock ID is a global name across the entire database. Be sure to choose meaningful names, ideally with namespaces, when serializing code with pglock.advisory.
Configuring Lock Wait Time
By default, pglock.advisory will wait forever until the lock can be acquired. Use the timeout
argument to change this behavior. For example, timeout=0
will avoid waiting for the lock:
with pglock.advisory("my_lock_id", timeout=0) as acquired:
if not acquired:
# Do stuff if the lock cannot be acquired
else:
# The lock was acquired
As shown above, the context manager returns a flag to indicate if the lock was successfully acquired. Here we wait up to two seconds to acquire the lock:
Tip
Use a datetime.timedelta
argument for timeout
for better precision or None
to set an infinite timeout.
Side Effects
The side_effect
argument adjusts runtime characteristics when using a timeout. For example, below we're using timeout=0
and side_effect=pglock.Raise
to raise an exception if the lock cannot be acquired:
with pglock.advisory(timeout=0, side_effect=pglock.Raise):
# A django.db.utils.OperationalError will be thrown if the lock cannot be acquired.
Note
When using the decorator, the side effect defaults to pglock.Raise
since the return value is not available to the decorated function.
Use side_effect=pglock.Skip
to skip the function entirely if the lock cannot be acquired. This only applies to usage of the decorator:
@pglock.advisory(timeout=0, side_effect=pglock.Skip)
def one_function_at_a_time():
# This function runs once at a time. If this function runs anywhere else, it will be skipped.
Shared Locks
Advisory locks can be acquired in shared mode using shared=True
. Shared locks do not conflict with other shared locks. They only conflict with other exclusive locks of the same lock ID. See the Postgres docs for more information.