Opened 3 weeks ago

Last modified 8 days ago

#37406 assigned Bug

BaseDatabaseSchemaEditor.__exit__ doesn't exit its atomic block when deferred SQL raises

Reported by: Gavin Wahl(vendor) Owned by: Vishy
Component: Database layer (models, ORM) Version: 6.1
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Gavin Wahl(vendor))

BaseDatabaseSchemaEditor (normally accessed through connection.schema_editor) implements the context manager protocol and wraps transaction.atomic() in its __enter__ and __exit__. In __exit__, it runs the deferred SQL and then calls self.atomic.__exit__. If the deferred SQL raises, the second call never happens and the atomic block stays open.

After that the connection is stuck. in_atomic_block is True with no atomic block on the stack, so nothing will ever commit. Once anything sets needs_rollback you get TransactionManagementError on every query. close() doesn't help because inside an atomic block it keeps the dead connection object around and waits for an Atomic.__exit__ to reset it.

This doesn't affect manage.py migrate from a shell, since the process exits and the server rolls back. It matters if you catch the error and keep going, e.g. tests, call_command("migrate"), or a tool using schema_editor directly.

There is an existing test_migrations_not_applied_on_deferred_sql_failure but it has atomic = False so it doesn't hit this.

Minimal reproducer attached.

Related #29721

Attachments (1)

deferred_fail_atomic.py​ (1.1 KB ) - added by Gavin Wahl(vendor) 3 weeks ago.

Download all attachments as: .zip

Change History (5)

by Gavin Wahl(vendor), 3 weeks ago

Attachment: deferred_fail_atomic.py​ added

comment:1 by Gavin Wahl(vendor), 3 weeks ago

Description: modified (diff)

comment:2 by Sarah Boyce, 3 weeks ago

Triage Stage: Unreviewed → Accepted
Type: Uncategorized → Bug

Thank you, reproduced:

  • tests/schema/tests.py

    a b class SchemaTests(TransactionTestCase):  
    60786078            if connection.vendor == "postgresql":
    60796079                with connection.cursor() as cursor:
    60806080                    cursor.execute("DROP COLLATION IF EXISTS case_insensitive")
     6081
     6082
     6083class SchemaEditorTransactionTests(TransactionTestCase):
     6084    available_apps = []
     6085
     6086    @skipUnlessDBFeature("can_rollback_ddl")
     6087    def test_deferred_sql_failure_exits_atomic_block(self):
     6088        editor = connection.schema_editor()
     6089        try:
     6090            with self.assertRaises(DatabaseError):
     6091                with editor:
     6092                    editor.deferred_sql.append(
     6093                        "SELECT * FROM django_nonexistent_table"
     6094                    )
     6095
     6096            self.assertFalse(connection.in_atomic_block)
     6097        finally:
     6098            # Keep the test database usable while the bug is present.
     6099            if editor.atomic in connection.atomic_blocks:
     6100                editor.atomic.__exit__(DatabaseError, DatabaseError(), None)

comment:3 by Vishy, 3 weeks ago

Owner: set to Vishy
Status: new → assigned

comment:4 by Vishy, 8 days ago

Has patch: set
Note: See TracTickets for help on using tickets.
Back to Top