Skip to content

feat: Add option to run partition migration command outside transaction #239

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ dist/

# Ignore PyCharm / IntelliJ files
.idea/
*.iml
10 changes: 10 additions & 0 deletions docs/source/table_partitioning.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ Time-based partitioning
])


Running management operations in a non atomic way
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Partitions creation and deletion can be done in a non-atomic way.
This can be useful to reduce lock contention when performing partition operations on a table while it is under heavy load.
Note that obviously this can lead to partially created/deleted partitions if something goes wrong during the operations.
By default all operations are done in an atomic way.

You can disable atomic operations by setting the `atomic` parameter to `False` in the `PostgresPartitioningConfig` constructor.

Changing a time partitioning strategy
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
14 changes: 10 additions & 4 deletions psqlextra/backend/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class PostgresSchemaEditor(SchemaEditor):
sql_add_range_partition = (
"CREATE TABLE %s PARTITION OF %s FOR VALUES FROM (%s) TO (%s)"
)
sql_detach_partition = "ALTER TABLE %s DETACH PARTITION %s"
sql_add_list_partition = (
"CREATE TABLE %s PARTITION OF %s FOR VALUES IN (%s)"
)
Expand Down Expand Up @@ -807,11 +808,16 @@ def add_default_partition(

def delete_partition(self, model: Type[Model], name: str) -> None:
"""Deletes the partition with the specified name."""

sql = self.sql_delete_partition % self.quote_name(
self.create_partition_table_name(model, name)
partition_table_name = self.create_partition_table_name(model, name)
detach_sql = self.sql_detach_partition % (
self.quote_name(model._meta.db_table),
self.quote_name(partition_table_name),
)
self.execute(sql)
delete_sql = self.sql_delete_partition % self.quote_name(
partition_table_name
)
self.execute(detach_sql)
self.execute(delete_sql)

def alter_db_table(
self, model: Type[Model], old_db_table: str, new_db_table: str
Expand Down
2 changes: 2 additions & 0 deletions psqlextra/partitioning/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ def __init__(
self,
model: Type[PostgresPartitionedModel],
strategy: PostgresPartitioningStrategy,
atomic: bool = True,
) -> None:
self.model = model
self.strategy = strategy
self.atomic = atomic


__all__ = ["PostgresPartitioningConfig"]
27 changes: 24 additions & 3 deletions psqlextra/partitioning/plan.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import contextlib
import sys

from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, cast
from typing import TYPE_CHECKING, ContextManager, List, Optional, Union, cast

from django.db import connections, transaction

Expand Down Expand Up @@ -36,8 +39,10 @@ def apply(self, using: Optional[str]) -> None:

connection = connections[using or "default"]

with transaction.atomic():
with connection.schema_editor() as schema_editor:
with self._migration_context_manager():
with connection.schema_editor(
atomic=self.config.atomic
) as schema_editor:
for partition in self.creations:
partition.create(
self.config.model,
Expand All @@ -51,6 +56,22 @@ def apply(self, using: Optional[str]) -> None:
cast("PostgresSchemaEditor", schema_editor),
)

def _migration_context_manager(
self,
) -> Union[transaction.Atomic, ContextManager[None]]:
if sys.version_info >= (3, 7):
return (
transaction.atomic()
if self.config.atomic
else contextlib.nullcontext()
)
else:
return (
transaction.atomic()
if self.config.atomic
else contextlib.suppress()
)

def print(self) -> None:
"""Prints this model plan to the terminal in a readable format."""

Expand Down
5 changes: 5 additions & 0 deletions psqlextra/partitioning/shorthands.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def partition_by_current_time(
days: Optional[int] = None,
max_age: Optional[relativedelta] = None,
name_format: Optional[str] = None,
atomic: bool = True,
) -> PostgresPartitioningConfig:
"""Short-hand for generating a partitioning config that partitions the
specified model by time.
Expand Down Expand Up @@ -53,6 +54,9 @@ def partition_by_current_time(
name_format:
The datetime format which is being passed to datetime.strftime
to generate the partition name.

atomic:
If set to True, the partitioning operations will be run inside a transaction.
"""

size = PostgresTimePartitionSize(
Expand All @@ -61,6 +65,7 @@ def partition_by_current_time(

return PostgresPartitioningConfig(
model=model,
atomic=atomic,
strategy=PostgresCurrentTimePartitioningStrategy(
size=size,
count=count,
Expand Down
31 changes: 31 additions & 0 deletions tests/test_partitioning_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,37 @@ def test_partitioning_time_delete(kwargs, timepoints):
assert len(table.partitions) == partition_count


@pytest.mark.postgres_version(lt=110000)
def test_partitioning_time_when_non_atomic():
model = define_fake_partitioned_model(
{"timestamp": models.DateTimeField()}, {"key": ["timestamp"]}
)

schema_editor = connection.schema_editor()
schema_editor.create_partitioned_model(model)

manager = PostgresPartitioningManager(
[
partition_by_current_time(
model=model,
count=6,
days=7,
max_age=relativedelta(weeks=1),
atomic=False,
)
]
)

with freezegun.freeze_time("2019-1-1"):
manager.plan().apply()

with freezegun.freeze_time("2019-1-15"):
manager.plan(skip_create=True).apply()

table = _get_partitioned_table(model)
assert len(table.partitions) == 4


@pytest.mark.postgres_version(lt=110000)
def test_partitioning_time_delete_ignore_manual():
"""Tests whether partitions that were created manually are ignored.
Expand Down