From e9c07b538a2727ad07f3884a80b9d247d3a11354 Mon Sep 17 00:00:00 2001 From: J Berg Date: Wed, 1 Oct 2025 22:30:38 +0100 Subject: [PATCH 1/7] Report which process terminated as cause of BPE --- Lib/concurrent/futures/process.py | 9 +++++++++ .../test_process_pool.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index a14650bf5fa47c..4c927977a90989 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -477,6 +477,15 @@ def _terminate_broken(self, cause): if cause is not None: bpe.__cause__ = _RemoteTraceback( f"\n'''\n{''.join(cause)}'''") + else: + # No cause known, so try to report some helpful info about + # which process(es) terminated and with what exit code + errors = [] + for p in self.processes.values(): + if p.exitcode: # Report any nonzero exit codes + errors.append(f"Process {p.pid} terminated abruptly with exit code {p.exitcode}") + if errors: + bpe.__cause__ = _RemoteTraceback("\n".join(errors)) # Mark pending tasks as failed. for work_id, work_item in self.pending_work_items.items(): diff --git a/Lib/test/test_concurrent_futures/test_process_pool.py b/Lib/test/test_concurrent_futures/test_process_pool.py index 9685f980119a0e..08967301742ac6 100644 --- a/Lib/test/test_concurrent_futures/test_process_pool.py +++ b/Lib/test/test_concurrent_futures/test_process_pool.py @@ -1,3 +1,4 @@ +import ctypes import os import queue import sys @@ -106,6 +107,23 @@ def test_traceback(self): self.assertIn('raise RuntimeError(123) # some comment', f1.getvalue()) + @staticmethod + def _segfault(): + ctypes.string_at(0) + + def test_traceback_when_child_process_segfaults(self): + # gh-139462 enhancement - BrokenProcessPool exceptions + # should describe which process terminated. + future = self.executor.submit(self._segfault) + with self.assertRaises(Exception) as cm: + future.result() + + bpe = cm.exception + self.assertIs(type(bpe), BrokenProcessPool) + cause = bpe.__cause__ + self.assertIs(type(cause), futures.process._RemoteTraceback) + self.assertIn("terminated abruptly with exit code", cause.tb) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @hashlib_helper.requires_hashdigest('md5') def test_ressources_gced_in_workers(self): From 0ece4223e83df1635d3472193ec4d81f1ca66d03 Mon Sep 17 00:00:00 2001 From: J Berg Date: Thu, 2 Oct 2025 22:13:45 +0100 Subject: [PATCH 2/7] PR feedback --- Lib/concurrent/futures/process.py | 16 ++++++------ .../test_process_pool.py | 25 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index 4c927977a90989..1e71b5bd3131d5 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -474,18 +474,20 @@ def _terminate_broken(self, cause): bpe = BrokenProcessPool("A process in the process pool was " "terminated abruptly while the future was " "running or pending.") + cause_str = None if cause is not None: - bpe.__cause__ = _RemoteTraceback( - f"\n'''\n{''.join(cause)}'''") + cause_str = ''.join(cause) else: - # No cause known, so try to report some helpful info about - # which process(es) terminated and with what exit code + # No cause known, synthesize from child process exitcodes errors = [] for p in self.processes.values(): - if p.exitcode: # Report any nonzero exit codes - errors.append(f"Process {p.pid} terminated abruptly with exit code {p.exitcode}") + if p.exitcode is not None and p.exitcode != 0: + errors.append(f"Process {p.pid} terminated abruptly " + f"with exit code {p.exitcode}") if errors: - bpe.__cause__ = _RemoteTraceback("\n".join(errors)) + cause_str = "\n".join(errors) + if cause_str: + bpe.__cause__ = _RemoteTraceback(f"\n'''\n{cause_str}'''") # Mark pending tasks as failed. for work_id, work_item in self.pending_work_items.items(): diff --git a/Lib/test/test_concurrent_futures/test_process_pool.py b/Lib/test/test_concurrent_futures/test_process_pool.py index 08967301742ac6..25f43d8af9781f 100644 --- a/Lib/test/test_concurrent_futures/test_process_pool.py +++ b/Lib/test/test_concurrent_futures/test_process_pool.py @@ -1,4 +1,3 @@ -import ctypes import os import queue import sys @@ -108,21 +107,25 @@ def test_traceback(self): f1.getvalue()) @staticmethod - def _segfault(): - ctypes.string_at(0) + def _terminate_abruptly_with_exit_code(exit_code): + os._exit(exit_code) - def test_traceback_when_child_process_segfaults(self): + def test_traceback_when_child_process_terminates_abruptly(self): # gh-139462 enhancement - BrokenProcessPool exceptions # should describe which process terminated. - future = self.executor.submit(self._segfault) - with self.assertRaises(Exception) as cm: + exit_code = 99 + future = self.executor.submit( + self._terminate_abruptly_with_exit_code, + exit_code + ) + with self.assertRaises(BrokenProcessPool) as bpe: future.result() - bpe = cm.exception - self.assertIs(type(bpe), BrokenProcessPool) - cause = bpe.__cause__ - self.assertIs(type(cause), futures.process._RemoteTraceback) - self.assertIn("terminated abruptly with exit code", cause.tb) + cause = bpe.exception.__cause__ + self.assertIsInstance(cause, futures.process._RemoteTraceback) + self.assertIn( + f"terminated abruptly with exit code {exit_code}", cause.tb + ) @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @hashlib_helper.requires_hashdigest('md5') From c8fc9ded400b046b506195545896a4136d38d0c2 Mon Sep 17 00:00:00 2001 From: J Berg Date: Thu, 2 Oct 2025 22:30:47 +0100 Subject: [PATCH 3/7] Add a NEWS entry --- .../Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst b/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst new file mode 100644 index 00000000000000..4ac591f04e7730 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst @@ -0,0 +1,3 @@ +When a child process in a :class:`concurrent.futures.ProcessPoolExecutor` +terminates abruptly, the resulting traceback will now tell you the process +id and exit code of the terminated process. Contributed by Jonathan Berg. From f81efde1770abca49ca19b12a3d6cf7fb026df11 Mon Sep 17 00:00:00 2001 From: J Berg Date: Thu, 2 Oct 2025 22:39:32 +0100 Subject: [PATCH 4/7] Add to whats new in python --- Doc/whatsnew/3.15.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index b5e138aa674697..11c3eb24e56847 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -283,6 +283,15 @@ New modules Improved modules ================ +concurrent.futures +--- + +* Improved error reporting when a child process in a + :class:`concurrent.futures.ProcessPoolExecutor` terminates abruptly. + The resulting traceback will now tell you the process + id and exit code of the terminated process. + (Contributed by Jonathan Berg in :gh:`139486`.) + dbm --- From 206dfc470466008b68bd97b2d96392e7badd2a1b Mon Sep 17 00:00:00 2001 From: J Berg Date: Thu, 2 Oct 2025 22:49:13 +0100 Subject: [PATCH 5/7] add back hack to get merge conflict checker to pass --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d48cb3c7c37e53..edb8b721ff5f43 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -677,7 +677,7 @@ __version__ .. include:: ../deprecations/pending-removal-in-future.rst Removed -====== +======= ctypes ------ From d80ebf1ce8f9195353f2d11739b9e82dda382798 Mon Sep 17 00:00:00 2001 From: J Berg Date: Thu, 2 Oct 2025 22:56:34 +0100 Subject: [PATCH 6/7] PR comments for docs --- Doc/whatsnew/3.15.rst | 4 ++-- .../Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index edb8b721ff5f43..a8f62012e86e2c 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -326,8 +326,8 @@ concurrent.futures * Improved error reporting when a child process in a :class:`concurrent.futures.ProcessPoolExecutor` terminates abruptly. - The resulting traceback will now tell you the process - id and exit code of the terminated process. + The resulting traceback will now tell you the PID and exit code of the + terminated process. (Contributed by Jonathan Berg in :gh:`139486`.) dbm diff --git a/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst b/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst index 4ac591f04e7730..390a6124386151 100644 --- a/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst +++ b/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst @@ -1,3 +1,3 @@ When a child process in a :class:`concurrent.futures.ProcessPoolExecutor` -terminates abruptly, the resulting traceback will now tell you the process -id and exit code of the terminated process. Contributed by Jonathan Berg. +terminates abruptly, the resulting traceback will now tell you the PID +and exit code of the terminated process. Contributed by Jonathan Berg. From a5d3266423f691162bbda321ee7582581ec53ff0 Mon Sep 17 00:00:00 2001 From: J Berg Date: Fri, 3 Oct 2025 22:30:31 +0100 Subject: [PATCH 7/7] Simplify tests slightly and try fresh pool --- .../test_concurrent_futures/test_process_pool.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_concurrent_futures/test_process_pool.py b/Lib/test/test_concurrent_futures/test_process_pool.py index 25f43d8af9781f..731419a48bd128 100644 --- a/Lib/test/test_concurrent_futures/test_process_pool.py +++ b/Lib/test/test_concurrent_futures/test_process_pool.py @@ -106,20 +106,14 @@ def test_traceback(self): self.assertIn('raise RuntimeError(123) # some comment', f1.getvalue()) - @staticmethod - def _terminate_abruptly_with_exit_code(exit_code): - os._exit(exit_code) - def test_traceback_when_child_process_terminates_abruptly(self): # gh-139462 enhancement - BrokenProcessPool exceptions # should describe which process terminated. exit_code = 99 - future = self.executor.submit( - self._terminate_abruptly_with_exit_code, - exit_code - ) - with self.assertRaises(BrokenProcessPool) as bpe: - future.result() + with self.executor_type(max_workers=1) as executor: + future = executor.submit(os._exit, exit_code) + with self.assertRaises(BrokenProcessPool) as bpe: + future.result() cause = bpe.exception.__cause__ self.assertIsInstance(cause, futures.process._RemoteTraceback)