A distributed lock implementation based on SQLAlchemy.
# Install with all dependency groups
uv sync --all
# Install with specific groups
uv sync --group test
uv sync --group typecheck
uv sync --group docs
# Run tests (requires databases running)
python -m unittest
# Type checking
uv run --no-dev mypy
# Linting and formatting
ruff check .
ruff format .
# Build package
uv build# Start test databases (MySQL, PostgreSQL, MSSQL, Oracle)
docker compose -f db.docker-compose.yml up
# Run full test matrix across Python and SQLAlchemy versions
cd tests
docker compose up --abort-on-container-exitNote: Oracle testing requires Oracle Database Enterprise/Standard Edition. Oracle Database Free (23c/23ai) does NOT support
DBMS_LOCK.REQUESTwhich is required for distributed lock functionality. This is a fundamental limitation of the Free/Express edition, not related to the container image flavor.
For local Oracle testing, ensure you have a full Oracle Database installation or use the official Oracle image:
docker compose -f db.docker-compose.yml up# Install pre-commit hooks
pre-commit install
# Run manually
pre-commit run --all-filesEntry Points: create_sadlock(), create_async_sadlock() in factory.py
Factory Pattern:
- Inspect SQLAlchemy engine name from connection/session
- Look up lock class in registry.py (
REGISTRYfor sync,ASYNCIO_REGISTRYfor async) - Instantiate database-specific lock implementation
Key Directories:
- lock/ - Database-specific lock implementations
- base.py -
BaseSadLock(sync),BaseAsyncSadLock(async) - mysql.py - MySQL/MariaDB named locks
- postgresql.py - PostgreSQL advisory locks
- mssql.py - MSSQL application locks
- oracle.py - Oracle user locks
- base.py -
- statement/ - SQL statement templates for each database
- registry.py - Engine name to lock class mapping
Test Structure:
- tests/ - Synchronous tests
- tests/asyncio/ - Asynchronous tests
- tests/engines.py - Test database connection factory
- Thread-local locks:
BaseSadLockextendsthreading.local- lock objects CANNOT be safely passed between threads. Each thread must create its own lock instance. - MySQL re-entrant behavior: MySQL allows acquiring the same named lock multiple times on the same connection. This is NOT true mutual exclusion.
- Lock lifetime: Locks are tied to database connections. Closing a connection releases all associated locks.
- Key hashing: PostgreSQL and Oracle convert string keys to 64-bit integers via BLAKE2b hash.
- PostgreSQL timeout: Implemented through polling, may have ~1 second variance.
- Oracle lock ID range: 0-1073741823 (uses
DBMS_LOCK.REQUEST) - Oracle Free limitation: Oracle Database Free (23c/23ai) does NOT support
DBMS_LOCK.REQUEST. This is a fundamental limitation of the Free/Express edition. CI skips Oracle tests; test locally withdocker compose -f db.docker-compose.yml upwhich uses the official Oracle image. - MSSQL driver: Requires ODBC driver installation (
msodbcsql18on Ubuntu)
- MSSQL container: Requires at least 2GB RAM
- Oracle container: Requires at least 2GB RAM
contextual_timeoutparameter ONLY affectswithstatements, not directacquire()calls- Async lock classes are separate (
*AsyncSadLock) but defined in same modules as sync variants
Python: 3.9+ (CI tests 3.10-3.14)
Testing Databases: Optional Docker services in db.docker-compose.yml
- MySQL:
mysql://test:test@127.0.0.1:3306/test - PostgreSQL:
postgresql://postgres:test@127.0.0.1:5432/ - MSSQL:
mssql+pyodbc://sa:YourStrongPassword123@127.0.0.1:1433/master - Oracle:
oracle+oracledb://sys:YourStrong@Passw0rd@127.0.0.1:1521/?service_name=FREEPDB1
Environment Variables: Test URLs can be set via TEST_URLS and TEST_ASYNC_URLS, or loaded from tests/.env
- Formatter: Ruff (line length: 128)
- Linter: Ruff with import sorting (
I) - Type checker: mypy
- Source layout:
src/directory with setuptools - Config: .ruff.toml, .mypy.ini
When adding support for a new database:
- Create lock implementation in lock/ inheriting from
BaseSadLock/BaseAsyncSadLock - Create SQL templates in statement/
- Add entry to
REGISTRYandASYNCIO_REGISTRYin registry.py - Add tests in tests/ and tests/asyncio/
- Update db.docker-compose.yml if testing locally