mirror of
https://github.com/python/cpython.git
synced 2025-09-26 10:19:53 +00:00
gh-109047: concurrent.futures catches PythonFinalizationError (#109810)
concurrent.futures: The *executor manager thread* now catches
exceptions when adding an item to the *call queue*. During Python
finalization, creating a new thread can now raise RuntimeError. Catch
the exception and call terminate_broken() in this case.
Add test_python_finalization_error() to test_concurrent_futures.
concurrent.futures._ExecutorManagerThread changes:
* terminate_broken() no longer calls shutdown_workers() since the
call queue is no longer working anymore (read and write ends of
the queue pipe are closed).
* terminate_broken() now terminates child processes, not only
wait until they complete.
* _ExecutorManagerThread.terminate_broken() now holds shutdown_lock
to prevent race conditons with ProcessPoolExecutor.submit().
multiprocessing.Queue changes:
* Add _terminate_broken() method.
* _start_thread() sets _thread to None on exception to prevent
leaking "dangling threads" even if the thread was not started
yet.
(cherry picked from commit 6351842121
)
This commit is contained in:
parent
41eb0c7286
commit
356de021d7
4 changed files with 90 additions and 17 deletions
|
@ -341,7 +341,14 @@ class _ExecutorManagerThread(threading.Thread):
|
||||||
# Main loop for the executor manager thread.
|
# Main loop for the executor manager thread.
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
self.add_call_item_to_queue()
|
# gh-109047: During Python finalization, self.call_queue.put()
|
||||||
|
# creation of a thread can fail with RuntimeError.
|
||||||
|
try:
|
||||||
|
self.add_call_item_to_queue()
|
||||||
|
except BaseException as exc:
|
||||||
|
cause = format_exception(exc)
|
||||||
|
self.terminate_broken(cause)
|
||||||
|
return
|
||||||
|
|
||||||
result_item, is_broken, cause = self.wait_result_broken_or_wakeup()
|
result_item, is_broken, cause = self.wait_result_broken_or_wakeup()
|
||||||
|
|
||||||
|
@ -425,8 +432,8 @@ class _ExecutorManagerThread(threading.Thread):
|
||||||
try:
|
try:
|
||||||
result_item = result_reader.recv()
|
result_item = result_reader.recv()
|
||||||
is_broken = False
|
is_broken = False
|
||||||
except BaseException as e:
|
except BaseException as exc:
|
||||||
cause = format_exception(type(e), e, e.__traceback__)
|
cause = format_exception(exc)
|
||||||
|
|
||||||
elif wakeup_reader in ready:
|
elif wakeup_reader in ready:
|
||||||
is_broken = False
|
is_broken = False
|
||||||
|
@ -473,7 +480,7 @@ class _ExecutorManagerThread(threading.Thread):
|
||||||
return (_global_shutdown or executor is None
|
return (_global_shutdown or executor is None
|
||||||
or executor._shutdown_thread)
|
or executor._shutdown_thread)
|
||||||
|
|
||||||
def terminate_broken(self, cause):
|
def _terminate_broken(self, cause):
|
||||||
# Terminate the executor because it is in a broken state. The cause
|
# Terminate the executor because it is in a broken state. The cause
|
||||||
# argument can be used to display more information on the error that
|
# argument can be used to display more information on the error that
|
||||||
# lead the executor into becoming broken.
|
# lead the executor into becoming broken.
|
||||||
|
@ -498,7 +505,14 @@ class _ExecutorManagerThread(threading.Thread):
|
||||||
|
|
||||||
# Mark pending tasks as failed.
|
# Mark pending tasks as failed.
|
||||||
for work_id, work_item in self.pending_work_items.items():
|
for work_id, work_item in self.pending_work_items.items():
|
||||||
work_item.future.set_exception(bpe)
|
try:
|
||||||
|
work_item.future.set_exception(bpe)
|
||||||
|
except _base.InvalidStateError:
|
||||||
|
# set_exception() fails if the future is cancelled: ignore it.
|
||||||
|
# Trying to check if the future is cancelled before calling
|
||||||
|
# set_exception() would leave a race condition if the future is
|
||||||
|
# cancelled between the check and set_exception().
|
||||||
|
pass
|
||||||
# Delete references to object. See issue16284
|
# Delete references to object. See issue16284
|
||||||
del work_item
|
del work_item
|
||||||
self.pending_work_items.clear()
|
self.pending_work_items.clear()
|
||||||
|
@ -508,16 +522,18 @@ class _ExecutorManagerThread(threading.Thread):
|
||||||
for p in self.processes.values():
|
for p in self.processes.values():
|
||||||
p.terminate()
|
p.terminate()
|
||||||
|
|
||||||
# Prevent queue writing to a pipe which is no longer read.
|
self.call_queue._terminate_broken()
|
||||||
# https://github.com/python/cpython/issues/94777
|
|
||||||
self.call_queue._reader.close()
|
|
||||||
|
|
||||||
# gh-107219: Close the connection writer which can unblock
|
# gh-107219: Close the connection writer which can unblock
|
||||||
# Queue._feed() if it was stuck in send_bytes().
|
# Queue._feed() if it was stuck in send_bytes().
|
||||||
self.call_queue._writer.close()
|
self.call_queue._writer.close()
|
||||||
|
|
||||||
# clean up resources
|
# clean up resources
|
||||||
self.join_executor_internals()
|
self._join_executor_internals(broken=True)
|
||||||
|
|
||||||
|
def terminate_broken(self, cause):
|
||||||
|
with self.shutdown_lock:
|
||||||
|
self._terminate_broken(cause)
|
||||||
|
|
||||||
def flag_executor_shutting_down(self):
|
def flag_executor_shutting_down(self):
|
||||||
# Flag the executor as shutting down and cancel remaining tasks if
|
# Flag the executor as shutting down and cancel remaining tasks if
|
||||||
|
@ -560,15 +576,24 @@ class _ExecutorManagerThread(threading.Thread):
|
||||||
break
|
break
|
||||||
|
|
||||||
def join_executor_internals(self):
|
def join_executor_internals(self):
|
||||||
self.shutdown_workers()
|
with self.shutdown_lock:
|
||||||
|
self._join_executor_internals()
|
||||||
|
|
||||||
|
def _join_executor_internals(self, broken=False):
|
||||||
|
# If broken, call_queue was closed and so can no longer be used.
|
||||||
|
if not broken:
|
||||||
|
self.shutdown_workers()
|
||||||
|
|
||||||
# Release the queue's resources as soon as possible.
|
# Release the queue's resources as soon as possible.
|
||||||
self.call_queue.close()
|
self.call_queue.close()
|
||||||
self.call_queue.join_thread()
|
self.call_queue.join_thread()
|
||||||
with self.shutdown_lock:
|
self.thread_wakeup.close()
|
||||||
self.thread_wakeup.close()
|
|
||||||
# If .join() is not called on the created processes then
|
# If .join() is not called on the created processes then
|
||||||
# some ctx.Queue methods may deadlock on Mac OS X.
|
# some ctx.Queue methods may deadlock on Mac OS X.
|
||||||
for p in self.processes.values():
|
for p in self.processes.values():
|
||||||
|
if broken:
|
||||||
|
p.terminate()
|
||||||
p.join()
|
p.join()
|
||||||
|
|
||||||
def get_n_children_alive(self):
|
def get_n_children_alive(self):
|
||||||
|
|
|
@ -158,6 +158,15 @@ class Queue(object):
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _terminate_broken(self):
|
||||||
|
# Close a Queue on error.
|
||||||
|
|
||||||
|
# gh-94777: Prevent queue writing to a pipe which is no longer read.
|
||||||
|
self._reader.close()
|
||||||
|
|
||||||
|
self.close()
|
||||||
|
self.join_thread()
|
||||||
|
|
||||||
def _start_thread(self):
|
def _start_thread(self):
|
||||||
debug('Queue._start_thread()')
|
debug('Queue._start_thread()')
|
||||||
|
|
||||||
|
@ -169,13 +178,19 @@ class Queue(object):
|
||||||
self._wlock, self._reader.close, self._writer.close,
|
self._wlock, self._reader.close, self._writer.close,
|
||||||
self._ignore_epipe, self._on_queue_feeder_error,
|
self._ignore_epipe, self._on_queue_feeder_error,
|
||||||
self._sem),
|
self._sem),
|
||||||
name='QueueFeederThread'
|
name='QueueFeederThread',
|
||||||
|
daemon=True,
|
||||||
)
|
)
|
||||||
self._thread.daemon = True
|
|
||||||
|
|
||||||
debug('doing self._thread.start()')
|
try:
|
||||||
self._thread.start()
|
debug('doing self._thread.start()')
|
||||||
debug('... done self._thread.start()')
|
self._thread.start()
|
||||||
|
debug('... done self._thread.start()')
|
||||||
|
except:
|
||||||
|
# gh-109047: During Python finalization, creating a thread
|
||||||
|
# can fail with RuntimeError.
|
||||||
|
self._thread = None
|
||||||
|
raise
|
||||||
|
|
||||||
if not self._joincancelled:
|
if not self._joincancelled:
|
||||||
self._jointhread = Finalize(
|
self._jointhread = Finalize(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
|
@ -187,6 +188,34 @@ class ProcessPoolExecutorTest(ExecutorTest):
|
||||||
for i, future in enumerate(futures):
|
for i, future in enumerate(futures):
|
||||||
self.assertEqual(future.result(), mul(i, i))
|
self.assertEqual(future.result(), mul(i, i))
|
||||||
|
|
||||||
|
def test_python_finalization_error(self):
|
||||||
|
# gh-109047: Catch RuntimeError on thread creation
|
||||||
|
# during Python finalization.
|
||||||
|
|
||||||
|
context = self.get_context()
|
||||||
|
|
||||||
|
# gh-109047: Mock the threading.start_new_thread() function to inject
|
||||||
|
# RuntimeError: simulate the error raised during Python finalization.
|
||||||
|
# Block the second creation: create _ExecutorManagerThread, but block
|
||||||
|
# QueueFeederThread.
|
||||||
|
orig_start_new_thread = threading._start_new_thread
|
||||||
|
nthread = 0
|
||||||
|
def mock_start_new_thread(func, *args):
|
||||||
|
nonlocal nthread
|
||||||
|
if nthread >= 1:
|
||||||
|
raise RuntimeError("can't create new thread at "
|
||||||
|
"interpreter shutdown")
|
||||||
|
nthread += 1
|
||||||
|
return orig_start_new_thread(func, *args)
|
||||||
|
|
||||||
|
with support.swap_attr(threading, '_start_new_thread',
|
||||||
|
mock_start_new_thread):
|
||||||
|
executor = self.executor_type(max_workers=2, mp_context=context)
|
||||||
|
with executor:
|
||||||
|
with self.assertRaises(BrokenProcessPool):
|
||||||
|
list(executor.map(mul, [(2, 3)] * 10))
|
||||||
|
executor.shutdown()
|
||||||
|
|
||||||
|
|
||||||
create_executor_tests(globals(), ProcessPoolExecutorTest,
|
create_executor_tests(globals(), ProcessPoolExecutorTest,
|
||||||
executor_mixins=(ProcessPoolForkMixin,
|
executor_mixins=(ProcessPoolForkMixin,
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
:mod:`concurrent.futures`: The *executor manager thread* now catches exceptions
|
||||||
|
when adding an item to the *call queue*. During Python finalization, creating a
|
||||||
|
new thread can now raise :exc:`RuntimeError`. Catch the exception and call
|
||||||
|
``terminate_broken()`` in this case. Patch by Victor Stinner.
|
Loading…
Add table
Add a link
Reference in a new issue