diff --git a/AUTHORS b/AUTHORS index bd972b16c7..956a5e06f1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -349,6 +349,7 @@ answer newbie questions, and generally made Django that much better: Federico Capoano Felipe Lee Filip Noetzel + Filip Owczarek Filip Wasilewski Finn Gruwier Larsen Fiza Ashraf diff --git a/django/db/backends/base/client.py b/django/db/backends/base/client.py index 031056372d..1640bedcad 100644 --- a/django/db/backends/base/client.py +++ b/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( diff --git a/django/db/backends/base/creation.py b/django/db/backends/base/creation.py index 845235bc72..1ed583f9e4 100644 --- a/django/db/backends/base/creation.py +++ b/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() diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 95230bb06b..6617894afa 100644 --- a/django/db/backends/base/features.py +++ b/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?""" diff --git a/django/db/backends/base/introspection.py b/django/db/backends/base/introspection.py index 5e4acb3ff9..12360538b9 100644 --- a/django/db/backends/base/introspection.py +++ b/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 diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py index 5d1f260edf..fea73bc1e4 100644 --- a/django/db/backends/base/operations.py +++ b/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 diff --git a/django/db/backends/base/validation.py b/django/db/backends/base/validation.py index d0e3e2157d..b57b7dd16d 100644 --- a/django/db/backends/base/validation.py +++ b/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 [] diff --git a/django/db/utils.py b/django/db/utils.py index e45f1db249..faaf3bf862 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -64,6 +64,9 @@ class DatabaseErrorWrapper: """ self.wrapper = wrapper + def __del__(self): + del self.wrapper + def __enter__(self): pass diff --git a/tests/backends/base/test_base.py b/tests/backends/base/test_base.py index 4418d010ea..8f47e30172 100644 --- a/tests/backends/base/test_base.py +++ b/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"]