From e56211f6117801719e0f63c508a45ccd6491f71f Mon Sep 17 00:00:00 2001 From: lipengyu Date: Sun, 6 Sep 2026 16:56:00 +0800 Subject: [PATCH 1/2] gh-157025: Fix transport cleanup when cancelling asyncio native sendfile Ensure cancellation while native sendfile waits for the write buffer to drain resets the empty waiter, restores the prior reading state, and restores the selector transport registry entry. --- Lib/asyncio/proactor_events.py | 3 +- Lib/asyncio/selector_events.py | 3 +- Lib/test/test_asyncio/test_sendfile.py | 41 +++++++++++++++++++ ...-09-06-15-12-18.gh-issue-157025.ZvFpFT.rst | 2 + 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-06-15-12-18.gh-issue-157025.ZvFpFT.rst diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 7adb09f3fce5de7..97ad1e4fe144a8f 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -766,8 +766,9 @@ async def _sock_sendfile_native(self, sock, file, offset, count): async def _sendfile_native(self, transp, file, offset, count): resume_reading = transp.is_reading() transp.pause_reading() - await transp._make_empty_waiter() + empty_waiter = transp._make_empty_waiter() try: + await empty_waiter return await self.sock_sendfile(transp._sock, file, offset, count, fallback=False) finally: diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 16d5c1b6f0a3e19..93bacadda899ab2 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -739,8 +739,9 @@ async def _sendfile_native(self, transp, file, offset, count): del self._transports[transp._sock_fd] resume_reading = transp.is_reading() transp.pause_reading() - await transp._make_empty_waiter() + empty_waiter = transp._make_empty_waiter() try: + await empty_waiter return await self.sock_sendfile(transp._sock, file, offset, count, fallback=False) finally: diff --git a/Lib/test/test_asyncio/test_sendfile.py b/Lib/test/test_asyncio/test_sendfile.py index c8d429c3d1651f0..91bbbc2e6993dae 100644 --- a/Lib/test/test_asyncio/test_sendfile.py +++ b/Lib/test/test_asyncio/test_sendfile.py @@ -376,6 +376,47 @@ def test_sendfile(self): self.assertEqual(srv_proto.data, self.DATA) self.assertEqual(self.file.tell(), len(self.DATA)) + def test_sendfile_cancel_empty_waiter(self): + for reading in (True, False): + with self.subTest(reading=reading): + srv_proto, cli_proto = self.prepare_sendfile() + transport = cli_proto.transport + if not reading: + transport.pause_reading() + waiter = self.loop.create_future() + + def make_empty_waiter(): + transport._empty_waiter = waiter + return waiter + + with mock.patch.object(transport, '_make_empty_waiter', + side_effect=make_empty_waiter): + task = self.loop.create_task( + self.loop.sendfile(transport, self.file)) + test_utils.run_briefly(self.loop) + self.assertIs(transport._empty_waiter, waiter) + self.assertFalse(waiter.done()) + self.assertFalse(transport.is_reading()) + task.cancel() + with self.assertRaises(asyncio.CancelledError): + self.run_loop(task) + + try: + self.assertIsNone(transport._empty_waiter) + self.assertEqual(transport.is_reading(), reading) + if isinstance(self.loop, asyncio.SelectorEventLoop): + self.assertIs( + self.loop._transports[transport._sock_fd], + transport) + finally: + transport._reset_empty_waiter() + + ret = self.run_loop(self.loop.sendfile(transport, self.file)) + transport.close() + self.run_loop(srv_proto.done) + self.assertEqual(ret, len(self.DATA)) + self.assertEqual(srv_proto.data, self.DATA) + def test_sendfile_force_fallback(self): srv_proto, cli_proto = self.prepare_sendfile() diff --git a/Misc/NEWS.d/next/Library/2026-09-06-15-12-18.gh-issue-157025.ZvFpFT.rst b/Misc/NEWS.d/next/Library/2026-09-06-15-12-18.gh-issue-157025.ZvFpFT.rst new file mode 100644 index 000000000000000..fd1a6d7c5f47527 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-06-15-12-18.gh-issue-157025.ZvFpFT.rst @@ -0,0 +1,2 @@ +Fix transport cleanup when cancelling :meth:`asyncio.loop.sendfile` while +waiting for the write buffer to drain in the native implementation. From b0d3b343ebc14d938ba6145579d177771c3a9a85 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Sun, 6 Sep 2026 23:10:56 +0800 Subject: [PATCH 2/2] update --- Lib/asyncio/proactor_events.py | 3 +-- Lib/asyncio/selector_events.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 97ad1e4fe144a8f..6717f067caa08bb 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -766,9 +766,8 @@ async def _sock_sendfile_native(self, sock, file, offset, count): async def _sendfile_native(self, transp, file, offset, count): resume_reading = transp.is_reading() transp.pause_reading() - empty_waiter = transp._make_empty_waiter() try: - await empty_waiter + await transp._make_empty_waiter() return await self.sock_sendfile(transp._sock, file, offset, count, fallback=False) finally: diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 93bacadda899ab2..d843de7cde9b87f 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -739,9 +739,8 @@ async def _sendfile_native(self, transp, file, offset, count): del self._transports[transp._sock_fd] resume_reading = transp.is_reading() transp.pause_reading() - empty_waiter = transp._make_empty_waiter() try: - await empty_waiter + await transp._make_empty_waiter() return await self.sock_sendfile(transp._sock, file, offset, count, fallback=False) finally: