diff --git a/CHANGES/8510.misc.rst b/CHANGES/8510.misc.rst new file mode 100644 index 00000000000..d0a90c7388f --- /dev/null +++ b/CHANGES/8510.misc.rst @@ -0,0 +1 @@ +When using Python 3.12 or later, the writer is no longer scheduled on the event loop if it can finish synchronously. Avoiding event loop scheduling reduces latency and improves performance. -- by :user:`bdraco`. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 127708c9ef8..5f71cb6b9bd 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -283,7 +283,12 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None: if self.__writer is not None: self.__writer.remove_done_callback(self.__reset_writer) self.__writer = writer - if writer is not None: + if writer is None: + return + if writer.done(): + # The writer is already done, so we can reset it immediately. + self.__reset_writer() + else: writer.add_done_callback(self.__reset_writer) def is_ssl(self) -> bool: @@ -657,9 +662,17 @@ async def send(self, conn: "Connection") -> "ClientResponse": self.method, path, v=self.version ) await writer.write_headers(status_line, self.headers) + coro = self.write_bytes(writer, conn) - self._writer = self.loop.create_task(self.write_bytes(writer, conn)) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to write + # bytes immediately to avoid having to schedule + # the task on the event loop. + task = asyncio.Task(coro, loop=self.loop, eager_start=True) + else: + task = self.loop.create_task(coro) + self._writer = task response_class = self.response_class assert response_class is not None self.response = response_class( @@ -776,7 +789,12 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None: if self.__writer is not None: self.__writer.remove_done_callback(self.__reset_writer) self.__writer = writer - if writer is not None: + if writer is None: + return + if writer.done(): + # The writer is already done, so we can reset it immediately. + self.__reset_writer() + else: writer.add_done_callback(self.__reset_writer) @reify diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 8ef90b5a1d5..4d8f6f5b7ac 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -972,8 +972,15 @@ async def gen(): req = ClientRequest("POST", URL("http://python.org/"), data=gen(), loop=loop) assert req.chunked assert req.headers["TRANSFER-ENCODING"] == "chunked" + original_write_bytes = req.write_bytes - resp = await req.send(conn) + async def _mock_write_bytes(*args, **kwargs): + # Ensure the task is scheduled + await asyncio.sleep(0) + return await original_write_bytes(*args, **kwargs) + + with mock.patch.object(req, "write_bytes", _mock_write_bytes): + resp = await req.send(conn) assert asyncio.isfuture(req._writer) await resp.wait_for_close() assert req._writer is None @@ -1155,14 +1162,28 @@ async def test_oserror_on_write_bytes(loop: Any, conn: Any) -> None: async def test_terminate(loop: Any, conn: Any) -> None: req = ClientRequest("get", URL("http://python.org"), loop=loop) - resp = await req.send(conn) + + async def _mock_write_bytes(*args, **kwargs): + # Ensure the task is scheduled + await asyncio.sleep(0) + + with mock.patch.object(req, "write_bytes", _mock_write_bytes): + resp = await req.send(conn) + assert req._writer is not None - writer = req._writer = WriterMock() + assert resp._writer is not None + await resp._writer + writer = WriterMock() + writer.done = mock.Mock(return_value=False) writer.cancel = mock.Mock() + req._writer = writer + resp._writer = writer + assert req._writer is not None + assert resp._writer is not None req.terminate() - assert req._writer is None writer.cancel.assert_called_with() + writer.done.assert_called_with() resp.close() @@ -1172,9 +1193,19 @@ def test_terminate_with_closed_loop(loop: Any, conn: Any) -> None: async def go(): nonlocal req, resp, writer req = ClientRequest("get", URL("http://python.org"), loop=loop) - resp = await req.send(conn) + + async def _mock_write_bytes(*args, **kwargs): + # Ensure the task is scheduled + await asyncio.sleep(0) + + with mock.patch.object(req, "write_bytes", _mock_write_bytes): + resp = await req.send(conn) + assert req._writer is not None - writer = req._writer = WriterMock() + writer = WriterMock() + writer.done = mock.Mock(return_value=False) + req._writer = writer + resp._writer = writer await asyncio.sleep(0.05)