mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[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:
		| @@ -63,6 +63,7 @@ class BaseDatabaseWrapper(object): | |||||||
|  |  | ||||||
|         # Connection termination related attributes |         # Connection termination related attributes | ||||||
|         self.close_at = None |         self.close_at = None | ||||||
|  |         self.closed_in_transaction = False | ||||||
|         self.errors_occurred = False |         self.errors_occurred = False | ||||||
|  |  | ||||||
|         # Thread-safety related attributes |         # Thread-safety related attributes | ||||||
| @@ -103,9 +104,11 @@ class BaseDatabaseWrapper(object): | |||||||
|         # In case the previous connection was closed while in an atomic block |         # In case the previous connection was closed while in an atomic block | ||||||
|         self.in_atomic_block = False |         self.in_atomic_block = False | ||||||
|         self.savepoint_ids = [] |         self.savepoint_ids = [] | ||||||
|  |         self.needs_rollback = False | ||||||
|         # Reset parameters defining when to close the connection |         # Reset parameters defining when to close the connection | ||||||
|         max_age = self.settings_dict['CONN_MAX_AGE'] |         max_age = self.settings_dict['CONN_MAX_AGE'] | ||||||
|         self.close_at = None if max_age is None else time.time() + 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 |         self.errors_occurred = False | ||||||
|         # Establish the connection |         # Establish the connection | ||||||
|         conn_params = self.get_connection_params() |         conn_params = self.get_connection_params() | ||||||
| @@ -188,6 +191,10 @@ class BaseDatabaseWrapper(object): | |||||||
|         try: |         try: | ||||||
|             self._close() |             self._close() | ||||||
|         finally: |         finally: | ||||||
|  |             if self.in_atomic_block: | ||||||
|  |                 self.closed_in_transaction = True | ||||||
|  |                 self.needs_rollback = True | ||||||
|  |             else: | ||||||
|                 self.connection = None |                 self.connection = None | ||||||
|         self.set_clean() |         self.set_clean() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ PostgreSQL database backend for Django. | |||||||
|  |  | ||||||
| Requires psycopg 2: http://initd.org/projects/psycopg2 | Requires psycopg 2: http://initd.org/projects/psycopg2 | ||||||
| """ | """ | ||||||
| import logging |  | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from django.db.backends import * | 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(SafeBytes, psycopg2.extensions.QuotedString) | ||||||
| psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) | psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) | ||||||
|  |  | ||||||
| logger = logging.getLogger('django.db.backends') |  | ||||||
|  |  | ||||||
| def utc_tzinfo_factory(offset): | def utc_tzinfo_factory(offset): | ||||||
|     if offset != 0: |     if offset != 0: | ||||||
|         raise AssertionError("database connection isn't set to UTC") |         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 |         cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None | ||||||
|         return cursor |         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): |     def _set_isolation_level(self, isolation_level): | ||||||
|         assert isolation_level in range(1, 5)     # Use set_autocommit for level = 0 |         assert isolation_level in range(1, 5)     # Use set_autocommit for level = 0 | ||||||
|         if self.psycopg2_version >= (2, 4, 2): |         if self.psycopg2_version >= (2, 4, 2): | ||||||
|   | |||||||
| @@ -290,7 +290,12 @@ class Atomic(object): | |||||||
|             connection.in_atomic_block = False |             connection.in_atomic_block = False | ||||||
|  |  | ||||||
|         try: |         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: |                 if connection.in_atomic_block: | ||||||
|                     # Release savepoint if there is one |                     # Release savepoint if there is one | ||||||
|                     if sid is not None: |                     if sid is not None: | ||||||
| @@ -330,12 +335,17 @@ class Atomic(object): | |||||||
|         finally: |         finally: | ||||||
|             # Outermost block exit when autocommit was enabled. |             # Outermost block exit when autocommit was enabled. | ||||||
|             if not connection.in_atomic_block: |             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 |                     connection.autocommit = True | ||||||
|                 else: |                 else: | ||||||
|                     connection.set_autocommit(True) |                     connection.set_autocommit(True) | ||||||
|             # Outermost block exit when autocommit was disabled. |             # Outermost block exit when autocommit was disabled. | ||||||
|             elif not connection.savepoint_ids and not connection.commit_on_exit: |             elif not connection.savepoint_ids and not connection.commit_on_exit: | ||||||
|  |                 if connection.closed_in_transaction: | ||||||
|  |                     connection.connection = None | ||||||
|  |                 else: | ||||||
|                     connection.in_atomic_block = False |                     connection.in_atomic_block = False | ||||||
|  |  | ||||||
|     def __call__(self, func): |     def __call__(self, func): | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ except ImportError: | |||||||
| import time | import time | ||||||
|  |  | ||||||
| from django.db import (connection, transaction, | from django.db import (connection, transaction, | ||||||
|     DatabaseError, IntegrityError, OperationalError) |     DatabaseError, Error, IntegrityError, OperationalError) | ||||||
| from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature | from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature | ||||||
| from django.test.utils import IgnorePendingDeprecationWarningsMixin | from django.test.utils import IgnorePendingDeprecationWarningsMixin | ||||||
| from django.utils import six | from django.utils import six | ||||||
| @@ -359,6 +359,20 @@ class AtomicErrorsTests(TransactionTestCase): | |||||||
|             r2.save(force_update=True) |             r2.save(force_update=True) | ||||||
|         self.assertEqual(Reporter.objects.get(pk=r1.pk).last_name, "Calculus") |         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") | @skipUnless(connection.vendor == 'mysql', "MySQL-specific behaviors") | ||||||
| class AtomicMySQLTests(TransactionTestCase): | class AtomicMySQLTests(TransactionTestCase): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user