From 7f12e9cd81783900ecfb47bb9755236cf4a5a375 Mon Sep 17 00:00:00 2001 From: "Ziegler, Tim" Date: Thu, 1 Oct 2026 13:30:51 +0200 Subject: [PATCH 1/3] Fix ProcessPoolExecutor shutdown deadlock when result pipe is full --- Lib/concurrent/futures/process.py | 8 ++++ .../test_concurrent_futures/test_deadlock.py | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index 7f4f225c0ad4fbd..df92b7ea1d28edb 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -678,9 +678,17 @@ def _join_executor_internals(self, broken=False): # If .join() is not called on the created processes then # some ctx.Queue methods may deadlock on Mac OS X. + result_reader = self.result_queue._reader for p in self.processes.values(): if broken: p.terminate() + else: + # Exiting workers put their pid on the result queue. Keep + # draining it, otherwise once the pipe is full a worker + # blocks forever writing to it and never exits. + while p.sentinel not in mp.connection.wait( + [result_reader, p.sentinel]): + result_reader.recv_bytes() p.join() def get_n_children_alive(self): diff --git a/Lib/test/test_concurrent_futures/test_deadlock.py b/Lib/test/test_concurrent_futures/test_deadlock.py index 5cd84f7e43043c6..68371fa0c250f57 100644 --- a/Lib/test/test_concurrent_futures/test_deadlock.py +++ b/Lib/test/test_concurrent_futures/test_deadlock.py @@ -2,6 +2,7 @@ import queue import signal import sys +import threading import time import unittest import unittest.mock @@ -329,6 +330,44 @@ def clear(self): signal.signal(signal.SIGALRM, old_handler) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_shutdown_should_not_deadlock_if_result_pipe_full(self): + # Exiting workers put their pid on the result queue while shutdown() + # joins them. If nothing drains the queue, a full pipe blocks the + # workers forever. Fill the pipe once the executor manager thread + # stops reading results, right before the workers are told to exit. + self.executor.shutdown(wait=True) + + data = b"a" * support.PIPE_MAX_SIZE + fillers = [] + shutdown_workers = futures.process._ExecutorManagerThread.shutdown_workers + def mock_shutdown_workers(self): + # The put() blocks holding the result queue's write lock until + # the pipe is drained, so every exiting worker blocks behind it. + filler = threading.Thread(target=self.result_queue.put, + args=(data,)) + filler.start() + fillers.append(filler) + shutdown_workers(self) + + executor = self.executor_type(max_workers=2, + mp_context=self.get_context()) + self.executor = executor # Allow clean up in fail_on_deadlock + with unittest.mock.patch.object(futures.process._ExecutorManagerThread, + 'shutdown_workers', + mock_shutdown_workers): + self.assertEqual(list(executor.map(int, range(10))), + list(range(10))) + shutdown = threading.Thread(target=executor.shutdown) + shutdown.start() + shutdown.join(self.TIMEOUT) + if shutdown.is_alive(): + self._fail_on_deadlock(executor) + + self.assertEqual(len(fillers), 1) + fillers[0].join() + + create_executor_tests(globals(), ExecutorDeadlockTest, executor_mixins=(ProcessPoolForkMixin, ProcessPoolForkserverMixin, From 6c0483c7ac93606b87421ec58b8a86f283a79d4b Mon Sep 17 00:00:00 2001 From: "Ziegler, Tim" Date: Thu, 1 Oct 2026 18:46:53 +0200 Subject: [PATCH 2/3] Add NEWS entry --- .../Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst b/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst new file mode 100644 index 000000000000000..145bafda8f4c709 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst @@ -0,0 +1,3 @@ +Fix a deadlock in :meth:`concurrent.futures.ProcessPoolExecutor.shutdown` +when the pipe used to return results fills up with worker exit +notifications. From 4ec18e81c2d6d6607e00bc261c22b7d284e81366 Mon Sep 17 00:00:00 2001 From: "Ziegler, Tim" Date: Fri, 2 Oct 2026 08:53:07 +0200 Subject: [PATCH 3/3] Fix NEWS entry reference --- .../Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst b/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst index 145bafda8f4c709..68cb1602b73c8e6 100644 --- a/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst +++ b/Misc/NEWS.d/next/Library/2026-10-01-18-46-33.gh-issue-158570.rixXY7.rst @@ -1,3 +1,4 @@ -Fix a deadlock in :meth:`concurrent.futures.ProcessPoolExecutor.shutdown` -when the pipe used to return results fills up with worker exit -notifications. +Fix a deadlock in :meth:`ProcessPoolExecutor.shutdown() +` when the messages that exiting +workers send back fill up the pipe used to return results. This could +happen with many workers or a small pipe buffer.