Fixed #19707 -- Reset transaction state after requests

This commit is contained in:
Anssi Kääriäinen 2013-02-05 23:52:29 +02:00
parent 0e18fb04ba
commit a4e97cf315
8 changed files with 127 additions and 6 deletions

View File

@ -42,8 +42,17 @@ backend = load_backend(connection.settings_dict['ENGINE'])
# Register an event that closes the database connection # Register an event that closes the database connection
# when a Django request is finished. # when a Django request is finished.
def close_connection(**kwargs): def close_connection(**kwargs):
for conn in connections.all(): # Avoid circular imports
conn.close() from django.db import transaction
for conn in connections:
try:
transaction.abort(conn)
connections[conn].close()
except Exception:
# The connection's state is unknown, so it has to be
# abandoned. This could happen for example if the network
# connection has a failure.
del connections[conn]
signals.request_finished.connect(close_connection) signals.request_finished.connect(close_connection)
# Register an event that resets connection.queries # Register an event that resets connection.queries

View File

@ -88,6 +88,17 @@ class BaseDatabaseWrapper(object):
return return
self.cursor().execute(self.ops.savepoint_commit_sql(sid)) self.cursor().execute(self.ops.savepoint_commit_sql(sid))
def abort(self):
"""
Roll back any ongoing transaction and clean the transaction state
stack.
"""
if self._dirty:
self._rollback()
self._dirty = False
while self.transaction_state:
self.leave_transaction_management()
def enter_transaction_management(self, managed=True): def enter_transaction_management(self, managed=True):
""" """
Enters transaction management for a running thread. It must be balanced with Enters transaction management for a running thread. It must be balanced with

View File

@ -24,6 +24,21 @@ class TransactionManagementError(Exception):
""" """
pass pass
def abort(using=None):
"""
Roll back any ongoing transactions and clean the transaction management
state of the connection.
This method is to be used only in cases where using balanced
leave_transaction_management() calls isn't possible. For example after a
request has finished, the transaction state isn't known, yet the connection
must be cleaned up for the next request.
"""
if using is None:
using = DEFAULT_DB_ALIAS
connection = connections[using]
connection.abort()
def enter_transaction_management(managed=True, using=None): def enter_transaction_management(managed=True, using=None):
""" """
Enters transaction management for a running thread. It must be balanced with Enters transaction management for a running thread. It must be balanced with

View File

@ -99,6 +99,9 @@ class ConnectionHandler(object):
def __setitem__(self, key, value): def __setitem__(self, key, value):
setattr(self._connections, key, value) setattr(self._connections, key, value)
def __delitem__(self, key):
delattr(self._connections, key)
def __iter__(self): def __iter__(self):
return iter(self.databases) return iter(self.databases)

View File

@ -15,6 +15,10 @@ class TransactionMiddleware(object):
def process_exception(self, request, exception): def process_exception(self, request, exception):
"""Rolls back the database and leaves transaction management""" """Rolls back the database and leaves transaction management"""
if transaction.is_dirty(): if transaction.is_dirty():
# This rollback might fail because of network failure for example.
# If rollback isn't possible it is impossible to clean the
# connection's state. So leave the connection in dirty state and
# let request_finished signal deal with cleaning the connection.
transaction.rollback() transaction.rollback()
transaction.leave_transaction_management() transaction.leave_transaction_management()
@ -22,6 +26,21 @@ class TransactionMiddleware(object):
"""Commits and leaves transaction management.""" """Commits and leaves transaction management."""
if transaction.is_managed(): if transaction.is_managed():
if transaction.is_dirty(): if transaction.is_dirty():
transaction.commit() # Note: it is possible that the commit fails. If the reason is
# closed connection or some similar reason, then there is
# little hope to proceed nicely. However, in some cases (
# deferred foreign key checks for exampl) it is still possible
# to rollback().
try:
transaction.commit()
except Exception:
# If the rollback fails, the transaction state will be
# messed up. It doesn't matter, the connection will be set
# to clean state after the request finishes. And, we can't
# clean the state here properly even if we wanted to, the
# connection is in transaction but we can't rollback...
transaction.rollback()
transaction.leave_transaction_management()
raise
transaction.leave_transaction_management() transaction.leave_transaction_management()
return response return response

View File

@ -70,6 +70,7 @@ real_rollback = transaction.rollback
real_enter_transaction_management = transaction.enter_transaction_management real_enter_transaction_management = transaction.enter_transaction_management
real_leave_transaction_management = transaction.leave_transaction_management real_leave_transaction_management = transaction.leave_transaction_management
real_managed = transaction.managed real_managed = transaction.managed
real_abort = transaction.abort
def nop(*args, **kwargs): def nop(*args, **kwargs):
return return
@ -80,6 +81,7 @@ def disable_transaction_methods():
transaction.enter_transaction_management = nop transaction.enter_transaction_management = nop
transaction.leave_transaction_management = nop transaction.leave_transaction_management = nop
transaction.managed = nop transaction.managed = nop
transaction.abort = nop
def restore_transaction_methods(): def restore_transaction_methods():
transaction.commit = real_commit transaction.commit = real_commit
@ -87,6 +89,7 @@ def restore_transaction_methods():
transaction.enter_transaction_management = real_enter_transaction_management transaction.enter_transaction_management = real_enter_transaction_management
transaction.leave_transaction_management = real_leave_transaction_management transaction.leave_transaction_management = real_leave_transaction_management
transaction.managed = real_managed transaction.managed = real_managed
transaction.abort = real_abort
def assert_and_parse_html(self, html, user_msg, msg): def assert_and_parse_html(self, html, user_msg, msg):

View File

@ -9,9 +9,9 @@ import warnings
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
from django.db import transaction from django.db import (transaction, connections, DEFAULT_DB_ALIAS,
from django.http import HttpRequest IntegrityError)
from django.http import HttpResponse, StreamingHttpResponse from django.http import HttpRequest, HttpResponse, StreamingHttpResponse
from django.middleware.clickjacking import XFrameOptionsMiddleware from django.middleware.clickjacking import XFrameOptionsMiddleware
from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware
from django.middleware.http import ConditionalGetMiddleware from django.middleware.http import ConditionalGetMiddleware
@ -710,3 +710,22 @@ class TransactionMiddlewareTest(TransactionTestCase):
TransactionMiddleware().process_exception(self.request, None) TransactionMiddleware().process_exception(self.request, None)
self.assertEqual(Band.objects.count(), 0) self.assertEqual(Band.objects.count(), 0)
self.assertFalse(transaction.is_dirty()) self.assertFalse(transaction.is_dirty())
def test_failing_commit(self):
# It is possible that connection.commit() fails. Check that
# TransactionMiddleware handles such cases correctly.
try:
def raise_exception():
raise IntegrityError()
connections[DEFAULT_DB_ALIAS].commit = raise_exception
transaction.enter_transaction_management()
transaction.managed(True)
Band.objects.create(name='The Beatles')
self.assertTrue(transaction.is_dirty())
with self.assertRaises(IntegrityError):
TransactionMiddleware().process_response(self.request, None)
self.assertEqual(Band.objects.count(), 0)
self.assertFalse(transaction.is_dirty())
self.assertFalse(transaction.is_managed())
finally:
del connections[DEFAULT_DB_ALIAS].commit

View File

@ -6,9 +6,12 @@ import warnings
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from django.db import connection, connections, DEFAULT_DB_ALIAS
from django.core import signals
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.handlers.wsgi import WSGIRequest, LimitedStream from django.core.handlers.wsgi import WSGIRequest, LimitedStream
from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError
from django.test import TransactionTestCase
from django.test.client import FakePayload from django.test.client import FakePayload
from django.test.utils import override_settings, str_prefix from django.test.utils import override_settings, str_prefix
from django.utils import six from django.utils import six
@ -524,3 +527,42 @@ class RequestsTests(unittest.TestCase):
with self.assertRaises(UnreadablePostError): with self.assertRaises(UnreadablePostError):
request.body request.body
class TransactionRequestTests(TransactionTestCase):
def test_request_finished_db_state(self):
# The GET below will not succeed, but it will give a response with
# defined ._handler_class. That is needed for sending the
# request_finished signal.
response = self.client.get('/')
# Make sure there is an open connection
connection.cursor()
connection.enter_transaction_management()
connection.managed(True)
signals.request_finished.send(sender=response._handler_class)
# In-memory sqlite doesn't actually close connections.
if connection.vendor != 'sqlite':
self.assertIs(connection.connection, None)
self.assertEqual(len(connection.transaction_state), 0)
@unittest.skipIf(connection.vendor == 'sqlite',
'This test will close the connection, in-memory '
'sqlite connections must not be closed.')
def test_request_finished_failed_connection(self):
# See comments in test_request_finished_db_state() for the self.client
# usage.
response = self.client.get('/')
conn = connections[DEFAULT_DB_ALIAS]
conn.enter_transaction_management()
conn.managed(True)
conn.set_dirty()
# Test that the rollback doesn't succeed (for example network failure
# could cause this).
def fail_horribly():
raise Exception("Horrible failure!")
conn._rollback = fail_horribly
signals.request_finished.send(sender=response._handler_class)
# As even rollback wasn't possible the connection wrapper itself was
# abandoned. Accessing the connections[alias] will create a new
# connection wrapper, whch must be different than the original one.
self.assertIsNot(conn, connections[DEFAULT_DB_ALIAS])
self.assertEqual(len(connection.transaction_state), 0)