From 476d707435fd199e995a3f9cda3897f295bed100 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Mon, 3 Aug 2026 23:26:06 +0300 Subject: [PATCH 1/2] gh-154971: Decompose IocpProactor._poll() --- Lib/asyncio/windows_events.py | 75 +++++++++++-------- Lib/test/test_asyncio/test_windows_events.py | 22 ++++++ ...-08-03-22-31-37.gh-issue-154971.cQPtrD.rst | 2 + 3 files changed, 66 insertions(+), 33 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py index efc7c0c158d3905..2a7c18cda8a76a5 100644 --- a/Lib/asyncio/windows_events.py +++ b/Lib/asyncio/windows_events.py @@ -760,6 +760,46 @@ def _get_accept_socket(self, family): s.settimeout(0) return s + def _process_completion_status(self, status): + """Process a single status from the completion port. + + A caller that waits on the completion port itself can pass each + status it receives here. + """ + err, transferred, key, address = status + try: + f, ov, obj, callback = self._cache.pop(address) + except KeyError: + if self._loop.get_debug(): + self._loop.call_exception_handler({ + 'message': ('GetQueuedCompletionStatus() returned an ' + 'unexpected event'), + 'status': ('err=%s transferred=%s key=%#x address=%#x' + % (err, transferred, key, address)), + }) + + # key is either zero, or it is used to return a pipe + # handle which should be closed to avoid a leak. + if key not in (0, _overlapped.INVALID_HANDLE_VALUE): + _winapi.CloseHandle(key) + return + + if obj in self._stopped_serving: + f.cancel() + # Don't call the callback if _register() already read the result or + # if the overlapped has been cancelled + elif not f.done(): + try: + value = callback(transferred, key, ov) + except OSError as e: + f.set_exception(e) + self._results.append(f) + else: + f.set_result(value) + self._results.append(f) + finally: + f = None + def _poll(self, timeout=None): if timeout is None: ms = INFINITE @@ -778,39 +818,8 @@ def _poll(self, timeout=None): break ms = 0 - err, transferred, key, address = status - try: - f, ov, obj, callback = self._cache.pop(address) - except KeyError: - if self._loop.get_debug(): - self._loop.call_exception_handler({ - 'message': ('GetQueuedCompletionStatus() returned an ' - 'unexpected event'), - 'status': ('err=%s transferred=%s key=%#x address=%#x' - % (err, transferred, key, address)), - }) - - # key is either zero, or it is used to return a pipe - # handle which should be closed to avoid a leak. - if key not in (0, _overlapped.INVALID_HANDLE_VALUE): - _winapi.CloseHandle(key) - continue - - if obj in self._stopped_serving: - f.cancel() - # Don't call the callback if _register() already read the result or - # if the overlapped has been cancelled - elif not f.done(): - try: - value = callback(transferred, key, ov) - except OSError as e: - f.set_exception(e) - self._results.append(f) - else: - f.set_result(value) - self._results.append(f) - finally: - f = None + # gh-154971: split out so custom event loops can call it directly + self._process_completion_status(status) # Remove unregistered futures for ov in self._unregistered: diff --git a/Lib/test/test_asyncio/test_windows_events.py b/Lib/test/test_asyncio/test_windows_events.py index c23427b8652069d..bb4ba74f19a17f0 100644 --- a/Lib/test/test_asyncio/test_windows_events.py +++ b/Lib/test/test_asyncio/test_windows_events.py @@ -327,6 +327,28 @@ def threadMain(): stop.set() thr.join() + def test_custom_poll_integration(self): + # gh-154971: a caller can wait on the completion port and process statuses itself + proactor = self.loop._proactor + + a, b = socket.socketpair() + self.addCleanup(a.close) + self.addCleanup(b.close) + + fut = proactor.recv(a, 100) + self.assertFalse(fut.done()) + + b.send(b'data') + + deadline = time.monotonic() + support.SHORT_TIMEOUT + while not fut.done() and time.monotonic() < deadline: + status = _overlapped.GetQueuedCompletionStatus(proactor._iocp, 100) + if status is not None: + proactor._process_completion_status(status) + + self.assertTrue(fut.done()) + self.assertEqual(fut.result(), b'data') + class ProactorPipeObjectSupportTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst b/Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst new file mode 100644 index 000000000000000..093a54619f38c17 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst @@ -0,0 +1,2 @@ +Split the completion status handling out of ``IocpProactor._poll()`` into a +separate ``IocpProactor._process_completion_status()`` method. From afd01e729a2126bf9a447275f2951574bebabb47 Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:04:40 +0300 Subject: [PATCH 2/2] Delete Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst --- .../next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst b/Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst deleted file mode 100644 index 093a54619f38c17..000000000000000 --- a/Misc/NEWS.d/next/Library/2026-08-03-22-31-37.gh-issue-154971.cQPtrD.rst +++ /dev/null @@ -1,2 +0,0 @@ -Split the completion status handling out of ``IocpProactor._poll()`` into a -separate ``IocpProactor._process_completion_status()`` method.