Skip to content

Commit 0f99e6e

Browse files
committed
Propagate exception from forked subprocess
1 parent 5c8d9e9 commit 0f99e6e

File tree

1 file changed

+71
-5
lines changed

1 file changed

+71
-5
lines changed

src/sage/parallel/use_fork.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515

1616
import sys
17+
import traceback
1718
from shutil import rmtree
1819

1920
if sys.platform != 'win32':
@@ -157,6 +158,58 @@ def __call__(self, f, inputs):
157158
sage: del unpickle_override[('sage.rings.polynomial.polynomial_rational_flint', 'Polynomial_rational_flint')]
158159
sage: list(Polygen([QQ,QQ]))
159160
[(((Rational Field,), {}), x), (((Rational Field,), {}), x)]
161+
162+
When the function raises an exception, the exception is re-raised in the parent process with the same type.
163+
The traceback is gone, but we print a textual version::
164+
165+
sage: def fff(x): return ggg(x)
166+
sage: def ggg(x): return hhh(x)
167+
sage: def hhh(x): return 1/x
168+
sage: try: list(F(fff, [([0],{}), ([1],{})]))
169+
....: except: import traceback; traceback.print_exc()
170+
RuntimeError: forked subprocess raised:
171+
Traceback (most recent call last):
172+
...in ggg...
173+
ZeroDivisionError: rational division by zero
174+
The above exception was the direct cause of the following exception:
175+
...
176+
ZeroDivisionError: rational division by zero
177+
178+
When the erroneous invocation is not the first one, because of parallelism,
179+
the exception might be raised on the first or second ``next()`` function call.
180+
Both of the following behaviors are possible::
181+
182+
sage: # not tested
183+
sage: it = F(fff, [([1],{}), ([0],{})])
184+
sage: next(it)
185+
(([1], {}), 1)
186+
sage: next(it)
187+
Traceback (most recent call last):
188+
...
189+
ZeroDivisionError: rational division by zero
190+
191+
sage: # not tested
192+
sage: it = F(fff, [([1],{}), ([0],{})])
193+
sage: next(it)
194+
Traceback (most recent call last):
195+
...
196+
ZeroDivisionError: rational division by zero
197+
198+
Other corner cases::
199+
200+
sage: class UnpicklableException(Exception): __reduce__ = None
201+
sage: def fff(x): raise UnpicklableException()
202+
sage: list(F(fff, [([0],{}), ([1],{})]))
203+
Traceback (most recent call last):
204+
...
205+
RuntimeError: cannot pickle exception object
206+
207+
sage: class UnpicklableValue: __reduce__ = None
208+
sage: def fff(x): return UnpicklableValue()
209+
sage: list(F(fff, [([0],{}), ([1],{})]))
210+
Traceback (most recent call last):
211+
...
212+
RuntimeError: cannot pickle return value
160213
"""
161214
import os
162215
import sys
@@ -222,13 +275,13 @@ def __call__(self, f, inputs):
222275
with open(sobj, "rb") as file:
223276
data = file.read()
224277
except OSError:
225-
answer = "NO DATA" + W.failure
278+
should_raise, answer = False, "NO DATA" + W.failure
226279
else:
227280
os.unlink(sobj)
228281
try:
229-
answer = loads(data, compress=False)
282+
should_raise, answer = loads(data, compress=False)
230283
except Exception as E:
231-
answer = "INVALID DATA {}".format(E)
284+
should_raise, answer = False, "INVALID DATA {}".format(E)
232285

233286
out = os.path.join(dir, '%s.out' % pid)
234287
try:
@@ -238,6 +291,10 @@ def __call__(self, f, inputs):
238291
except OSError:
239292
pass
240293

294+
if should_raise:
295+
exc, tb_str = answer
296+
raise exc from RuntimeError(f"forked subprocess raised:\n{tb_str}")
297+
241298
yield (W.input, answer)
242299
finally:
243300
# Send SIGKILL signal to workers that are left.
@@ -325,8 +382,17 @@ def _subprocess(self, f, dir, args, kwds={}):
325382
set_random_seed(self.worker_seed)
326383

327384
# Now evaluate the function f.
328-
value = f(*args, **kwds)
385+
try:
386+
value = False, f(*args, **kwds)
387+
except BaseException as e:
388+
value = True, (e, traceback.format_exc())
329389

330390
# And save the result to disk.
331391
sobj = os.path.join(dir, '%s.sobj' % os.getpid())
332-
save(value, sobj, compress=False)
392+
try:
393+
save(value, sobj, compress=False)
394+
except BaseException as e:
395+
if value[0]:
396+
save((True, (RuntimeError('cannot pickle exception object'), traceback.format_exc())), sobj, compress=False)
397+
else:
398+
save((True, (RuntimeError('cannot pickle return value'), traceback.format_exc())), sobj, compress=False)

0 commit comments

Comments
 (0)