Ticket #19707: ticket_19707-2.diff

File ticket_19707-2.diff, 10.9 KB (added by akaariai, 2 years ago)
  • django/db/__init__.py

    diff --git a/django/db/__init__.py b/django/db/__init__.py
    index b198048..dc9b34c 100644
    a b backend = load_backend(connection.settings_dict['ENGINE']) 
    4242# Register an event that closes the database connection
    4343# when a Django request is finished.
    4444def close_connection(**kwargs):
    45     for conn in connections.all():
    46         conn.close()
     45    # Avoid circular imports
     46    from django.db import transaction
     47    for conn in connections:
     48        try:
     49            transaction.abort(conn)
     50            connections[conn].close()
     51        except:
     52            # The connection's state is unknown, so it has to be
     53            # abandoned. This could happen for example if the network
     54            # connection has a failure.
     55            del connections[conn]
    4756signals.request_finished.connect(close_connection)
    4857
    4958# Register an event that resets connection.queries
  • django/db/backends/__init__.py

    diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
    index 7dc5456..bbb5a5b 100644
    a b class BaseDatabaseWrapper(object): 
    8888            return
    8989        self.cursor().execute(self.ops.savepoint_commit_sql(sid))
    9090
     91    def abort(self):
     92        """
     93        Roll back any ongoing transaction and clean the transaction state
     94        stack.
     95        """
     96        if self._dirty:
     97            self._rollback()
     98            self._dirty = False
     99        while self.transaction_state:
     100            self.leave_transaction_management()
     101
    91102    def enter_transaction_management(self, managed=True):
    92103        """
    93104        Enters transaction management for a running thread. It must be balanced with
  • django/db/transaction.py

    diff --git a/django/db/transaction.py b/django/db/transaction.py
    index f3ce2b2..dd7e2f4 100644
    a b class TransactionManagementError(Exception): 
    2424    """
    2525    pass
    2626
     27def abort(using=None):
     28    """
     29    Roll back any ongoing transactions and clean the transaction management
     30    state of the connection.
     31
     32    This method is to be used only in cases where using balanced
     33    leave_transaction_management() calls isn't possible. For example after a
     34    request has finished, the transaction state isn't known, yet the connection
     35    must be cleaned up for the next request.
     36    """
     37    if using is None:
     38        using = DEFAULT_DB_ALIAS
     39    connection = connections[using]
     40    connection.abort()
     41
    2742def enter_transaction_management(managed=True, using=None):
    2843    """
    2944    Enters transaction management for a running thread. It must be balanced with
  • django/db/utils.py

    diff --git a/django/db/utils.py b/django/db/utils.py
    index 842fd35..fb9bdc1 100644
    a b class ConnectionHandler(object): 
    9898    def __setitem__(self, key, value):
    9999        setattr(self._connections, key, value)
    100100
     101    def __delitem__(self, key):
     102        delattr(self._connections, key)
     103
    101104    def __iter__(self):
    102105        return iter(self.databases)
    103106
  • django/middleware/transaction.py

    diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py
    index 96b1538..7888839 100644
    a b class TransactionMiddleware(object): 
    1515    def process_exception(self, request, exception):
    1616        """Rolls back the database and leaves transaction management"""
    1717        if transaction.is_dirty():
     18            # This rollback might fail for closed connections for example.
    1819            transaction.rollback()
    1920        transaction.leave_transaction_management()
    2021
    class TransactionMiddleware(object): 
    2223        """Commits and leaves transaction management."""
    2324        if transaction.is_managed():
    2425            if transaction.is_dirty():
    25                 transaction.commit()
     26                # Note: it is possible that the commit fails. If the reason is
     27                # closed connection or some similar reason, then there is
     28                # little hope to proceed nicely. However, in case of deferred
     29                # foreign key check failures we can still rollback(). In any
     30                # case we want to have an informative exception raised.
     31                try:
     32                    transaction.commit()
     33                except:
     34                    # If the rollback fails, the transaction state will be
     35                    # messed up. It doesn't matter, the connection will be set
     36                    # to clean state after the request finishes. And, we can't
     37                    # clean the state here properly even if we wanted to, the
     38                    # connection is in transaction but we can't rollback...
     39                    transaction.rollback()
     40                    transaction.leave_transaction_management()
     41                    raise
    2642            transaction.leave_transaction_management()
    2743        return response
  • django/test/testcases.py

    diff --git a/django/test/testcases.py b/django/test/testcases.py
    index 3aa0afa..f7c34a9 100644
    a b real_rollback = transaction.rollback 
    7070real_enter_transaction_management = transaction.enter_transaction_management
    7171real_leave_transaction_management = transaction.leave_transaction_management
    7272real_managed = transaction.managed
     73real_abort = transaction.abort
    7374
    7475def nop(*args, **kwargs):
    7576    return
    def disable_transaction_methods(): 
    8081    transaction.enter_transaction_management = nop
    8182    transaction.leave_transaction_management = nop
    8283    transaction.managed = nop
     84    transaction.abort = nop
    8385
    8486def restore_transaction_methods():
    8587    transaction.commit = real_commit
    def restore_transaction_methods(): 
    8789    transaction.enter_transaction_management = real_enter_transaction_management
    8890    transaction.leave_transaction_management = real_leave_transaction_management
    8991    transaction.managed = real_managed
     92    transaction.abort = real_abort
    9093
    9194
    9295def assert_and_parse_html(self, html, user_msg, msg):
  • tests/regressiontests/middleware/tests.py

    diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py
    index a9a45c9..6c43641 100644
    a b import warnings 
    99
    1010from django.conf import settings
    1111from django.core import mail
    12 from django.db import transaction
    13 from django.http import HttpRequest
    14 from django.http import HttpResponse, StreamingHttpResponse
     12from django.db import (transaction, connections, DEFAULT_DB_ALIAS,
     13                       IntegrityError)
     14from django.http import HttpRequest, HttpResponse, StreamingHttpResponse
    1515from django.middleware.clickjacking import XFrameOptionsMiddleware
    1616from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware
    1717from django.middleware.http import ConditionalGetMiddleware
    class TransactionMiddlewareTest(TransactionTestCase): 
    710710        TransactionMiddleware().process_exception(self.request, None)
    711711        self.assertEqual(Band.objects.count(), 0)
    712712        self.assertFalse(transaction.is_dirty())
     713
     714    def test_failing_commit(self):
     715        # It is possible that connection.commit() fails. Check that
     716        # TransactionMiddleware handles such cases correctly.
     717        try:
     718            def raise_exception():
     719                raise IntegrityError()
     720            connections[DEFAULT_DB_ALIAS].commit = raise_exception
     721            transaction.enter_transaction_management()
     722            transaction.managed(True)
     723            Band.objects.create(name='The Beatles')
     724            self.assertTrue(transaction.is_dirty())
     725            with self.assertRaises(IntegrityError):
     726                TransactionMiddleware().process_response(self.request, None)
     727            self.assertEqual(Band.objects.count(), 0)
     728            self.assertFalse(transaction.is_dirty())
     729            self.assertFalse(transaction.is_managed())
     730        finally:
     731            del connections[DEFAULT_DB_ALIAS].commit
  • tests/regressiontests/requests/tests.py

    diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py
    index 799cd9b..bcd6ade 100644
    a b import warnings 
    66from datetime import datetime, timedelta
    77from io import BytesIO
    88
     9from django.db import connection, connections, DEFAULT_DB_ALIAS
     10from django.core import signals
    911from django.core.exceptions import SuspiciousOperation
    1012from django.core.handlers.wsgi import WSGIRequest, LimitedStream
    1113from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError
     14from django.test import TransactionTestCase
    1215from django.test.client import FakePayload
    1316from django.test.utils import override_settings, str_prefix
    1417from django.utils import six
    class RequestsTests(unittest.TestCase): 
    524527
    525528        with self.assertRaises(UnreadablePostError):
    526529            request.body
     530
     531class TransactionRequestTests(TransactionTestCase):
     532    def test_request_finished_db_state(self):
     533        # The GET below will not succeed, but it will give a response with
     534        # defined ._handler_class. That is needed for sending the request_finished
     535        # signal. However, the test client itself will not send request_finished
     536        # signals.
     537        response = self.client.get('/')
     538        # Taking a cursor will open a new connection.
     539        connection.cursor()
     540        connection.enter_transaction_management()
     541        connection.managed(True)
     542        signals.request_finished.send(sender=response._handler_class)
     543        # in-memory sqlite doesn't actually close connections when connections are closed.
     544        if connection.vendor != 'sqlite':
     545            self.assertIs(connection.connection, None)
     546        # And the transaction state is reset.
     547        self.assertEqual(len(connection.transaction_state), 0)
     548   
     549    @unittest.skipIf(connection.vendor == 'sqlite',
     550            'This test will close the connection which must not be done with '
     551            'in-memory sqlite')
     552    def test_request_finished_failed_connection(self):
     553        # See comments in test_request_finished_db_state() for the self.client
     554        # usage.
     555        response = self.client.get('/')
     556
     557        conn = connections[DEFAULT_DB_ALIAS]
     558        conn.enter_transaction_management()
     559        conn.managed(True)
     560        conn.set_dirty()
     561
     562        # Test that the rollback doesn't succeed (for example network failure
     563        # could cause this).
     564        def fail_horribly():
     565            raise Exception("Horrible failure!")
     566        conn._rollback = fail_horribly
     567
     568        signals.request_finished.send(sender=response._handler_class)
     569        # As even rollback wasn't possible the connection wrapper itself was
     570        # abandoned.
     571        self.assertIsNot(conn, connections[DEFAULT_DB_ALIAS])
     572        # And for the connection (that is, django.db.connection) that it has
     573        # its transaction state reset.
     574        self.assertEqual(len(connection.transaction_state), 0)
Back to Top