You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ProcessPoolExecutor executor manager thread crashes if task fails in some circumstances #119592
If you run this, then the following is printed to the console:
Exception in thread Thread-1:
Traceback (most recent call last):
File "python3.11/threading.py", line 1045, in _bootstrap_inner
self.run()
File "python3.11/concurrent/futures/process.py", line 364, in run
executor._adjust_process_count()
File "python3.11/concurrent/futures/process.py", line 759, in _adjust_process_count
process_count = len(self._processes)
^^^^^^^^^^^^^^^^^^^^
TypeError: object of type 'NoneType' has no len()
If you change the code to using wait=True, or remove the max_tasks_per_child=1 option then the message is not printed.
gh-115634: Fix ProcessPoolExecutor deadlock with max_tasks_per_child #140900 moved worker replacement into _replace_dead_worker(), which returns before touching self._processes when the executor is shutting down with no pending work -- exactly the state this reproducer creates. _shutdown_thread is read under _shutdown_lock, which shutdown() holds while setting it, so that check is not itself racy.
But... the underlying race is narrowed rather than closed so I'm keeping this open:
shutdown() sets _shutdown_thread under _shutdown_lock, but the reference-clearing block afterwards (self._processes = None and friends) runs outside the lock. With wait=False the manager thread is still alive, so a worker exit racing that block can still, in principle, observe self._processes as a dict at the None-check and as None at a later use.
In _replace_dead_worker() when pending work remains, and in the submit()-path _adjust_process_count()/_spawn_process() pair, which predates all of this. An unhandled TypeError there kills the manager thread and strands any pending futures.
The likely ideal fix is lock discipline in shutdown(): move the clearing of manager-thread-visible state (at minimum self._processes) under _shutdown_lock, or stop clearing self._processes entirely -- unlike the queues it holds no file descriptors, and the fd-reduction comment that motivates the block does not apply to it.
(Claude Fable 5 drafted most of that - i'd word it a bit differently myself but it's appropriate input to actually fix the remaining bits... -gpshead)
I would like to backport the merged into 3.15 PR further, but lets let this bake in a couple of 3.15 beta releases first as max_tasks_per_child= feature working usably enough at all was only just fixed in the first place. #152967 more specifically tracks that.
Activity
gpshead commentedon Jul 3, 2026
Status update after the related gh-115634 / GH-140900: The reproducer above no longer triggers on main.
Two changes account for that:
ProcessPoolExecutorraises exception or hangs the process whenshutdown(wait=False)and worker throws exception #132969 (gh-132969: Fix error/hang when shutdown(wait=False) and task exited abnormally #133222) added an early return to the spawn path when self._processes is None, which covers the common orderings of this race.But... the underlying race is narrowed rather than closed so I'm keeping this open:
shutdown() sets _shutdown_thread under _shutdown_lock, but the reference-clearing block afterwards (self._processes = None and friends) runs outside the lock. With wait=False the manager thread is still alive, so a worker exit racing that block can still, in principle, observe self._processes as a dict at the None-check and as None at a later use.
In _replace_dead_worker() when pending work remains, and in the submit()-path _adjust_process_count()/_spawn_process() pair, which predates all of this. An unhandled TypeError there kills the manager thread and strands any pending futures.
The likely ideal fix is lock discipline in shutdown(): move the clearing of manager-thread-visible state (at minimum self._processes) under _shutdown_lock, or stop clearing self._processes entirely -- unlike the queues it holds no file descriptors, and the fd-reduction comment that motivates the block does not apply to it.
(Claude Fable 5 drafted most of that - i'd word it a bit differently myself but it's appropriate input to actually fix the remaining bits... -gpshead)
gh-119592: gh-152967: Fix ProcessPoolExecutor stranding submitted wor…
[3.15] gh-119592: gh-152967: Fix ProcessPoolExecutor stranding submit…
gpshead commentedon Jul 8, 2026
I would like to backport the merged into 3.15 PR further, but lets let this bake in a couple of 3.15 beta releases first as max_tasks_per_child= feature working usably enough at all was only just fixed in the first place. #152967 more specifically tracks that.
[3.14] gh-119592: gh-152967: Fix ProcessPoolExecutor stranding submit…