diff --git a/doc/changelog.rst b/doc/changelog.rst index 6fffcdf696..c008066c22 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -42,6 +42,16 @@ PyMongo 4.9 brings a number of improvements including: - Fixed a bug where PyMongo would raise ``InvalidBSON: date value out of range`` when using :attr:`~bson.codec_options.DatetimeConversion.DATETIME_CLAMP` or :attr:`~bson.codec_options.DatetimeConversion.DATETIME_AUTO` with a non-UTC timezone. +- Added a warning to unclosed MongoClient instances + telling users to explicitly close clients when finished with them to avoid leaking resources. + For example: + + .. code-block:: + + sys:1: ResourceWarning: Unclosed MongoClient opened at: + File "/Users//my_file.py", line 8, in `` + client = MongoClient() + Call MongoClient.close() to safely shut down your client and free up resources. - The default value for ``connect`` in ``MongoClient`` is changed to ``False`` when running on unction-as-a-service (FaaS) like AWS Lambda, Google Cloud Functions, and Microsoft Azure Functions. On some FaaS systems, there is a ``fork()`` operation at function diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index b5e73e8de8..a84fbf2e59 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -34,6 +34,7 @@ import contextlib import os +import warnings import weakref from collections import defaultdict from typing import ( @@ -871,6 +872,7 @@ def __init__( ) self._opened = False + self._closed = False self._init_background() if _IS_SYNC and connect: @@ -1180,6 +1182,22 @@ def __getitem__(self, name: str) -> database.AsyncDatabase[_DocumentType]: """ return database.AsyncDatabase(self, name) + def __del__(self) -> None: + """Check that this AsyncMongoClient has been closed and issue a warning if not.""" + try: + if not self._closed: + warnings.warn( + ( + f"Unclosed {type(self).__name__} opened at:\n{self._topology_settings._stack}" + f"Call {type(self).__name__}.close() to safely shut down your client and free up resources." + ), + ResourceWarning, + stacklevel=2, + source=self, + ) + except AttributeError: + pass + def _close_cursor_soon( self, cursor_id: int, @@ -1547,6 +1565,7 @@ async def close(self) -> None: if self._encrypter: # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. await self._encrypter.close() + self._closed = True if not _IS_SYNC: # Add support for contextlib.aclosing. diff --git a/pymongo/asynchronous/settings.py b/pymongo/asynchronous/settings.py index c41c638e6c..1103e1bd18 100644 --- a/pymongo/asynchronous/settings.py +++ b/pymongo/asynchronous/settings.py @@ -82,7 +82,7 @@ def __init__( self._topology_id = ObjectId() # Store the allocation traceback to catch unclosed clients in the # test suite. - self._stack = "".join(traceback.format_stack()) + self._stack = "".join(traceback.format_stack()[:-2]) @property def seeds(self) -> Collection[tuple[str, int]]: diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 26af488acd..cec78463b3 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -34,6 +34,7 @@ import contextlib import os +import warnings import weakref from collections import defaultdict from typing import ( @@ -871,6 +872,7 @@ def __init__( ) self._opened = False + self._closed = False self._init_background() if _IS_SYNC and connect: @@ -1180,6 +1182,22 @@ def __getitem__(self, name: str) -> database.Database[_DocumentType]: """ return database.Database(self, name) + def __del__(self) -> None: + """Check that this MongoClient has been closed and issue a warning if not.""" + try: + if not self._closed: + warnings.warn( + ( + f"Unclosed {type(self).__name__} opened at:\n{self._topology_settings._stack}" + f"Call {type(self).__name__}.close() to safely shut down your client and free up resources." + ), + ResourceWarning, + stacklevel=2, + source=self, + ) + except AttributeError: + pass + def _close_cursor_soon( self, cursor_id: int, @@ -1543,6 +1561,7 @@ def close(self) -> None: if self._encrypter: # TODO: PYTHON-1921 Encrypted MongoClients cannot be re-opened. self._encrypter.close() + self._closed = True if not _IS_SYNC: # Add support for contextlib.closing. diff --git a/pymongo/synchronous/settings.py b/pymongo/synchronous/settings.py index 8719e86083..040776713f 100644 --- a/pymongo/synchronous/settings.py +++ b/pymongo/synchronous/settings.py @@ -82,7 +82,7 @@ def __init__( self._topology_id = ObjectId() # Store the allocation traceback to catch unclosed clients in the # test suite. - self._stack = "".join(traceback.format_stack()) + self._stack = "".join(traceback.format_stack()[:-2]) @property def seeds(self) -> Collection[tuple[str, int]]: diff --git a/pyproject.toml b/pyproject.toml index 225be8e1d8..19db00f19a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,9 @@ filterwarnings = [ "module:please use dns.resolver.Resolver.resolve:DeprecationWarning", # https://github.com/dateutil/dateutil/issues/1314 "module:datetime.datetime.utc:DeprecationWarning:dateutil", + # TODO: Remove both of these in https://jira.mongodb.org/browse/PYTHON-4731 + "ignore:Unclosed AsyncMongoClient*", + "ignore:Unclosed MongoClient*", ] markers = [ "auth_aws: tests that rely on pymongo-auth-aws",