Skip to main content

Postpone index creation to provide Zero Downtime Migration feature

Project description

Tests

Django Postpone Index

This package provides modules and tools to postpone any index creation instead doing it inside the migration, to provide Zero Downtime Migration feature.

The package is now using the PostgresSQL-specific CREATE INDEX CONCURRENTLY SQL command, so is applicable only to the PostgreSQL backend.

Installation

Stable version from the PyPi package repository

pip install django-postpone-index

Last development version from the GitHub source version control system

pip install git+git://github.com/nnseva/django-postpone-index.git

Problem Description

Large data leads to long index creation time.

When the migration is automatically created, it executes all SQL commands creating index inside a transaction.

Large data and index creation inside a transaction lead to long-term table lock which blocks any data writting to the table.

On the other side, CREATE INDEX CONCURRENTLY SQL command may solve the problem, but this SQL command can not be executed inside a transaction block.

The AddIndexConcurrently might be created in a separate migration, moving out the automatically generated AddIndex from the migration, but not all indexes are created using AddIndex.

Solution

All index creation SQL commands (as well as unique constraints creation) are catched and postponed using a special PostponedSQL model (the DROP INDEX and DROP CONSTRAINT SQL commands are still executed immediately).

When the migration is finished, the postponed indexes may be created in a separate process using CREATE INDEX CONCURRENTLY SQL command by the apply_postponed management command. Apart from the standard migration, this process doesn't lock the whole table for a long time.

Failed index creation statements don't lead to the command failure (until a special command line parameter passed). Every failed statement is stored as erroneous instead. When the data is fixed, you can execute the apply_postponed management command again to restore the failed indexes.

Complex Use Cases

The following complex use cases are processed by the package.

  • Several create/drop pairs. There can be several create/drop index pairs if several migrations applied at once.
  • Back Migration. The both, forward and backward migrations are processed.
  • Implicit index drop while removing the table. The Django doesn't issue a separate SQL to drop indexes of the dropped table.
  • Implicit index drop while removing the field. The Django doesn't issue a separate SQL to drop indexes related to the dropped column.
  • Rename field (column) name or model (table) name

Using

Include the postpone_index application in setting.py:

INSTALLED_APPS = [
    ...
    'postpone_index',
    ...
]

Use pospone_index.contrib.postgres or postpone_index.contrib.postgis engines instead of the Django-provided in settings.py:

DATABASES = {
    'default': {
        'ENGINE': 'postpone_index.contrib.postgres',
        ...
    }
}

If you provide your own database engine instead of the Django-provided, you can also combine pospone_index.contrib.postgres.schema.DatabaseSchemaEditorMixin with your own Database Schema Editor, f.e.:

mybackend/schema.py

from django.db.backends.postgresql.schema import DatabaseSchemaEditor as _DatabaseSchemaEditor
from pospone_index.contrib.postgres.schema import DatabaseSchemaEditorMixin

class PostponeIndexDatabaseSchemaEditor(DatabaseSchemaEditorMixin, _DatabaseSchemaEditor):
    # Your own code
    ...

mybackend/base.py

from django.db.backends.postgresql.base import (
    DatabaseWrapper as _DatabaseWrapper,
)

from mybackend.schema import PostponeIndexDatabaseSchemaEditor


class DatabaseWrapper(_DatabaseWrapper):
    """Database wrapper"""

    SchemaEditorClass = PostponeIndexDatabaseSchemaEditor
    # Your own code
    ...

Execute apply_postponed management command every time after the migrate management command to create new postponed indexes.

Monitor PostponedSQL model instances to see errors on the SQL execution.

After the data is fixed, you can try to recreate the postponed invalid indexes just calling the apply_postponed migration command again. All not-applied indexes will be tried to create again.

NOTICE the apply_postponed management command doesn't have any explicit locking mechanics. Avoid starting this command concurrently with itself or another migrate command on the same database.

Intermediate migration state

Apart from standard Django migrations, using the postpone_index package leads to the intermediate migration state after the migrate management command finished:

  • new model structure is applied
  • indexes to be deleted are deleted
  • indexes to be created are not created yet

You should be aware that if you introduce a new unique index or constraint, the database does not control uniqueness based on not yet created indexes at this time.

Your code works now as expected everywhere, except the code which is based on new unique constraints introduced in applied migrations.

Apply the apply_postponed run management command to make these new indexes work.

Any error while apply_postponed run execution is stored in the PostponedSQL model instance.

You can see erroneous lines using apply_postponed list command. See the [E] mark at the start of the line.

You also can see the error details using the format parameter of the apply_postponed list -f '... %(error)s' management command.

The apply_postponed run -x breaks execution on any error. You can see the error in the standard error or logging streams.

The apply_postponed run (without -x parameter) doesn't stop on error, but outputs warning to the log stream instead.

When the error happened, it most probably is caused by the non-unique records. Fix the data and try to execute apply_postponed run again to create an index.

After the successfull apply_postponed run execution, the migration state is finalised to be equal as if you applied the migration without postpone_index package at all.

The apply_postponed run management command marks all successfully executed PostponedSQL instances as done. You can see [X] mark at the start of the line produced by apply_ponsponed list management command.

You can cleanup done instances using apply_postponed cleanup management command. This step is optional.

Django testing

Django migrates testing database before tests. Always use POSTPONE_INDEX_IGNORE = True settings to avoid postpone index for the testing database.

If you want to check your own migration with the postpone index switched on, use the postpone_index.testing_utils.TestCase and override_settings Django feature with the following trick:

from django.core.management import call_command
from django.test import override_settings
from postpone_index.models import PostponedSQL
from postpone_index import testing_utils

class ModuleTest(testing_utils.TestCase):
    # Notice that the base TestCase is TransactionalTestCase

    @classmethod
    def setUpClass(cls):
        # If you want to have customized setUpClass, call the method of the base class
        super().setUpClass()

    @classmethod
    def tearDownClass(cls):
        # If you want to have customized tearDownClass, call the method of the base class
        super().tearDownClass()

    def test_my_special_migration_case(self):
        """Explicitly check my migration with postpone_index"""

        module_to_check = "my_module"           # Your Django App
        migration_before_the_check = "0005"     # Just before your migration
        migration_to_check = "0006"             # The migration you check

        # Notice that POSTPONED_INDEX_IGNORE is True by default while testing
        call_command('migrate', module_to_check, migration_before_the_check)

        with override_settings(
            POSTPONE_INDEX_IGNORE=False
        ):
            # Here we can check how it's going with `postpone_index` activated

            # Check whether your migration works as expected with postponed indexes
            call_command('migrate', module_to_check, migration_to_check)

            # Here you can check how the module works before apply_postponed
            ...

            # Check whether the indexes applied properly. The `-x` parameter
            # causes exception on errors
            call_command('apply_postponed', 'run', '-x')

Django settings

POSTPONE_INDEX_IGNORE

The setting totally switches off the functionality of the package.

Always use this setting in the test environment to avoid using postponed index creation for the test database.

May be used in a heterogeneous database environment to switch off the package functionality on unsupported databases.

POSTPONE_INDEX_ADMIN_IGNORE

The PostponedSQL model admin view is switched on by default. You can totally switch it off, or create your own admin class instead. Use postpone_index.admin.PostponedSQLAdminMixin as a base class if necessary.

Django database

The Django supports heterogeneous database environment in a single project. Every single database has it's own state of migrations executed by the manage.py migrate --database <alias>.

The apply_postponed command also supports selection of the database alias using similar syntax:

# The 'default' database alias is used as a default
python manage.py migrate
python manage.py apply_postponed

# A non-default database alias parameter has similar syntax
python manage.py migrate --database another-postgres-database
python manage.py apply_postponed --database another-postgres-database

Use POSTPONE_INDEX_IGNORE=1 environment to switch off the package functionality on migrations running on unsupported database engines like:

POSTPONE_INDEX_IGNORE=1 python manage.py migrate --database non-postgres-database

Special migrations to avoid postpone index

Sometimes you may need to avoid the postpone_index applied to a single migration.

Just include the PostponeIndexIgnoreMigrationMixin into a base class list for your special migration:

from django.db import migrations, models
from postpone_index.migration_utils import PostponeIndexIgnoreMigrationMixin

class Migration(PostponeIndexIgnoreMigrationMixin, migrations.Migration):
    ...

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_postpone_index-0.0.5.tar.gz (33.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_postpone_index-0.0.5-py3-none-any.whl (17.5 kB view details)

Uploaded Python 3

File details

Details for the file django_postpone_index-0.0.5.tar.gz.

File metadata

  • Download URL: django_postpone_index-0.0.5.tar.gz
  • Upload date:
  • Size: 33.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_postpone_index-0.0.5.tar.gz
Algorithm Hash digest
SHA256 598a5a6c636e538fea4cd2d77699f077954daee4371843eff3ebb9da849a39b9
MD5 a9e72162e2ef5903a4ada893af0802d8
BLAKE2b-256 bf16d8ec2f6f91569834a09b2eaf2734fb5a4e121b3f683d8fcb06f3b588184e

See more details on using hashes here.

File details

Details for the file django_postpone_index-0.0.5-py3-none-any.whl.

File metadata

File hashes

Hashes for django_postpone_index-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 fbd8b664fd73a80bc7a627c33f96fee67f729c4ac6f4ae9de05175c0ad4382e7
MD5 84208581c77cd8f3bc18ef5ed2d1d729
BLAKE2b-256 7687cd47949308f750a80df699c78839f65f59c084cf4ca7fb3cb2f74fde6813

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page