Skip to content

Commit 1285c9b

Browse files
committed
Issue #21817: When an exception is raised in a task submitted to a ProcessPoolExecutor, the remote traceback is now displayed in the parent process.
Patch by Claudiu Popa.
1 parent 26795ba commit 1285c9b

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

Lib/concurrent/futures/process.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import weakref
5858
from functools import partial
5959
import itertools
60+
import traceback
6061

6162
# Workers are created as daemon threads and processes. This is done to allow the
6263
# interpreter to exit when there are still idle processes in a
@@ -90,6 +91,27 @@ def _python_exit():
9091
# (Futures in the call queue cannot be cancelled).
9192
EXTRA_QUEUED_CALLS = 1
9293

94+
# Hack to embed stringification of remote traceback in local traceback
95+
96+
class _RemoteTraceback(Exception):
97+
def __init__(self, tb):
98+
self.tb = tb
99+
def __str__(self):
100+
return self.tb
101+
102+
class _ExceptionWithTraceback:
103+
def __init__(self, exc, tb):
104+
tb = traceback.format_exception(type(exc), exc, tb)
105+
tb = ''.join(tb)
106+
self.exc = exc
107+
self.tb = '\n"""\n%s"""' % tb
108+
def __reduce__(self):
109+
return _rebuild_exc, (self.exc, self.tb)
110+
111+
def _rebuild_exc(exc, tb):
112+
exc.__cause__ = _RemoteTraceback(tb)
113+
return exc
114+
93115
class _WorkItem(object):
94116
def __init__(self, future, fn, args, kwargs):
95117
self.future = future
@@ -152,8 +174,8 @@ def _process_worker(call_queue, result_queue):
152174
try:
153175
r = call_item.fn(*call_item.args, **call_item.kwargs)
154176
except BaseException as e:
155-
result_queue.put(_ResultItem(call_item.work_id,
156-
exception=e))
177+
exc = _ExceptionWithTraceback(e, e.__traceback__)
178+
result_queue.put(_ResultItem(call_item.work_id, exception=exc))
157179
else:
158180
result_queue.put(_ResultItem(call_item.work_id,
159181
result=r))

Lib/test/test_concurrent_futures.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,32 @@ def bad_map():
480480
ref)
481481
self.assertRaises(ValueError, bad_map)
482482

483+
@classmethod
484+
def _test_traceback(cls):
485+
raise RuntimeError(123) # some comment
486+
487+
def test_traceback(self):
488+
# We want ensure that the traceback from the child process is
489+
# contained in the traceback raised in the main process.
490+
future = self.executor.submit(self._test_traceback)
491+
with self.assertRaises(Exception) as cm:
492+
future.result()
493+
494+
exc = cm.exception
495+
self.assertIs(type(exc), RuntimeError)
496+
self.assertEqual(exc.args, (123,))
497+
cause = exc.__cause__
498+
self.assertIs(type(cause), futures.process._RemoteTraceback)
499+
self.assertIn('raise RuntimeError(123) # some comment', cause.tb)
500+
501+
with test.support.captured_stderr() as f1:
502+
try:
503+
raise exc
504+
except RuntimeError:
505+
sys.excepthook(*sys.exc_info())
506+
self.assertIn('raise RuntimeError(123) # some comment',
507+
f1.getvalue())
508+
483509

484510
class FutureTests(unittest.TestCase):
485511
def test_done_callback_with_result(self):

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ Core and Builtins
203203
Library
204204
-------
205205

206+
- Issue #21817: When an exception is raised in a task submitted to a
207+
ProcessPoolExecutor, the remote traceback is now displayed in the
208+
parent process. Patch by Claudiu Popa.
209+
206210
- Issue #15955: Add an option to limit output size when decompressing LZMA
207211
data. Patch by Nikolaus Rath and Martin Panter.
208212

0 commit comments

Comments
 (0)