-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-107219: Fix concurrent.futures terminate_broken() #109244
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ | |||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
__all__ = [ 'Client', 'Listener', 'Pipe', 'wait' ] | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import errno | ||||||||||||||||||||||||||||||||
import io | ||||||||||||||||||||||||||||||||
import os | ||||||||||||||||||||||||||||||||
import sys | ||||||||||||||||||||||||||||||||
|
@@ -41,6 +42,7 @@ | |||||||||||||||||||||||||||||||
BUFSIZE = 8192 | ||||||||||||||||||||||||||||||||
# A very generous timeout when it comes to local connections... | ||||||||||||||||||||||||||||||||
CONNECTION_TIMEOUT = 20. | ||||||||||||||||||||||||||||||||
WSA_OPERATION_ABORTED = 995 | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
_mmap_counter = itertools.count() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
@@ -271,12 +273,22 @@ class PipeConnection(_ConnectionBase): | |||||||||||||||||||||||||||||||
with FILE_FLAG_OVERLAPPED. | ||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||
_got_empty_message = False | ||||||||||||||||||||||||||||||||
_send_ov = None | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def _close(self, _CloseHandle=_winapi.CloseHandle): | ||||||||||||||||||||||||||||||||
ov = self._send_ov | ||||||||||||||||||||||||||||||||
if ov is not None: | ||||||||||||||||||||||||||||||||
# Interrupt WaitForMultipleObjects() in _send_bytes() | ||||||||||||||||||||||||||||||||
ov.cancel() | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. asyncio uses a similar code in ProactorEventLoop: cpython/Lib/asyncio/windows_events.py Lines 67 to 81 in 1ec4537
asyncio uses more advanced code around to handle more cases. For example, in asyncio, the cancel() API is part of the public API. Here the cancellation is a standard action in the Windows Overlapped API. The cancellation is synchronous, it's easy! Hopefully, we are not in the very complicated |
||||||||||||||||||||||||||||||||
_CloseHandle(self._handle) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def _send_bytes(self, buf): | ||||||||||||||||||||||||||||||||
if self._send_ov is not None: | ||||||||||||||||||||||||||||||||
# A connection should only be used by a single thread | ||||||||||||||||||||||||||||||||
raise ValueError("concurrent send_bytes() calls " | ||||||||||||||||||||||||||||||||
"are not supported") | ||||||||||||||||||||||||||||||||
ov, err = _winapi.WriteFile(self._handle, buf, overlapped=True) | ||||||||||||||||||||||||||||||||
self._send_ov = ov | ||||||||||||||||||||||||||||||||
try: | ||||||||||||||||||||||||||||||||
if err == _winapi.ERROR_IO_PENDING: | ||||||||||||||||||||||||||||||||
waitres = _winapi.WaitForMultipleObjects( | ||||||||||||||||||||||||||||||||
|
@@ -286,7 +298,13 @@ def _send_bytes(self, buf): | |||||||||||||||||||||||||||||||
ov.cancel() | ||||||||||||||||||||||||||||||||
raise | ||||||||||||||||||||||||||||||||
finally: | ||||||||||||||||||||||||||||||||
self._send_ov = None | ||||||||||||||||||||||||||||||||
nwritten, err = ov.GetOverlappedResult(True) | ||||||||||||||||||||||||||||||||
if err == WSA_OPERATION_ABORTED: | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What other value can it be? There is Could we simply check that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose to write a minimalist change: change at least code as possible. I introduce one new error, I added a check for this error, and that's all. I don't know the code enough to answer to your question. I'm not a multiprocessing or Windows API expert at all :-( |
||||||||||||||||||||||||||||||||
# close() was called by another thread while | ||||||||||||||||||||||||||||||||
# WaitForMultipleObjects() was waiting for the overlapped | ||||||||||||||||||||||||||||||||
# operation. | ||||||||||||||||||||||||||||||||
raise OSError(errno.EPIPE, "handle is closed") | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose to raise a cpython/Lib/multiprocessing/queues.py Lines 255 to 257 in 1ec4537
And concurrent.futures uses this code path for its "call queue" which is causing troubles here: cpython/Lib/concurrent/futures/process.py Lines 724 to 732 in 1ec4537
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds like we got lucky that callers were handling one thing we could raise! :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the beginning, I started by adding a new exception. But I chose to reuse the existing code instead. IMO BrokenPipeError perfectly makes sense for a PipeConnection. |
||||||||||||||||||||||||||||||||
assert err == 0 | ||||||||||||||||||||||||||||||||
assert nwritten == len(buf) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
Fix a race condition in ``concurrent.futures``. When a process in the | ||
process pool was terminated abruptly (while the future was running or | ||
pending), close the connection write end. If the call queue is blocked on | ||
sending bytes to a worker process, closing the connection write end interrupts | ||
the send, so the queue can be closed. Patch by Victor Stinner. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the same as
_winapi.ERROR_OPERATION_ABORTED
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I'm confused. I don't recall which doc I was looking to. WriteFile() is documented to return
ERROR_OPERATION_ABORTED
when it's canceled: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile