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 0bffc849817..3d76fc1f1d7 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -487,7 +487,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 239aea4c25e..8b65d1e08fb 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -2312,3 +2312,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