Skip to content

Commit b91ef1d

Browse files
committed
Make enable_cleanup_closed a NOOP for Python 3.12.7+ and 3.13.1+ (#9726)
(cherry picked from commit c3a9c3e)
1 parent 79ab3a4 commit b91ef1d

File tree

6 files changed

+78
-12
lines changed

6 files changed

+78
-12
lines changed

CHANGES/9726.misc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Passing ``enable_cleanup_closed`` to :py:class:`aiohttp.TCPConnector` is now ignored on Python 3.12.7+ and 3.13.1+ since the underlying bug that caused asyncio to leak SSL connections has been fixed upstream -- by :user:`bdraco`.

aiohttp/connector.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@
7676
HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET
7777
HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET
7878

79+
NEEDS_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < (
80+
3,
81+
13,
82+
1,
83+
) or sys.version_info < (3, 12, 7)
84+
# Cleanup closed is no longer needed after https://github.com/python/cpython/pull/118960
85+
# which first appeared in Python 3.12.7 and 3.13.1
86+
7987

8088
__all__ = ("BaseConnector", "TCPConnector", "UnixConnector", "NamedPipeConnector")
8189

@@ -284,6 +292,17 @@ def __init__(
284292

285293
# start cleanup closed transports task
286294
self._cleanup_closed_handle: Optional[asyncio.TimerHandle] = None
295+
296+
if enable_cleanup_closed and not NEEDS_CLEANUP_CLOSED:
297+
warnings.warn(
298+
"enable_cleanup_closed ignored because "
299+
"https://github.com/python/cpython/pull/118960 is fixed in "
300+
f"in Python version {sys.version_info}",
301+
DeprecationWarning,
302+
stacklevel=2,
303+
)
304+
enable_cleanup_closed = False
305+
287306
self._cleanup_closed_disabled = not enable_cleanup_closed
288307
self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = []
289308
self._cleanup_closed()

docs/client_reference.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,10 +979,14 @@ is controlled by *force_close* constructor's parameter).
979979
connection releasing (optional).
980980

981981
:param bool enable_cleanup_closed: some SSL servers do not properly complete
982-
SSL shutdown process, in that case asyncio leaks ssl connections.
982+
SSL shutdown process, in that case asyncio leaks SSL connections.
983983
If this parameter is set to True, aiohttp additionally aborts underlining
984984
transport after 2 seconds. It is off by default.
985985

986+
For Python version 3.12.7+, or 3.13.1 and later,
987+
this parameter is ignored because the asyncio SSL connection
988+
leak is fixed in these versions of Python.
989+
986990

987991
:param loop: :ref:`event loop<asyncio-event-loop>`
988992
used for handling connections.

tests/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from hashlib import md5, sha1, sha256
88
from pathlib import Path
99
from tempfile import TemporaryDirectory
10-
from typing import Any
10+
from typing import Any, Generator
1111
from unittest import mock
1212
from uuid import uuid4
1313

@@ -238,3 +238,14 @@ def key(key_data: Any):
238238
@pytest.fixture
239239
def ws_key(key: Any):
240240
return base64.b64encode(sha1(key + WS_KEY).digest()).decode()
241+
242+
243+
@pytest.fixture
244+
def enable_cleanup_closed() -> Generator[None, None, None]:
245+
"""Fixture to override the NEEDS_CLEANUP_CLOSED flag.
246+
247+
On Python 3.12.7+ and 3.13.1+ enable_cleanup_closed is not needed,
248+
however we still want to test that it works.
249+
"""
250+
with mock.patch("aiohttp.connector.NEEDS_CLEANUP_CLOSED", True):
251+
yield

tests/test_connector.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import pytest
1717
from aiohappyeyeballs import AddrInfoType
18+
from pytest_mock import MockerFixture
1819
from yarl import URL
1920

2021
import aiohttp
@@ -355,6 +356,7 @@ async def test_get_expired(loop: asyncio.AbstractEventLoop) -> None:
355356
await conn.close()
356357

357358

359+
@pytest.mark.usefixtures("enable_cleanup_closed")
358360
async def test_get_expired_ssl(loop: asyncio.AbstractEventLoop) -> None:
359361
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
360362
key = ConnectionKey("localhost", 80, True, False, None, None, None)
@@ -421,9 +423,16 @@ async def test_release(loop, key) -> None:
421423
await conn.close()
422424

423425

424-
async def test_release_ssl_transport(loop, ssl_key) -> None:
425-
conn = aiohttp.BaseConnector(loop=loop, enable_cleanup_closed=True)
426-
conn._release_waiter = mock.Mock()
426+
@pytest.mark.usefixtures("enable_cleanup_closed")
427+
async def test_release_ssl_transport(
428+
loop: asyncio.AbstractEventLoop, ssl_key: ConnectionKey
429+
) -> None:
430+
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
431+
with mock.patch.object(conn, "_release_waiter", autospec=True, spec_set=True):
432+
proto = create_mocked_conn(loop)
433+
transport = proto.transport
434+
conn._acquired.add(proto)
435+
conn._acquired_per_host[ssl_key].add(proto)
427436

428437
proto = mock.Mock()
429438
transport = proto.transport
@@ -1678,6 +1687,7 @@ async def test_close_during_connect(loop: asyncio.AbstractEventLoop) -> None:
16781687
assert proto.close.called
16791688

16801689

1690+
@pytest.mark.usefixtures("enable_cleanup_closed")
16811691
async def test_ctor_cleanup() -> None:
16821692
loop = mock.Mock()
16831693
loop.time.return_value = 1.5
@@ -1711,8 +1721,11 @@ async def test_cleanup(key: ConnectionKey) -> None:
17111721
assert conn._cleanup_handle is None
17121722

17131723

1714-
async def test_cleanup_close_ssl_transport(ssl_key) -> None:
1715-
proto = mock.Mock()
1724+
@pytest.mark.usefixtures("enable_cleanup_closed")
1725+
async def test_cleanup_close_ssl_transport(
1726+
loop: asyncio.AbstractEventLoop, ssl_key: ConnectionKey
1727+
) -> None:
1728+
proto = create_mocked_conn(loop)
17161729
transport = proto.transport
17171730
testset: DefaultDict[ConnectionKey, Deque[Tuple[ResponseHandler, float]]] = (
17181731
defaultdict(deque)
@@ -1779,7 +1792,10 @@ async def test_cleanup3(loop: asyncio.AbstractEventLoop, key: ConnectionKey) ->
17791792
await conn.close()
17801793

17811794

1782-
async def test_cleanup_closed(loop, mocker) -> None:
1795+
@pytest.mark.usefixtures("enable_cleanup_closed")
1796+
async def test_cleanup_closed(
1797+
loop: asyncio.AbstractEventLoop, mocker: MockerFixture
1798+
) -> None:
17831799
if not hasattr(loop, "__dict__"):
17841800
pytest.skip("can not override loop attributes")
17851801

@@ -1796,8 +1812,19 @@ async def test_cleanup_closed(loop, mocker) -> None:
17961812
assert cleanup_closed_handle.cancel.called
17971813

17981814

1799-
async def test_cleanup_closed_disabled(loop, mocker) -> None:
1800-
conn = aiohttp.BaseConnector(loop=loop, enable_cleanup_closed=False)
1815+
async def test_cleanup_closed_is_noop_on_fixed_cpython() -> None:
1816+
"""Ensure that enable_cleanup_closed is a noop on fixed Python versions."""
1817+
with mock.patch("aiohttp.connector.NEEDS_CLEANUP_CLOSED", False), pytest.warns(
1818+
DeprecationWarning, match="cleanup_closed ignored"
1819+
):
1820+
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
1821+
assert conn._cleanup_closed_disabled is True
1822+
1823+
1824+
async def test_cleanup_closed_disabled(
1825+
loop: asyncio.AbstractEventLoop, mocker: MockerFixture
1826+
) -> None:
1827+
conn = aiohttp.BaseConnector(enable_cleanup_closed=False)
18011828

18021829
tr = mock.Mock()
18031830
conn._cleanup_closed_transports = [tr]
@@ -2303,8 +2330,11 @@ async def test_close_abort_closed_transports(loop: asyncio.AbstractEventLoop) ->
23032330
assert conn.closed
23042331

23052332

2306-
async def test_close_cancels_cleanup_closed_handle(loop) -> None:
2307-
conn = aiohttp.BaseConnector(loop=loop, enable_cleanup_closed=True)
2333+
@pytest.mark.usefixtures("enable_cleanup_closed")
2334+
async def test_close_cancels_cleanup_closed_handle(
2335+
loop: asyncio.AbstractEventLoop,
2336+
) -> None:
2337+
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
23082338
assert conn._cleanup_closed_handle is not None
23092339
await conn.close()
23102340
assert conn._cleanup_closed_handle is None

tests/test_proxy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ async def make_conn():
384384
autospec=True,
385385
spec_set=True,
386386
)
387+
@pytest.mark.usefixtures("enable_cleanup_closed")
387388
def test_https_connect_fingerprint_mismatch(
388389
self, start_connection: mock.Mock, ClientRequestMock: mock.Mock
389390
) -> None:

0 commit comments

Comments
 (0)