Skip to content

Commit b054963

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 821300b commit b054963

File tree

6 files changed

+80
-13
lines changed

6 files changed

+80
-13
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

@@ -279,6 +287,17 @@ def __init__(
279287

280288
# start cleanup closed transports task
281289
self._cleanup_closed_handle: Optional[asyncio.TimerHandle] = None
290+
291+
if enable_cleanup_closed and not NEEDS_CLEANUP_CLOSED:
292+
warnings.warn(
293+
"enable_cleanup_closed ignored because "
294+
"https://github.com/python/cpython/pull/118960 is fixed in "
295+
f"in Python version {sys.version_info}",
296+
DeprecationWarning,
297+
stacklevel=2,
298+
)
299+
enable_cleanup_closed = False
300+
282301
self._cleanup_closed_disabled = not enable_cleanup_closed
283302
self._cleanup_closed_transports: List[Optional[asyncio.Transport]] = []
284303
self._cleanup_closed()

docs/client_reference.rst

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

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

974+
For Python version 3.12.7+, or 3.13.1 and later,
975+
this parameter is ignored because the asyncio SSL connection
976+
leak is fixed in these versions of Python.
977+
974978

975979
:param loop: :ref:`event loop<asyncio-event-loop>`
976980
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
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616
from aiohappyeyeballs import AddrInfoType
17+
from pytest_mock import MockerFixture
1718
from yarl import URL
1819

1920
import aiohttp
@@ -359,6 +360,7 @@ async def test_get_expired(loop: asyncio.AbstractEventLoop) -> None:
359360
await conn.close()
360361

361362

363+
@pytest.mark.usefixtures("enable_cleanup_closed")
362364
async def test_get_expired_ssl(loop: asyncio.AbstractEventLoop) -> None:
363365
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
364366
key = ConnectionKey("localhost", 80, True, False, None, None, None)
@@ -425,9 +427,16 @@ async def test_release(loop, key) -> None:
425427
await conn.close()
426428

427429

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

432441
proto = mock.Mock()
433442
transport = proto.transport
@@ -1682,6 +1691,7 @@ async def test_close_during_connect(loop: asyncio.AbstractEventLoop) -> None:
16821691
assert proto.close.called
16831692

16841693

1694+
@pytest.mark.usefixtures("enable_cleanup_closed")
16851695
async def test_ctor_cleanup() -> None:
16861696
loop = mock.Mock()
16871697
loop.time.return_value = 1.5
@@ -1712,8 +1722,11 @@ async def test_cleanup(key) -> None:
17121722
assert conn._cleanup_handle is None
17131723

17141724

1715-
async def test_cleanup_close_ssl_transport(ssl_key) -> None:
1716-
proto = mock.Mock()
1725+
@pytest.mark.usefixtures("enable_cleanup_closed")
1726+
async def test_cleanup_close_ssl_transport(
1727+
loop: asyncio.AbstractEventLoop, ssl_key: ConnectionKey
1728+
) -> None:
1729+
proto = create_mocked_conn(loop)
17171730
transport = proto.transport
17181731
testset = {ssl_key: [(proto, 10)]}
17191732

@@ -1769,7 +1782,10 @@ async def test_cleanup3(key) -> None:
17691782
await conn.close()
17701783

17711784

1772-
async def test_cleanup_closed(loop, mocker) -> None:
1785+
@pytest.mark.usefixtures("enable_cleanup_closed")
1786+
async def test_cleanup_closed(
1787+
loop: asyncio.AbstractEventLoop, mocker: MockerFixture
1788+
) -> None:
17731789
if not hasattr(loop, "__dict__"):
17741790
pytest.skip("can not override loop attributes")
17751791

@@ -1786,8 +1802,19 @@ async def test_cleanup_closed(loop, mocker) -> None:
17861802
assert cleanup_closed_handle.cancel.called
17871803

17881804

1789-
async def test_cleanup_closed_disabled(loop, mocker) -> None:
1790-
conn = aiohttp.BaseConnector(loop=loop, enable_cleanup_closed=False)
1805+
async def test_cleanup_closed_is_noop_on_fixed_cpython() -> None:
1806+
"""Ensure that enable_cleanup_closed is a noop on fixed Python versions."""
1807+
with mock.patch("aiohttp.connector.NEEDS_CLEANUP_CLOSED", False), pytest.warns(
1808+
DeprecationWarning, match="cleanup_closed ignored"
1809+
):
1810+
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
1811+
assert conn._cleanup_closed_disabled is True
1812+
1813+
1814+
async def test_cleanup_closed_disabled(
1815+
loop: asyncio.AbstractEventLoop, mocker: MockerFixture
1816+
) -> None:
1817+
conn = aiohttp.BaseConnector(enable_cleanup_closed=False)
17911818

17921819
tr = mock.Mock()
17931820
conn._cleanup_closed_transports = [tr]
@@ -2293,8 +2320,11 @@ async def test_close_abort_closed_transports(loop: asyncio.AbstractEventLoop) ->
22932320
assert conn.closed
22942321

22952322

2296-
async def test_close_cancels_cleanup_closed_handle(loop) -> None:
2297-
conn = aiohttp.BaseConnector(loop=loop, enable_cleanup_closed=True)
2323+
@pytest.mark.usefixtures("enable_cleanup_closed")
2324+
async def test_close_cancels_cleanup_closed_handle(
2325+
loop: asyncio.AbstractEventLoop,
2326+
) -> None:
2327+
conn = aiohttp.BaseConnector(enable_cleanup_closed=True)
22982328
assert conn._cleanup_closed_handle is not None
22992329
await conn.close()
23002330
assert conn._cleanup_closed_handle is None

tests/test_proxy.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,9 @@ async def make_conn():
384384
autospec=True,
385385
spec_set=True,
386386
)
387-
def test_https_connect(self, start_connection: Any, ClientRequestMock: Any) -> None:
387+
def test_https_connect(
388+
self, start_connection: mock.Mock, ClientRequestMock: mock.Mock
389+
) -> None:
388390
proxy_req = ClientRequest(
389391
"GET", URL("http://proxy.example.com"), loop=self.loop
390392
)

0 commit comments

Comments
 (0)