Skip to content

Commit 2e42c85

Browse files
committed
[1.7.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 2586009 from master.
1 parent 5f22bda commit 2e42c85

File tree

4 files changed

+37
-30
lines changed

4 files changed

+37
-30
lines changed

django/db/backends/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS,
6262

6363
# Connection termination related attributes
6464
self.close_at = None
65+
self.closed_in_transaction = False
6566
self.errors_occurred = False
6667

6768
# Thread-safety related attributes
@@ -104,9 +105,11 @@ def connect(self):
104105
# In case the previous connection was closed while in an atomic block
105106
self.in_atomic_block = False
106107
self.savepoint_ids = []
108+
self.needs_rollback = False
107109
# Reset parameters defining when to close the connection
108110
max_age = self.settings_dict['CONN_MAX_AGE']
109111
self.close_at = None if max_age is None else time.time() + max_age
112+
self.closed_in_transaction = False
110113
self.errors_occurred = False
111114
# Establish the connection
112115
conn_params = self.get_connection_params()
@@ -188,7 +191,11 @@ def close(self):
188191
try:
189192
self._close()
190193
finally:
191-
self.connection = None
194+
if self.in_atomic_block:
195+
self.closed_in_transaction = True
196+
self.needs_rollback = True
197+
else:
198+
self.connection = None
192199
self.set_clean()
193200

194201
##### Backend-specific savepoint management methods #####

django/db/backends/postgresql_psycopg2/base.py

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Requires psycopg 2: http://initd.org/projects/psycopg2
55
"""
6-
import logging
6+
77
import sys
88

99
from django.conf import settings
@@ -36,8 +36,6 @@
3636
psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString)
3737
psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
3838

39-
logger = logging.getLogger('django.db.backends')
40-
4139

4240
def utc_tzinfo_factory(offset):
4341
if offset != 0:
@@ -162,28 +160,6 @@ def create_cursor(self):
162160
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
163161
return cursor
164162

165-
def close(self):
166-
self.validate_thread_sharing()
167-
if self.connection is None:
168-
return
169-
170-
try:
171-
self.connection.close()
172-
self.connection = None
173-
except Database.Error:
174-
# In some cases (database restart, network connection lost etc...)
175-
# the connection to the database is lost without giving Django a
176-
# notification. If we don't set self.connection to None, the error
177-
# will occur a every request.
178-
self.connection = None
179-
logger.warning(
180-
'psycopg2 error while closing the connection.',
181-
exc_info=sys.exc_info()
182-
)
183-
raise
184-
finally:
185-
self.set_clean()
186-
187163
def _set_isolation_level(self, isolation_level):
188164
assert isolation_level in range(1, 5) # Use set_autocommit for level = 0
189165
if self.psycopg2_version >= (2, 4, 2):

django/db/transaction.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,12 @@ def __exit__(self, exc_type, exc_value, traceback):
313313
connection.in_atomic_block = False
314314

315315
try:
316-
if exc_type is None and not connection.needs_rollback:
316+
if connection.closed_in_transaction:
317+
# The database will perform a rollback by itself.
318+
# Wait until we exit the outermost block.
319+
pass
320+
321+
elif exc_type is None and not connection.needs_rollback:
317322
if connection.in_atomic_block:
318323
# Release savepoint if there is one
319324
if sid is not None:
@@ -353,13 +358,18 @@ def __exit__(self, exc_type, exc_value, traceback):
353358
finally:
354359
# Outermost block exit when autocommit was enabled.
355360
if not connection.in_atomic_block:
356-
if connection.features.autocommits_when_autocommit_is_off:
361+
if connection.closed_in_transaction:
362+
connection.connection = None
363+
elif connection.features.autocommits_when_autocommit_is_off:
357364
connection.autocommit = True
358365
else:
359366
connection.set_autocommit(True)
360367
# Outermost block exit when autocommit was disabled.
361368
elif not connection.savepoint_ids and not connection.commit_on_exit:
362-
connection.in_atomic_block = False
369+
if connection.closed_in_transaction:
370+
connection.connection = None
371+
else:
372+
connection.in_atomic_block = False
363373

364374
def __call__(self, func):
365375
@wraps(func, assigned=available_attrs(func))

tests/transactions/tests.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from unittest import skipIf, skipUnless
1010

1111
from django.db import (connection, transaction,
12-
DatabaseError, IntegrityError, OperationalError)
12+
DatabaseError, Error, IntegrityError, OperationalError)
1313
from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
1414
from django.test.utils import IgnoreDeprecationWarningsMixin
1515
from django.utils import six
@@ -360,6 +360,20 @@ def test_atomic_allows_queries_after_fixing_transaction(self):
360360
r2.save(force_update=True)
361361
self.assertEqual(Reporter.objects.get(pk=r1.pk).last_name, "Calculus")
362362

363+
@skipUnlessDBFeature('test_db_allows_multiple_connections')
364+
def test_atomic_prevents_queries_in_broken_transaction_after_client_close(self):
365+
with transaction.atomic():
366+
Reporter.objects.create(first_name="Archibald", last_name="Haddock")
367+
connection.close()
368+
# The connection is closed and the transaction is marked as
369+
# needing rollback. This will raise an InterfaceError on databases
370+
# that refuse to create cursors on closed connections (PostgreSQL)
371+
# and a TransactionManagementError on other databases.
372+
with self.assertRaises(Error):
373+
Reporter.objects.create(first_name="Cuthbert", last_name="Calculus")
374+
# The connection is usable again .
375+
self.assertEqual(Reporter.objects.count(), 0)
376+
363377

364378
@skipUnless(connection.vendor == 'mysql', "MySQL-specific behaviors")
365379
class AtomicMySQLTests(TransactionTestCase):

0 commit comments

Comments
 (0)