[1.6.x] Fixed #21239 -- Maintained atomicity when closing the connection.

Refs #15802 -- Reverted #7c657b24 as BaseDatabaseWrapper.close() now
has a proper "finally" clause that may need to preserve self.connection.

Backport of 25860096 from master.
This commit is contained in:
Aymeric Augustin 2014-03-23 21:45:31 +01:00
parent 746cded010
commit 4ea02bdb0d
4 changed files with 37 additions and 29 deletions

View File

@ -63,6 +63,7 @@ class BaseDatabaseWrapper(object):
# Connection termination related attributes
self.close_at = None
self.closed_in_transaction = False
self.errors_occurred = False
# Thread-safety related attributes
@ -103,9 +104,11 @@ class BaseDatabaseWrapper(object):
# In case the previous connection was closed while in an atomic block
self.in_atomic_block = False
self.savepoint_ids = []
self.needs_rollback = False
# Reset parameters defining when to close the connection
max_age = self.settings_dict['CONN_MAX_AGE']
self.close_at = None if max_age is None else time.time() + max_age
self.closed_in_transaction = False
self.errors_occurred = False
# Establish the connection
conn_params = self.get_connection_params()
@ -188,7 +191,11 @@ class BaseDatabaseWrapper(object):
try:
self._close()
finally:
self.connection = None
if self.in_atomic_block:
self.closed_in_transaction = True
self.needs_rollback = True
else:
self.connection = None
self.set_clean()
##### Backend-specific savepoint management methods #####

View File

@ -3,7 +3,7 @@ PostgreSQL database backend for Django.
Requires psycopg 2: http://initd.org/projects/psycopg2
"""
import logging
import sys
from django.db.backends import *
@ -31,8 +31,6 @@ psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString)
psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
logger = logging.getLogger('django.db.backends')
def utc_tzinfo_factory(offset):
if offset != 0:
raise AssertionError("database connection isn't set to UTC")
@ -140,27 +138,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
return cursor
def close(self):
self.validate_thread_sharing()
if self.connection is None:
return
try:
self.connection.close()
self.connection = None
except Database.Error:
# In some cases (database restart, network connection lost etc...)
# the connection to the database is lost without giving Django a
# notification. If we don't set self.connection to None, the error
# will occur a every request.
self.connection = None
logger.warning('psycopg2 error while closing the connection.',
exc_info=sys.exc_info()
)
raise
finally:
self.set_clean()
def _set_isolation_level(self, isolation_level):
assert isolation_level in range(1, 5) # Use set_autocommit for level = 0
if self.psycopg2_version >= (2, 4, 2):

View File

@ -290,7 +290,12 @@ class Atomic(object):
connection.in_atomic_block = False
try:
if exc_type is None and not connection.needs_rollback:
if connection.closed_in_transaction:
# The database will perform a rollback by itself.
# Wait until we exit the outermost block.
pass
elif exc_type is None and not connection.needs_rollback:
if connection.in_atomic_block:
# Release savepoint if there is one
if sid is not None:
@ -330,13 +335,18 @@ class Atomic(object):
finally:
# Outermost block exit when autocommit was enabled.
if not connection.in_atomic_block:
if connection.features.autocommits_when_autocommit_is_off:
if connection.closed_in_transaction:
connection.connection = None
elif connection.features.autocommits_when_autocommit_is_off:
connection.autocommit = True
else:
connection.set_autocommit(True)
# Outermost block exit when autocommit was disabled.
elif not connection.savepoint_ids and not connection.commit_on_exit:
connection.in_atomic_block = False
if connection.closed_in_transaction:
connection.connection = None
else:
connection.in_atomic_block = False
def __call__(self, func):
@wraps(func, assigned=available_attrs(func))

View File

@ -8,7 +8,7 @@ except ImportError:
import time
from django.db import (connection, transaction,
DatabaseError, IntegrityError, OperationalError)
DatabaseError, Error, IntegrityError, OperationalError)
from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test.utils import IgnorePendingDeprecationWarningsMixin
from django.utils import six
@ -359,6 +359,20 @@ class AtomicErrorsTests(TransactionTestCase):
r2.save(force_update=True)
self.assertEqual(Reporter.objects.get(pk=r1.pk).last_name, "Calculus")
@skipUnlessDBFeature('test_db_allows_multiple_connections')
def test_atomic_prevents_queries_in_broken_transaction_after_client_close(self):
with transaction.atomic():
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
connection.close()
# The connection is closed and the transaction is marked as
# needing rollback. This will raise an InterfaceError on databases
# that refuse to create cursors on closed connections (PostgreSQL)
# and a TransactionManagementError on other databases.
with self.assertRaises(Error):
Reporter.objects.create(first_name="Cuthbert", last_name="Calculus")
# The connection is usable again .
self.assertEqual(Reporter.objects.count(), 0)
@skipUnless(connection.vendor == 'mysql', "MySQL-specific behaviors")
class AtomicMySQLTests(TransactionTestCase):