-
Notifications
You must be signed in to change notification settings - Fork 106
Add support for explicit table-level locks #205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
.. include:: ./snippets/postgres_doc_links.rst | ||
|
||
.. _locking_page: | ||
|
||
Locking | ||
======= | ||
|
||
`Explicit table-level locks`_ are supported through the :meth:`psqlextra.locking.postgres_lock_model` and :meth:`psqlextra.locking.postgres_lock_table` methods. All table-level lock methods are supported. | ||
|
||
Locks are always bound to the current transaction and are released when the transaction is committed or rolled back. There is no support (in PostgreSQL) for explicitly releasing a lock. | ||
|
||
.. warning:: | ||
|
||
Locks are only released when the *outer* transaction commits or when a nested transaction is rolled back. You can ensure that the transaction you created is the outermost one by passing the ``durable=True`` argument to ``transaction.atomic``. | ||
|
||
.. note:: | ||
|
||
Use `django-pglocks <https://pypi.org/project/django-pglocks/>`_ if you need a advisory lock. | ||
|
||
Locking a model | ||
--------------- | ||
|
||
Use :class:`psqlextra.locking.PostgresTableLockMode` to indicate the type of lock to acquire. | ||
|
||
.. code-block:: python | ||
|
||
from django.db import transaction | ||
|
||
from psqlextra.locking import PostgresTableLockMode, postgres_lock_table | ||
|
||
with transaction.atomic(durable=True): | ||
postgres_lock_model(MyModel, PostgresTableLockMode.EXCLUSIVE) | ||
|
||
# locks are released here, when the transaction committed | ||
|
||
|
||
Locking a table | ||
--------------- | ||
|
||
Use :meth:`psqlextra.locking.postgres_lock_table` to lock arbitrary tables in arbitrary schemas. | ||
|
||
.. code-block:: python | ||
|
||
from django.db import transaction | ||
|
||
from psqlextra.locking import PostgresTableLockMode, postgres_lock_table | ||
|
||
with transaction.atomic(durable=True): | ||
postgres_lock_table("mytable", PostgresTableLockMode.EXCLUSIVE) | ||
postgres_lock_table( | ||
"tableinotherschema", | ||
PostgresTableLockMode.EXCLUSIVE, | ||
schema_name="myschema" | ||
) | ||
|
||
# locks are released here, when the transaction committed |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
from enum import Enum | ||
from typing import Optional, Type | ||
|
||
from django.db import DEFAULT_DB_ALIAS, connections, models | ||
|
||
|
||
class PostgresTableLockMode(Enum): | ||
"""List of table locking modes. | ||
|
||
See: https://www.postgresql.org/docs/current/explicit-locking.html | ||
""" | ||
|
||
ACCESS_SHARE = "ACCESS SHARE" | ||
ROW_SHARE = "ROW SHARE" | ||
ROW_EXCLUSIVE = "ROW EXCLUSIVE" | ||
SHARE_UPDATE_EXCLUSIVE = "SHARE UPDATE EXCLUSIVE" | ||
SHARE = "SHARE" | ||
SHARE_ROW_EXCLUSIVE = "SHARE ROW EXCLUSIVE" | ||
EXCLUSIVE = "EXCLUSIVE" | ||
ACCESS_EXCLUSIVE = "ACCESS EXCLUSIVE" | ||
|
||
@property | ||
def alias(self) -> str: | ||
return ( | ||
"".join([word.title() for word in self.name.lower().split("_")]) | ||
+ "Lock" | ||
) | ||
|
||
|
||
def postgres_lock_table( | ||
table_name: str, | ||
lock_mode: PostgresTableLockMode, | ||
*, | ||
schema_name: Optional[str] = None, | ||
using: str = DEFAULT_DB_ALIAS, | ||
) -> None: | ||
"""Locks the specified table with the specified mode. | ||
|
||
The lock is held until the end of the current transaction. | ||
|
||
Arguments: | ||
table_name: | ||
Unquoted table name to acquire the lock on. | ||
|
||
lock_mode: | ||
Type of lock to acquire. | ||
|
||
schema_name: | ||
Optionally, the unquoted name of the schema | ||
the table to lock is in. If not specified, | ||
the table name is resolved by PostgreSQL | ||
using it's ``search_path``. | ||
|
||
using: | ||
Optional name of the database connection to use. | ||
""" | ||
|
||
connection = connections[using] | ||
|
||
with connection.cursor() as cursor: | ||
quoted_fqn = connection.ops.quote_name(table_name) | ||
if schema_name: | ||
quoted_fqn = ( | ||
connection.ops.quote_name(schema_name) + "." + quoted_fqn | ||
) | ||
|
||
cursor.execute(f"LOCK TABLE {quoted_fqn} IN {lock_mode.value} MODE") | ||
|
||
|
||
def postgres_lock_model( | ||
model: Type[models.Model], | ||
lock_mode: PostgresTableLockMode, | ||
*, | ||
using: str = DEFAULT_DB_ALIAS, | ||
schema_name: Optional[str] = None, | ||
) -> None: | ||
"""Locks the specified model with the specified mode. | ||
|
||
The lock is held until the end of the current transaction. | ||
|
||
Arguments: | ||
model: | ||
The model of which to lock the table. | ||
|
||
lock_mode: | ||
Type of lock to acquire. | ||
|
||
schema_name: | ||
Optionally, the unquoted name of the schema | ||
the table to lock is in. If not specified, | ||
the table name is resolved by PostgreSQL | ||
using it's ``search_path``. | ||
|
||
Django models always reside in the default | ||
("public") schema. You should not specify | ||
this unless you're doing something special. | ||
|
||
using: | ||
Optional name of the database connection to use. | ||
""" | ||
|
||
postgres_lock_table( | ||
model._meta.db_table, lock_mode, schema_name=schema_name, using=using | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import uuid | ||
|
||
import pytest | ||
|
||
from django.db import connection, models, transaction | ||
|
||
from psqlextra.locking import ( | ||
PostgresTableLockMode, | ||
postgres_lock_model, | ||
postgres_lock_table, | ||
) | ||
|
||
from .fake_model import get_fake_model | ||
|
||
|
||
@pytest.fixture | ||
def mocked_model(): | ||
return get_fake_model( | ||
{ | ||
"name": models.TextField(), | ||
} | ||
) | ||
|
||
|
||
def get_table_locks(): | ||
with connection.cursor() as cursor: | ||
return connection.introspection.get_table_locks(cursor) | ||
|
||
|
||
@pytest.mark.django_db(transaction=True) | ||
def test_postgres_lock_table(mocked_model): | ||
lock_signature = ( | ||
"public", | ||
mocked_model._meta.db_table, | ||
"AccessExclusiveLock", | ||
) | ||
with transaction.atomic(): | ||
postgres_lock_table( | ||
mocked_model._meta.db_table, PostgresTableLockMode.ACCESS_EXCLUSIVE | ||
) | ||
assert lock_signature in get_table_locks() | ||
|
||
assert lock_signature not in get_table_locks() | ||
|
||
|
||
@pytest.mark.django_db(transaction=True) | ||
def test_postgres_lock_table_in_schema(): | ||
schema_name = str(uuid.uuid4())[:8] | ||
table_name = str(uuid.uuid4())[:8] | ||
quoted_schema_name = connection.ops.quote_name(schema_name) | ||
quoted_table_name = connection.ops.quote_name(table_name) | ||
|
||
with connection.cursor() as cursor: | ||
cursor.execute(f"CREATE SCHEMA {quoted_schema_name}") | ||
cursor.execute( | ||
f"CREATE TABLE {quoted_schema_name}.{quoted_table_name} AS SELECT 'hello world'" | ||
) | ||
|
||
lock_signature = (schema_name, table_name, "ExclusiveLock") | ||
with transaction.atomic(): | ||
postgres_lock_table( | ||
table_name, PostgresTableLockMode.EXCLUSIVE, schema_name=schema_name | ||
) | ||
assert lock_signature in get_table_locks() | ||
|
||
assert lock_signature not in get_table_locks() | ||
|
||
|
||
@pytest.mark.parametrize("lock_mode", list(PostgresTableLockMode)) | ||
@pytest.mark.django_db(transaction=True) | ||
def test_postgres_lock_model(mocked_model, lock_mode): | ||
lock_signature = ( | ||
"public", | ||
mocked_model._meta.db_table, | ||
lock_mode.alias, | ||
) | ||
|
||
with transaction.atomic(): | ||
postgres_lock_model(mocked_model, lock_mode) | ||
assert lock_signature in get_table_locks() | ||
|
||
assert lock_signature not in get_table_locks() | ||
|
||
|
||
@pytest.mark.django_db(transaction=True) | ||
def test_postgres_lock_model_in_schema(mocked_model): | ||
schema_name = str(uuid.uuid4())[:8] | ||
quoted_schema_name = connection.ops.quote_name(schema_name) | ||
quoted_table_name = connection.ops.quote_name(mocked_model._meta.db_table) | ||
|
||
with connection.cursor() as cursor: | ||
cursor.execute(f"CREATE SCHEMA {quoted_schema_name}") | ||
cursor.execute( | ||
f"CREATE TABLE {quoted_schema_name}.{quoted_table_name} (LIKE public.{quoted_table_name} INCLUDING ALL)" | ||
) | ||
|
||
lock_signature = (schema_name, mocked_model._meta.db_table, "ExclusiveLock") | ||
with transaction.atomic(): | ||
postgres_lock_model( | ||
mocked_model, | ||
PostgresTableLockMode.EXCLUSIVE, | ||
schema_name=schema_name, | ||
) | ||
assert lock_signature in get_table_locks() | ||
|
||
assert lock_signature not in get_table_locks() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using
@pytest.mark.parametrize
withPostgresTableLockMode
to ensure that all lock modes are working?