]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-101267: ProcessPoolExecutor no longer shares 1 BrokenProcessPool exception among...
authorDaniel Shields <daniel.shields@live.com>
Sat, 13 Jun 2026 08:23:39 +0000 (03:23 -0500)
committerGitHub <noreply@github.com>
Sat, 13 Jun 2026 08:23:39 +0000 (01:23 -0700)
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Gregory P. Smith <greg@krypto.org>
Lib/concurrent/futures/process.py
Lib/test/test_concurrent_futures/test_process_pool.py
Misc/ACKS
Misc/NEWS.d/next/Library/2023-01-23-21-23-50.gh-issue-101267._f-cFH.rst [new file with mode: 0644]

index ed24cc250434130c842b847de0997c15a102bad0..10d4ac89d7257138742b5bd71d79f4d9e23283bb 100644 (file)
@@ -469,11 +469,9 @@ class _ExecutorManagerThread(threading.Thread):
             executor._shutdown_thread = True
             executor = None
 
-        # All pending tasks are to be marked failed with the following
-        # BrokenProcessPool error
-        bpe = BrokenProcessPool("A process in the process pool was "
-                                "terminated abruptly while the future was "
-                                "running or pending.")
+        # All pending tasks are to be marked failed with a
+        # BrokenProcessPool error, as separate instances to avoid sharing
+        # a traceback (gh-101267).
         cause_str = None
         if cause is not None:
             cause_str = ''.join(cause)
@@ -489,11 +487,15 @@ class _ExecutorManagerThread(threading.Thread):
                                   f"with exit code {p.exitcode}")
             if errors:
                 cause_str = "\n".join(errors)
-        if cause_str:
-            bpe.__cause__ = _RemoteTraceback(f"\n'''\n{cause_str}'''")
+        cause_tb = f"\n'''\n{cause_str}'''" if cause_str else None
 
         # Mark pending tasks as failed.
         for work_id, work_item in self.pending_work_items.items():
+            bpe = BrokenProcessPool("A process in the process pool was "
+                                    "terminated abruptly while the future was "
+                                    "running or pending.")
+            if cause_tb is not None:
+                bpe.__cause__ = _RemoteTraceback(cause_tb)
             try:
                 work_item.future.set_exception(bpe)
             except _base.InvalidStateError:
index da70d910dc356143c7b1feb6de25cb477f6a1f1c..45cfd46fb7befbf88f49f08b998a63b116701f45 100644 (file)
@@ -3,6 +3,7 @@ import queue
 import sys
 import threading
 import time
+import traceback
 import unittest
 import unittest.mock
 from concurrent import futures
@@ -62,6 +63,32 @@ class ProcessPoolExecutorTest(ExecutorTest):
         # Submitting other jobs fails as well.
         self.assertRaises(BrokenProcessPool, self.executor.submit, pow, 2, 8)
 
+    @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+    def test_broken_process_pool_traceback(self):
+        # When a child process is abruptly terminated, the whole pool gets
+        # "broken", and a BrokenProcessPool exception should be created
+        # for each future instead of sharing one exception among all futures.
+        event = self.create_event()
+        futures = [self.executor.submit(event.wait) for _ in range(3)]
+        p = next(iter(self.executor._processes.values()))
+        p.terminate()
+        for fut in futures:
+            # Don't use assertRaises(): it clears the traceback off exc.
+            try:
+                fut.result()
+            except BrokenProcessPool as exc:
+                tb = exc.__traceback__
+            else:
+                self.fail("BrokenProcessPool not raised")
+            count = sum(
+                1
+                for frame_summary in traceback.extract_tb(tb)
+                if frame_summary.filename == __file__
+            )
+            # This code file should appear exactly once in the traceback.
+            # A shared exception would accumulate a frame per result() call.
+            self.assertEqual(count, 1)
+
     @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
     def test_map_chunksize(self):
         def bad_map():
index 71466e0804ae3c1c17eab8b6a791b6b16c1ad822..ee68d91f13c431f7ed69360625db1c1b4ecd008f 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1763,6 +1763,7 @@ Charlie Shepherd
 Bruce Sherwood
 Gregory Shevchenko
 Hai Shi
+Daniel Shields
 Alexander Shigin
 Pete Shinners
 Michael Shiplett
diff --git a/Misc/NEWS.d/next/Library/2023-01-23-21-23-50.gh-issue-101267._f-cFH.rst b/Misc/NEWS.d/next/Library/2023-01-23-21-23-50.gh-issue-101267._f-cFH.rst
new file mode 100644 (file)
index 0000000..901a3fb
--- /dev/null
@@ -0,0 +1,7 @@
+When a worker process terminates unexpectedly,
+:class:`concurrent.futures.ProcessPoolExecutor` now sets a separate
+:exc:`~concurrent.futures.process.BrokenProcessPool` exception on each pending
+future instead of sharing a single instance among them all.  Sharing one
+exception produced malformed tracebacks: each
+:meth:`Future.result() <concurrent.futures.Future.result>` call re-raised the
+same object, appending another copy of the traceback to it.