Browse Source

Fixed #34865 -- Released memory earlier than garbage collection on database wrapping layers.

Thank you Florian Apolloner, Jake Howard and Patryk Zawadzki for
the clarifying comments and reviews.
fowczrek 1 month ago
parent
commit
6a9db1e626

+ 1 - 0
AUTHORS

@@ -349,6 +349,7 @@ answer newbie questions, and generally made Django that much better:
     Federico Capoano <nemesis@ninux.org>
     Felipe Lee <felipe.lee.garcia@gmail.com>
     Filip Noetzel <http://filip.noetzel.co.uk/>
+    Filip Owczarek <f.a.owczarek@gmail.com>
     Filip Wasilewski <filip.wasilewski@gmail.com>
     Finn Gruwier Larsen <finn@gruwier.dk>
     Fiza Ashraf <fizaashraf37@gmail.com>

+ 3 - 0
django/db/backends/base/client.py

@@ -13,6 +13,9 @@ class BaseDatabaseClient:
         # connection is an instance of BaseDatabaseWrapper.
         self.connection = connection
 
+    def __del__(self):
+        del self.connection
+
     @classmethod
     def settings_to_cmd_args_env(cls, settings_dict, parameters):
         raise NotImplementedError(

+ 3 - 0
django/db/backends/base/creation.py

@@ -25,6 +25,9 @@ class BaseDatabaseCreation:
     def __init__(self, connection):
         self.connection = connection
 
+    def __del__(self):
+        del self.connection
+
     def _nodb_cursor(self):
         return self.connection._nodb_cursor()
 

+ 3 - 0
django/db/backends/base/features.py

@@ -407,6 +407,9 @@ class BaseDatabaseFeatures:
     def __init__(self, connection):
         self.connection = connection
 
+    def __del__(self):
+        del self.connection
+
     @cached_property
     def supports_explaining_query_execution(self):
         """Does this backend support explaining query execution?"""

+ 3 - 0
django/db/backends/base/introspection.py

@@ -19,6 +19,9 @@ class BaseDatabaseIntrospection:
     def __init__(self, connection):
         self.connection = connection
 
+    def __del__(self):
+        del self.connection
+
     def get_field_type(self, data_type, description):
         """
         Hook for a database backend to use the cursor description to

+ 3 - 0
django/db/backends/base/operations.py

@@ -59,6 +59,9 @@ class BaseDatabaseOperations:
         self.connection = connection
         self._cache = None
 
+    def __del__(self):
+        del self.connection
+
     def autoinc_sql(self, table, column):
         """
         Return any SQL needed to support auto-incrementing primary keys, or

+ 3 - 0
django/db/backends/base/validation.py

@@ -4,6 +4,9 @@ class BaseDatabaseValidation:
     def __init__(self, connection):
         self.connection = connection
 
+    def __del__(self):
+        del self.connection
+
     def check(self, **kwargs):
         return []
 

+ 3 - 0
django/db/utils.py

@@ -64,6 +64,9 @@ class DatabaseErrorWrapper:
         """
         self.wrapper = wrapper
 
+    def __del__(self):
+        del self.wrapper
+
     def __enter__(self):
         pass
 

+ 31 - 0
tests/backends/base/test_base.py

@@ -1,3 +1,4 @@
+import gc
 from unittest.mock import MagicMock, patch
 
 from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
@@ -60,6 +61,36 @@ class DatabaseWrapperTests(SimpleTestCase):
         with patch.object(connection.features, "minimum_database_version", None):
             connection.check_database_version_supported()
 
+    def test_release_memory_without_garbage_collection(self):
+        # Schedule the restore of the garbage collection settings.
+        self.addCleanup(gc.set_debug, 0)
+        self.addCleanup(gc.enable)
+
+        # Disable automatic garbage collection to control when it's triggered,
+        # then run a full collection cycle to ensure `gc.garbage` is empty.
+        gc.disable()
+        gc.collect()
+
+        # The garbage list isn't automatically populated to avoid CPU overhead,
+        # so debugging needs to be enabled to track all unreachable items and
+        # have them stored in `gc.garbage`.
+        gc.set_debug(gc.DEBUG_SAVEALL)
+
+        # Create a new connection that will be closed during the test, and also
+        # ensure that a `DatabaseErrorWrapper` is created for this connection.
+        test_connection = connection.copy()
+        with test_connection.wrap_database_errors:
+            self.assertEqual(test_connection.queries, [])
+
+        # Close the connection and remove references to it. This will mark all
+        # objects related to the connection as garbage to be collected.
+        test_connection.close()
+        test_connection = None
+
+        # Enforce garbage collection to populate `gc.garbage` for inspection.
+        gc.collect()
+        self.assertEqual(gc.garbage, [])
+
 
 class DatabaseWrapperLoggingTests(TransactionTestCase):
     available_apps = ["backends"]