Skip to content

Commit 6e066a8

Browse files
gpsheaddaniel-shieldsblurb-it[bot]
authored andcommitted
[3.14] gh-101267: ProcessPoolExecutor no longer shares 1 BrokenProcessPool exception among all failed futures (GH-101268) (GH-151431)
* gh-101267: ProcessPoolExecutor no longer shares 1 BrokenProcessPool exception among all failed futures (GH-101268) (cherry picked from commit 3c00ebc) Co-authored-by: Daniel Shields <daniel.shields@twosigma.com> Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Gregory P. Smith <greg@krypto.org> (cherry picked from commit 4e8c9c6) * Drop the abrupt-exit-code reporting from the 3.14 backport Reporting the exit codes of processes that died without a known cause is a new feature, not part of the gh-101267 bugfix. Keep only the bugfix on 3.14: each failed future gets its own BrokenProcessPool exception instead of one shared instance. --------- (cherry picked from commit 27ff2c8) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Daniel Shields <daniel.shields@twosigma.com> Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
1 parent 52ee4d8 commit 6e066a8

4 files changed

Lines changed: 44 additions & 7 deletions

File tree

Lib/concurrent/futures/process.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -469,17 +469,20 @@ def _terminate_broken(self, cause):
469469
executor._shutdown_thread = True
470470
executor = None
471471

472-
# All pending tasks are to be marked failed with the following
473-
# BrokenProcessPool error
474-
bpe = BrokenProcessPool("A process in the process pool was "
475-
"terminated abruptly while the future was "
476-
"running or pending.")
472+
# All pending tasks are to be marked failed with a
473+
# BrokenProcessPool error, as separate instances to avoid sharing
474+
# a traceback (gh-101267).
475+
cause_tb = None
477476
if cause is not None:
478-
bpe.__cause__ = _RemoteTraceback(
479-
f"\n'''\n{''.join(cause)}'''")
477+
cause_tb = f"\n'''\n{''.join(cause)}'''"
480478

481479
# Mark pending tasks as failed.
482480
for work_id, work_item in self.pending_work_items.items():
481+
bpe = BrokenProcessPool("A process in the process pool was "
482+
"terminated abruptly while the future was "
483+
"running or pending.")
484+
if cause_tb is not None:
485+
bpe.__cause__ = _RemoteTraceback(cause_tb)
483486
try:
484487
work_item.future.set_exception(bpe)
485488
except _base.InvalidStateError:

Lib/test/test_concurrent_futures/test_process_pool.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33
import threading
44
import time
5+
import traceback
56
import unittest
67
from concurrent import futures
78
from concurrent.futures.process import BrokenProcessPool
@@ -43,6 +44,31 @@ def test_killed_child(self):
4344
# Submitting other jobs fails as well.
4445
self.assertRaises(BrokenProcessPool, self.executor.submit, pow, 2, 8)
4546

47+
def test_broken_process_pool_traceback(self):
48+
# When a child process is abruptly terminated, the whole pool gets
49+
# "broken", and a BrokenProcessPool exception should be created
50+
# for each future instead of sharing one exception among all futures.
51+
event = self.create_event()
52+
futures = [self.executor.submit(event.wait) for _ in range(3)]
53+
p = next(iter(self.executor._processes.values()))
54+
p.terminate()
55+
for fut in futures:
56+
# Don't use assertRaises(): it clears the traceback off exc.
57+
try:
58+
fut.result()
59+
except BrokenProcessPool as exc:
60+
tb = exc.__traceback__
61+
else:
62+
self.fail("BrokenProcessPool not raised")
63+
count = sum(
64+
1
65+
for frame_summary in traceback.extract_tb(tb)
66+
if frame_summary.filename == __file__
67+
)
68+
# This code file should appear exactly once in the traceback.
69+
# A shared exception would accumulate a frame per result() call.
70+
self.assertEqual(count, 1)
71+
4672
def test_map_chunksize(self):
4773
def bad_map():
4874
list(self.executor.map(pow, range(40), range(40), chunksize=-1))

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,7 @@ Charlie Shepherd
17111711
Bruce Sherwood
17121712
Gregory Shevchenko
17131713
Hai Shi
1714+
Daniel Shields
17141715
Alexander Shigin
17151716
Pete Shinners
17161717
Michael Shiplett
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
When a worker process terminates unexpectedly,
2+
:class:`concurrent.futures.ProcessPoolExecutor` now sets a separate
3+
:exc:`~concurrent.futures.process.BrokenProcessPool` exception on each pending
4+
future instead of sharing a single instance among them all. Sharing one
5+
exception produced malformed tracebacks: each
6+
:meth:`Future.result() <concurrent.futures.Future.result>` call re-raised the
7+
same object, appending another copy of the traceback to it.

0 commit comments

Comments
 (0)