From c80be677c02d9bb63b4e96a3fd5b8d29bd11f0c6 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:37:44 +0000 Subject: [PATCH] [PR #10151/7c12b1a9 backport][3.11] Fix infinite callback loop when time is not moving forward (#10173) Co-authored-by: Bruce Merry <1963944+bmerry@users.noreply.github.com> Fixes #123'). --> Fixes #10149. --- CHANGES/10149.misc.rst | 4 ++++ aiohttp/web_protocol.py | 2 +- tests/test_web_functional.py | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 CHANGES/10149.misc.rst diff --git a/CHANGES/10149.misc.rst b/CHANGES/10149.misc.rst new file mode 100644 index 00000000000..61765a50fcf --- /dev/null +++ b/CHANGES/10149.misc.rst @@ -0,0 +1,4 @@ +Fixed an infinite loop that can occur when using aiohttp in combination +with `async-solipsism`_ -- by :user:`bmerry`. + +.. _async-solipsism: https://github.com/bmerry/async-solipsism diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index e8bb41abf97..3306b86bded 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -458,7 +458,7 @@ def _process_keepalive(self) -> None: loop = self._loop now = loop.time() close_time = self._next_keepalive_close_time - if now <= close_time: + if now < close_time: # Keep alive close check fired too early, reschedule self._keepalive_handle = loop.call_at(close_time, self._process_keepalive) return diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index a3a990141a1..e4979851300 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -2324,3 +2324,41 @@ async def handler(request: web.Request) -> web.Response: # Make 2nd request which will hit the race condition. async with client.get("/") as resp: assert resp.status == 200 + + +async def test_keepalive_expires_on_time(aiohttp_client: AiohttpClient) -> None: + """Test that the keepalive handle expires on time.""" + + async def handler(request: web.Request) -> web.Response: + body = await request.read() + assert b"" == body + return web.Response(body=b"OK") + + app = web.Application() + app.router.add_route("GET", "/", handler) + + connector = aiohttp.TCPConnector(limit=1) + client = await aiohttp_client(app, connector=connector) + + loop = asyncio.get_running_loop() + now = loop.time() + + # Patch loop time so we can control when the keepalive timeout is processed + with mock.patch.object(loop, "time") as loop_time_mock: + loop_time_mock.return_value = now + resp1 = await client.get("/") + await resp1.read() + request_handler = client.server.handler.connections[0] + + # Ensure the keep alive handle is set + assert request_handler._keepalive_handle is not None + + # Set the loop time to exactly the keepalive timeout + loop_time_mock.return_value = request_handler._next_keepalive_close_time + + # sleep twice to ensure the keep alive timeout is processed + await asyncio.sleep(0) + await asyncio.sleep(0) + + # Ensure the keep alive handle expires + assert request_handler._keepalive_handle is None