From 3006c106a6ac0b2ead3400e95627db3a28533ac0 Mon Sep 17 00:00:00 2001 From: ANSHUL SINGH <72524975+ekanshul@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:30:38 +0530 Subject: [PATCH 1/3] gh-156070: Use ``and`` instead of ``&`` in multiprocessing's batched wait ``i > res[0] & i not in res`` parses as ``i > (res[0] & i) not in res`` because ``&`` binds tighter than comparisons, so the Windows >60-handle path of ``_exhaustive_wait`` kept the wrong handles. Co-Authored-By: Claude Fable 5 --- Lib/multiprocessing/connection.py | 2 +- .../Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index be2fc3edf5dce66..ba7f5692785d9be 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -1062,7 +1062,7 @@ def _exhaustive_wait(handles, timeout): return [] ready.extend(L[i] for i in res) if res: - L = [h for i, h in enumerate(L) if i > res[0] & i not in res] + L = [h for i, h in enumerate(L) if i > res[0] and i not in res] timeout = 0 while L: short_L = L[:60] if len(L) > 60 else L diff --git a/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst b/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst new file mode 100644 index 000000000000000..10013a2e2e9ebde --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst @@ -0,0 +1,3 @@ +Fix :func:`multiprocessing.connection.wait` on Windows with more than 60 +handles: the batched wait used ``&`` where ``and`` was meant when removing +already signalled handles from the remaining list. From d30221ef402778d61eb73eb911965b80274b5976 Mon Sep 17 00:00:00 2001 From: ekanshul <72524975+ekanshul@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:40:54 +0530 Subject: [PATCH 2/3] Add a regression test and reword the NEWS entry Co-Authored-By: Claude Fable 5 --- Lib/test/_test_multiprocessing.py | 19 +++++++++++++++++++ ...-08-19-21-00-25.gh-issue-156070.fwmRPu.rst | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index e5f618f5f2e84f4..4d7190bb4f841e6 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -5918,6 +5918,25 @@ def test_neg_timeout(self): a.close() b.close() + @unittest.skipUnless(WIN32, "skipped on non-Windows platforms") + def test_exhaustive_wait_more_than_60_handles(self): + import _winapi + from multiprocessing.connection import _exhaustive_wait + + # More than 60 handles takes the batched path. + events = [_winapi.CreateEventW(0, True, False, None) + for _ in range(70)] + self.addCleanup(lambda: [_winapi.CloseHandle(e) for e in events]) + + # Spread the signalled events out so they do not all land in the + # first batch, and leave index 0 unsignalled. + chosen = [3, 17, 42, 68] + for i in chosen: + _winapi.SetEvent(events[i]) + + ready = _exhaustive_wait(events, 0) + self.assertEqual(sorted(ready), sorted(events[i] for i in chosen)) + # # Issue 14151: Test invalid family on invalid environment # diff --git a/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst b/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst index 10013a2e2e9ebde..e4e60a0674243f8 100644 --- a/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst +++ b/Misc/NEWS.d/next/Library/2026-08-19-21-00-25.gh-issue-156070.fwmRPu.rst @@ -1,3 +1,3 @@ -Fix :func:`multiprocessing.connection.wait` on Windows with more than 60 -handles: the batched wait used ``&`` where ``and`` was meant when removing -already signalled handles from the remaining list. +Fix :func:`multiprocessing.connection.wait` on Windows when waiting on more +than 60 objects: an object that was already ready could be reported more than +once, and other ready objects could be left out of the result. From 158146497f0fa324604006194472c4c09fc90a42 Mon Sep 17 00:00:00 2001 From: ekanshul <72524975+ekanshul@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:03:16 +0530 Subject: [PATCH 3/3] Fix the regression test's wait timeout and signalled indices Co-Authored-By: Claude Fable 5 --- Lib/test/_test_multiprocessing.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 4d7190bb4f841e6..da807b4733cc3cd 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -5923,18 +5923,23 @@ def test_exhaustive_wait_more_than_60_handles(self): import _winapi from multiprocessing.connection import _exhaustive_wait - # More than 60 handles takes the batched path. + # More than 60 handles takes the batched path. Manual reset events + # stay signalled, so the handles the batched wait already reported + # must be dropped from the list that is scanned afterwards. events = [_winapi.CreateEventW(0, True, False, None) for _ in range(70)] self.addCleanup(lambda: [_winapi.CloseHandle(e) for e in events]) - # Spread the signalled events out so they do not all land in the - # first batch, and leave index 0 unsignalled. - chosen = [3, 17, 42, 68] + # BatchedWaitForMultipleObjects reports the lowest signalled handle + # of each 63 handle batch, so signalling the first event pins the + # first reported index at 0 and spreads the rest over both batches. + chosen = [0, 17, 42, 68] for i in chosen: _winapi.SetEvent(events[i]) - ready = _exhaustive_wait(events, 0) + # A zero timeout is not usable here: the batched wait would report a + # timeout before its worker threads have run. + ready = _exhaustive_wait(events, 10_000) self.assertEqual(sorted(ready), sorted(events[i] for i in chosen)) #