From fc0bd1adf0767a10c9787b707e9981fcacdd029a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 14 Apr 2026 14:20:55 +0100 Subject: [PATCH 1/5] WIP possible fix to windows parallel issue --- mypy/ipc.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mypy/ipc.py b/mypy/ipc.py index 607a27668caf..feff6993ab03 100644 --- a/mypy/ipc.py +++ b/mypy/ipc.py @@ -431,6 +431,18 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis ready.append(i) else: ov.cancel() + # CancelIoEx is asynchronous -- wait for it to finalize so we + # can tell whether the read actually completed before the cancel + # took effect. If it did, the byte has been consumed from the + # pipe and must be preserved in the connection buffer. + if _winapi.WaitForSingleObject(ov.event, 1000) == _winapi.WAIT_OBJECT_0: + try: + ov.GetOverlappedResult(False) + except OSError: + continue + data = ov.getbuffer() + if data: + conns[i].buffer.extend(data) return ready From 2978ac0f952e17a25fffa050cd92792dd85cdccf Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 14 Apr 2026 16:20:54 +0100 Subject: [PATCH 2/5] Use Ivan's idea for simplification --- mypy/ipc.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/mypy/ipc.py b/mypy/ipc.py index feff6993ab03..595e85e85537 100644 --- a/mypy/ipc.py +++ b/mypy/ipc.py @@ -421,28 +421,26 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis ov.cancel() raise IPCException(f"Failed to wait for connections: {_winapi.GetLastError()}") - # Check which pending operations completed, cancel the rest + # Cancel all pending operations. CancelIoEx is asynchronous, so an + # operation may have completed before the cancel took effect. We then + # wait for all operations to finalize and check each result: completed + # reads get their data saved and are marked ready; cancelled ones are + # simply skipped. This avoids a race between checking if an operation + # is signaled and cancelling it. + for _, ov in pending: + ov.cancel() for i, ov in pending: - if _winapi.WaitForSingleObject(ov.event, 0) == _winapi.WAIT_OBJECT_0: - _, err = ov.GetOverlappedResult(True) - data = ov.getbuffer() - if data: - conns[i].buffer.extend(data) - ready.append(i) - else: - ov.cancel() - # CancelIoEx is asynchronous -- wait for it to finalize so we - # can tell whether the read actually completed before the cancel - # took effect. If it did, the byte has been consumed from the - # pipe and must be preserved in the connection buffer. - if _winapi.WaitForSingleObject(ov.event, 1000) == _winapi.WAIT_OBJECT_0: - try: - ov.GetOverlappedResult(False) - except OSError: - continue - data = ov.getbuffer() - if data: - conns[i].buffer.extend(data) + # Wait for the cancel (or completed read) to finalize. + _winapi.WaitForSingleObject(ov.event, 1000) + try: + ov.GetOverlappedResult(False) + except OSError: + # Operation was successfully cancelled -- no data consumed. + continue + data = ov.getbuffer() + if data: + conns[i].buffer.extend(data) + ready.append(i) return ready From beece9420961b0bdce3f73155b30f10fd6d9ee7d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 14 Apr 2026 16:29:23 +0100 Subject: [PATCH 3/5] Improve error handling --- mypy/ipc.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/mypy/ipc.py b/mypy/ipc.py index 595e85e85537..a18537bfa4b9 100644 --- a/mypy/ipc.py +++ b/mypy/ipc.py @@ -433,13 +433,25 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis # Wait for the cancel (or completed read) to finalize. _winapi.WaitForSingleObject(ov.event, 1000) try: - ov.GetOverlappedResult(False) - except OSError: + _, err = ov.GetOverlappedResult(False) + except OSError as e: + err = e.winerror + # Cancellation is expected here; broken/disconnected pipes should be + # surfaced as readable so the follow-up receive observes EOF/closure. + if err not in ( + _winapi.ERROR_OPERATION_ABORTED, + _winapi.ERROR_BROKEN_PIPE, + _winapi.ERROR_NETNAME_DELETED, + ): + # Anything else is a real IPC failure, not part of the probe race. + raise + if err == _winapi.ERROR_OPERATION_ABORTED: # Operation was successfully cancelled -- no data consumed. continue - data = ov.getbuffer() - if data: - conns[i].buffer.extend(data) + if err == 0: + data = ov.getbuffer() + if data: + conns[i].buffer.extend(data) ready.append(i) return ready From b2e9b72fb04fac635c46fdeae536ae8538d5e066 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 14 Apr 2026 16:35:47 +0100 Subject: [PATCH 4/5] Handle ERROR_MORE_DATA --- mypy/ipc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/ipc.py b/mypy/ipc.py index a18537bfa4b9..71716761891f 100644 --- a/mypy/ipc.py +++ b/mypy/ipc.py @@ -448,7 +448,7 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis if err == _winapi.ERROR_OPERATION_ABORTED: # Operation was successfully cancelled -- no data consumed. continue - if err == 0: + if err in (0, _winapi.ERROR_MORE_DATA): data = ov.getbuffer() if data: conns[i].buffer.extend(data) From a26c4ec4f0f8d2d531bcd125db68c0615b22779b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 14 Apr 2026 16:39:05 +0100 Subject: [PATCH 5/5] Remove timeout --- mypy/ipc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/ipc.py b/mypy/ipc.py index 71716761891f..08ca0caf75f1 100644 --- a/mypy/ipc.py +++ b/mypy/ipc.py @@ -430,10 +430,8 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis for _, ov in pending: ov.cancel() for i, ov in pending: - # Wait for the cancel (or completed read) to finalize. - _winapi.WaitForSingleObject(ov.event, 1000) try: - _, err = ov.GetOverlappedResult(False) + _, err = ov.GetOverlappedResult(True) except OSError as e: err = e.winerror # Cancellation is expected here; broken/disconnected pipes should be