Skip to content

Commit a8e1d25

Browse files
authored
gh-155852: Do not cancel remaining Executor.map calls on a callable's TimeoutError (GH-155853)
A TimeoutError raised by the mapped callable was re-raised like the map(timeout=...) wait timeout, aborting the iteration and cancelling the remaining calls, unlike every other exception since gh-108518. A wait timeout only occurs while the future is still running, so a TimeoutError from an already-finished future is treated as the callable's own result.
1 parent b6c11fe commit a8e1d25

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

Lib/concurrent/futures/_base.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,13 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED):
309309
def _result_or_cancel(fut, timeout=None):
310310
try:
311311
try:
312-
return (fut.result(timeout), None)
313-
except TimeoutError:
314-
raise
315-
except BaseException as exc:
312+
# fut.exception() returns the call's own error but raises
313+
# TimeoutError only for a map() timeout.
314+
exc = fut.exception(timeout)
315+
if exc is not None:
316+
return (None, exc)
317+
return (fut.result(), None)
318+
except CancelledError as exc:
316319
return (None, exc)
317320
finally:
318321
fut.cancel()

Lib/test/test_concurrent_futures/executor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def raiser(exception, msg='std'):
2929
raise exception(msg)
3030

3131

32+
def timeout_on_one(x):
33+
if x == 1:
34+
raise TimeoutError
35+
return x
36+
37+
3238
class FalseyBoolException(Exception):
3339
def __bool__(self):
3440
return False
@@ -87,6 +93,20 @@ def test_map_exception(self):
8793
self.assertRaises(StopIteration, next, i)
8894
self.assertRaises(StopIteration, next, i)
8995

96+
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
97+
def test_map_timeout_from_callable(self):
98+
# A TimeoutError from the callable is not the map() timeout, whether
99+
# or not a map() timeout is set.
100+
for timeout in (None, support.SHORT_TIMEOUT):
101+
with self.subTest(timeout=timeout):
102+
i = self.executor.map(timeout_on_one, [0, 1, 2, 3],
103+
timeout=timeout)
104+
self.assertEqual(next(i), 0)
105+
self.assertRaises(TimeoutError, next, i)
106+
self.assertEqual(next(i), 2)
107+
self.assertEqual(next(i), 3)
108+
self.assertRaises(StopIteration, next, i)
109+
90110
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
91111
@support.requires_resource('walltime')
92112
def test_map_timeout(self):

0 commit comments

Comments
 (0)