Skip to main content

pg-advisory-lock

Pythonic PostgreSQL advisory locking for SQLAlchemy, with structured keys and explicit contention handling.

Provides a high-level interface for acquiring PostgreSQL advisory locks with structured lock arguments and timeout support. Advisory locks are application-level locks that are automatically released when the transaction commits or rolls back.

Key features:

  • Deterministic lock keys composed from str, int, and uuid.UUID components
  • Non-blocking immediate lock acquisition
  • Blocking lock acquisition with configurable timeout
  • Automatic lock release on transaction end (uses pg_advisory_xact_lock functions)
  • Configurable handling for expected lock contention: raise exceptions (default) or return a boolean
  • Database and infrastructure errors are propagated unchanged
  • Inline type hints shipped with the package (PEP 561)

Failure handling

raise_on_failure controls expected lock contention only:

  • raise_on_failure=True (default):
    • lock_immediate() raises PgAdvisoryLockNotAcquired when the lock is already held.
    • lock() raises PgAdvisoryLockTimeout when its timeout expires.
  • raise_on_failure=False:
    • Both methods return False for expected contention or timeout.
  • Unexpected database and infrastructure errors are always propagated.

raise_on_failure must be a boolean. Other values raise PgAdvisoryLockError.

PgAdvisoryLockTimeout is a subclass of PgAdvisoryLockNotAcquired.

Boolean mode discards the originating database error. Use the default exception mode when the cause matters: the underlying DBAPIError is chained as __cause__.

Usage examples:

Import the public API from the installed package:

from pg_advisory_lock import (
    PgAdvisoryLock,
    PgAdvisoryLockError,
    PgAdvisoryLockNotAcquired,
    PgAdvisoryLockTimeout,
)

Default behavior: raises exception on failure (safe by default):

locker = PgAdvisoryLock(lock_args=[UserModel.__table__.name, user_id])

with session.begin():
    locker.lock(session=session)
    process_payment()

Boolean mode: explicit handling of lock contention:

locker = PgAdvisoryLock(lock_args=[UserModel.__table__.name, user_id], raise_on_failure=False)

with session.begin():
    if locker.lock(session=session, timeout_in_ms=5000):
        process_payment()
    else:
        schedule_retry()

Immediate (non-blocking) attempts:

locker = PgAdvisoryLock(
    lock_args=['payment_processing', order_id],
    raise_on_failure=False,
)

with session.begin():
    if locker.lock_immediate(session=session):
        process_payment()
    else:
        schedule_retry()

Lock on multiple components:

locker = PgAdvisoryLock(lock_args=['user_sync', region, user_id])

with session.begin():
    locker.lock(session=session)
    synchronize_user()

Transaction behavior

The library uses transaction-scoped advisory locks. The critical section must remain inside the same open transaction in which the lock was acquired. Committing or rolling back that transaction releases the lock automatically.

When lock() reaches its timeout, the failed PostgreSQL statement is isolated within a savepoint. After False is returned or PgAdvisoryLockTimeout is caught, the caller's outer transaction remains usable.

lock() temporarily changes the transaction-local PostgreSQL lock_timeout. After a successful acquisition or handled timeout, the caller's previous lock_timeout value is restored.

A connection or infrastructure failure cannot be repaired by a savepoint and is propagated to the caller.

Lock arguments and key generation

lock_args must be a non-empty list or tuple. A bare string, set, mapping, generator, or other iterable is rejected.

The arguments are copied to an immutable tuple during construction, so later changes to the original list do not affect the lock identity.

For a lock with one component, wrap the value in a list or one-element tuple:

PgAdvisoryLock(lock_args=['payment_processing'])
PgAdvisoryLock(lock_args=('payment_processing',))

Each component must be a str, int, or uuid.UUID. Any other type, including None, SQLAlchemy model instances, model classes, and Table objects, raises PgAdvisoryLockError at construction.

The restriction exists because a value relying on Python's default repr stringifies with a memory address that differs in every process. Hashing one would derive a key that never matches another process, so the lock would appear granted while excluding nobody.

Pass the table name explicitly:

locker = PgAdvisoryLock(lock_args=[UserModel.__table__.name, user.id])

__table__.name avoids duplicating the table name in application code. Note that renaming the table changes the derived key, whereas a string literal would not.

The normalized strings are serialized as a compact JSON array, preserving component boundaries even when values contain separators. The serialized value is hashed with SHA-256 and mapped to PostgreSQL's signed 64-bit advisory-lock key range.

Normalization is intentionally type-insensitive, so values with the same normalized string representation, such as 1 and '1', produce the same lock key.

Thread safety

  • An instance keeps no state after construction and can be shared across sessions and threads.
  • Each thread needs its own Session; SQLAlchemy sessions are not thread-safe.
  • A single acquisition belongs to the session and transaction that made it.

Development

Running tests

Install the project and its development dependencies:

poetry install

Start the local PostgreSQL test database and run the test suite:

docker compose -f docker/compose.yaml up -d
poetry run pytest

Tests connect to the following database by default:

postgresql+psycopg://postgres:123@localhost:6543/testing

Override the database URL when using another PostgreSQL instance:

TEST_DATABASE_URL='postgresql+psycopg://user:password@host:5432/database' \
  poetry run pytest

Stop the local test database when it is no longer needed:

docker compose -f docker/compose.yaml down

License

MIT. See LICENSE.

Metadata

Release files for pg-advisory-lock 0.0.16

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pg-advisory-lock 0.0.16
File Size Uploaded
pg_advisory_lock-0.0.16.tar.gz 7.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pg-advisory-lock 0.0.16
File Interpreter ABI Platform
pg_advisory_lock-0.0.16-py3-none-any.whl Python 3 none any Details

Total release size: 17.6 kB

Release files / pg_advisory_lock-0.0.16.tar.gz

Download URL pg_advisory_lock-0.0.16.tar.gz
Size 7.8 kB
Tags Source
SHA-256 checksum
How to use checksums
67e18912c8a6e0d45aa55c9445000e3646522cd91b8d0ea545158fae6e2c4577
BLAKE2b-256 checksum
How to use checksums
ab15bcd24feb2f829c7704d2bd0912ab304bce15d4d0936cf709bf4709bb89cd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.8.3 CPython/3.12.13 Linux/6.8.0-139-generic

Release files / pg_advisory_lock-0.0.16-py3-none-any.whl

Download URL pg_advisory_lock-0.0.16-py3-none-any.whl
Size 9.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b952f5236df47dd41ce184a56062ba39faa44cce723e14be4bc546ee600b4655
BLAKE2b-256 checksum
How to use checksums
cd04191b99d2f5d3907459f3c41e73ea5b8276d378b7a87087e8898ead16a4e0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.8.3 CPython/3.12.13 Linux/6.8.0-139-generic

Release history Release notifications | RSS feed

0.1.0

2 release files

0.0.17

2 release files

This release

0.0.16 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page