Skip to content

Commit 9e7340c

Browse files
authored
gh-139462: Make the ProcessPoolExecutor BrokenProcessPool exception report which child process terminated (GH-139486)
Report which process terminated as cause of BPE
1 parent 7906f4d commit 9e7340c

File tree

4 files changed

+44
-2
lines changed

4 files changed

+44
-2
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,16 @@ collections.abc
369369
:mod:`!collections.abc` module.
370370

371371

372+
concurrent.futures
373+
------------------
374+
375+
* Improved error reporting when a child process in a
376+
:class:`concurrent.futures.ProcessPoolExecutor` terminates abruptly.
377+
The resulting traceback will now tell you the PID and exit code of the
378+
terminated process.
379+
(Contributed by Jonathan Berg in :gh:`139486`.)
380+
381+
372382
dataclasses
373383
-----------
374384

Lib/concurrent/futures/process.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,23 @@ def _terminate_broken(self, cause):
474474
bpe = BrokenProcessPool("A process in the process pool was "
475475
"terminated abruptly while the future was "
476476
"running or pending.")
477+
cause_str = None
477478
if cause is not None:
478-
bpe.__cause__ = _RemoteTraceback(
479-
f"\n'''\n{''.join(cause)}'''")
479+
cause_str = ''.join(cause)
480+
else:
481+
# No cause known, so report any processes that have
482+
# terminated with nonzero exit codes, e.g. from a
483+
# segfault. Multiple may terminate simultaneously,
484+
# so include all of them in the traceback.
485+
errors = []
486+
for p in self.processes.values():
487+
if p.exitcode is not None and p.exitcode != 0:
488+
errors.append(f"Process {p.pid} terminated abruptly "
489+
f"with exit code {p.exitcode}")
490+
if errors:
491+
cause_str = "\n".join(errors)
492+
if cause_str:
493+
bpe.__cause__ = _RemoteTraceback(f"\n'''\n{cause_str}'''")
480494

481495
# Mark pending tasks as failed.
482496
for work_id, work_item in self.pending_work_items.items():

Lib/test/test_concurrent_futures/test_process_pool.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ def test_traceback(self):
106106
self.assertIn('raise RuntimeError(123) # some comment',
107107
f1.getvalue())
108108

109+
def test_traceback_when_child_process_terminates_abruptly(self):
110+
# gh-139462 enhancement - BrokenProcessPool exceptions
111+
# should describe which process terminated.
112+
exit_code = 99
113+
with self.executor_type(max_workers=1) as executor:
114+
future = executor.submit(os._exit, exit_code)
115+
with self.assertRaises(BrokenProcessPool) as bpe:
116+
future.result()
117+
118+
cause = bpe.exception.__cause__
119+
self.assertIsInstance(cause, futures.process._RemoteTraceback)
120+
self.assertIn(
121+
f"terminated abruptly with exit code {exit_code}", cause.tb
122+
)
123+
109124
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
110125
@hashlib_helper.requires_hashdigest('md5')
111126
def test_ressources_gced_in_workers(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When a child process in a :class:`concurrent.futures.ProcessPoolExecutor`
2+
terminates abruptly, the resulting traceback will now tell you the PID
3+
and exit code of the terminated process. Contributed by Jonathan Berg.

0 commit comments

Comments
 (0)