gh-155852: Do not cancel remaining Executor.map calls on a callable's TimeoutError (GH-155853)
serhiy-storchaka <[email protected]>
| Newsgroups | gmane.comp.python.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://github.com/python/cpython/commit/a8e1d25355004ba8790e34dbf7fb5268674e7f08 commit: a8e1d25355004ba8790e34dbf7fb5268674e7f08 branch: main author: tonghuaroot (童话) <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-08-17T15:44:40+03:00 summary: 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. files: M Lib/concurrent/futures/_base.py M Lib/test/test_concurrent_futures/executor.py diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index cc335d9aa1ea55d..e728b8e0a91f744 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -309,10 +309,13 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED): def _result_or_cancel(fut, timeout=None): try: try: - return (fut.result(timeout), None) - except TimeoutError: - raise - except BaseException as exc: + # fut.exception() returns the call's own error but raises + # TimeoutError only for a map() timeout. + exc = fut.exception(timeout) + if exc is not None: + return (None, exc) + return (fut.result(), None) + except CancelledError as exc: return (None, exc) finally: fut.cancel() diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index 5d9f27c83bf9a81..ff7bd0db0c2199c 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -29,6 +29,12 @@ def raiser(exception, msg='std'): raise exception(msg) +def timeout_on_one(x): + if x == 1: + raise TimeoutError + return x + + class FalseyBoolException(Exception): def __bool__(self): return False @@ -87,6 +93,20 @@ def test_map_exception(self): self.assertRaises(StopIteration, next, i) self.assertRaises(StopIteration, next, i) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_map_timeout_from_callable(self): + # A TimeoutError from the callable is not the map() timeout, whether + # or not a map() timeout is set. + for timeout in (None, support.SHORT_TIMEOUT): + with self.subTest(timeout=timeout): + i = self.executor.map(timeout_on_one, [0, 1, 2, 3], + timeout=timeout) + self.assertEqual(next(i), 0) + self.assertRaises(TimeoutError, next, i) + self.assertEqual(next(i), 2) + self.assertEqual(next(i), 3) + self.assertRaises(StopIteration, next, i) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('walltime') def test_map_timeout(self): _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]