diff --git a/doc/changelog.rst b/doc/changelog.rst index 978233061e..ad6ca5c635 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -30,12 +30,16 @@ Bug fixes :class:`~pymongo.asynchronous.cursor.AsyncCursor` to be ``Optional[Mapping[str, Any]]`` instead of ``Optional[bool]``, matching what the argument accepts at runtime (`PYTHON-6145`_). +- Disabled TLS session reuse on Python 3.11 + due to a CPython memory leak bug (`python/cpython#116810 `_). + The bug was fixed in Python 3.12.7 and Python 3.13.1 (`PYTHON-6182`_). .. _PYTHON-5929: https://jira.mongodb.org/browse/PYTHON-5929 .. _PYTHON-6172: https://jira.mongodb.org/browse/PYTHON-6172 .. _PYTHON-6149: https://jira.mongodb.org/browse/PYTHON-6149 .. _PYTHON-5873: https://jira.mongodb.org/browse/PYTHON-5873 .. _PYTHON-6145: https://jira.mongodb.org/browse/PYTHON-6145 +.. _PYTHON-6182: https://jira.mongodb.org/browse/PYTHON-6182 Changes in Version 4.19.0 (2026/10/14) -------------------------------------- diff --git a/pymongo/pool_shared.py b/pymongo/pool_shared.py index ed4176fb4a..48a0a27bf1 100644 --- a/pymongo/pool_shared.py +++ b/pymongo/pool_shared.py @@ -87,6 +87,10 @@ _ConnT = TypeVar("_ConnT", bound="_AgnosticBaseConnection[Any]") _PoolT = TypeVar("_PoolT", bound="_AgnosticBasePool[Any]") +# Reading SSLSocket.session / SSLObject.session leaks a copy of the session on +# every call in CPython 3.11, so disable TLS session reuse on that version. +_TLS_SESSION_REUSE_LEAKS = sys.version_info < (3, 12) + class _ConnectionTelemetryInfo(Protocol): """Protocol for connection fields consumed by :class:`~pymongo._telemetry._CommandTelemetry`.""" @@ -308,7 +312,7 @@ def __init__( self._pending = 0 self._max_connecting = self.opts.max_connecting self._ssl_session_cache: Optional[list[Any]] = ( - [None] if self.opts._ssl_context is not None else None + [None] if self.opts._ssl_context is not None and not _TLS_SESSION_REUSE_LEAKS else None ) self._telemetry = _CmapTelemetry( client_id, address, options._event_listeners, publish=not is_sdam, log=not is_sdam diff --git a/test/asynchronous/test_ssl.py b/test/asynchronous/test_ssl.py index c66fb30af1..271f5e37fe 100644 --- a/test/asynchronous/test_ssl.py +++ b/test/asynchronous/test_ssl.py @@ -31,6 +31,7 @@ from pymongo.errors import ConfigurationError, ConnectionFailure, OperationFailure from pymongo.hello import HelloCompat from pymongo.pool_shared import ( + _TLS_SESSION_REUSE_LEAKS, _configured_protocol_interface, _configured_socket_interface, _get_ssl_session, @@ -923,11 +924,19 @@ async def test_pyopenssl_ignored_in_async(self): await client.close() @async_client_context.require_tls + @unittest.skipIf(_TLS_SESSION_REUSE_LEAKS, "TLS session reuse is not supported on Python 3.11") async def test_pool_has_ssl_session_cache(self): pool = list(self.client._topology._servers.values())[0].pool self.assertIsInstance(pool._ssl_session_cache, list) @async_client_context.require_tls + @unittest.skipUnless(_TLS_SESSION_REUSE_LEAKS, "TLS session reuse is enabled on Python 3.12+") + async def test_pool_has_no_ssl_session_cache_on_python_311(self): + pool = list(self.client._topology._servers.values())[0].pool + self.assertIsNone(pool._ssl_session_cache) + + @async_client_context.require_tls + @unittest.skipIf(_TLS_SESSION_REUSE_LEAKS, "TLS session reuse is not supported on Python 3.11") @unittest.skipUnless( _IS_SYNC and _HAVE_PYOPENSSL, "Sync stdlib ssl may return None for session on TLS 1.3; test limited to PyOpenSSL", diff --git a/test/test_ssl.py b/test/test_ssl.py index c725bda984..cc1b900eeb 100644 --- a/test/test_ssl.py +++ b/test/test_ssl.py @@ -31,6 +31,7 @@ from pymongo.errors import ConfigurationError, ConnectionFailure, OperationFailure from pymongo.hello import HelloCompat from pymongo.pool_shared import ( + _TLS_SESSION_REUSE_LEAKS, _configured_socket_interface, _get_ssl_session, ) @@ -912,11 +913,19 @@ def test_pyopenssl_ignored_in_async(self): client.close() @client_context.require_tls + @unittest.skipIf(_TLS_SESSION_REUSE_LEAKS, "TLS session reuse is not supported on Python 3.11") def test_pool_has_ssl_session_cache(self): pool = list(self.client._topology._servers.values())[0].pool self.assertIsInstance(pool._ssl_session_cache, list) @client_context.require_tls + @unittest.skipUnless(_TLS_SESSION_REUSE_LEAKS, "TLS session reuse is enabled on Python 3.12+") + def test_pool_has_no_ssl_session_cache_on_python_311(self): + pool = list(self.client._topology._servers.values())[0].pool + self.assertIsNone(pool._ssl_session_cache) + + @client_context.require_tls + @unittest.skipIf(_TLS_SESSION_REUSE_LEAKS, "TLS session reuse is not supported on Python 3.11") @unittest.skipUnless( _IS_SYNC and _HAVE_PYOPENSSL, "Sync stdlib ssl may return None for session on TLS 1.3; test limited to PyOpenSSL",