From 0c6f898005099be189ee65bcfda659f5fc13b802 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 24 Jul 2022 21:18:05 +0100 Subject: [PATCH] gh-95051: ensure that timeouts scheduled with `asyncio.Timeout` that have already expired are deliverered promptly (#95109) Co-authored-by: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> --- Doc/library/asyncio-task.rst | 3 +++ Lib/asyncio/timeouts.py | 8 +++---- Lib/test/test_asyncio/test_timeouts.py | 24 +++++++++++++++++++ ...2-07-21-22-59-22.gh-issue-95109.usxA9r.rst | 1 + 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-07-21-22-59-22.gh-issue-95109.usxA9r.rst diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index a307d22e456..a6b638c1124 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -625,6 +625,9 @@ Timeouts If *when* is a float, it is set as the new deadline. + if *when* is in the past, the timeout will trigger on the next + iteration of the event loop. + .. method:: expired() -> bool Return whether the context manager has exceeded its deadline diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index a89205348ff..94d25535fbc 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -52,10 +52,10 @@ class Timeout: self._timeout_handler = None else: loop = events.get_running_loop() - self._timeout_handler = loop.call_at( - when, - self._on_timeout, - ) + if when <= loop.time(): + self._timeout_handler = loop.call_soon(self._on_timeout) + else: + self._timeout_handler = loop.call_at(when, self._on_timeout) def expired(self) -> bool: """Is timeout expired during execution?""" diff --git a/Lib/test/test_asyncio/test_timeouts.py b/Lib/test/test_asyncio/test_timeouts.py index 9801541e55b..b9bac6f7837 100644 --- a/Lib/test/test_asyncio/test_timeouts.py +++ b/Lib/test/test_asyncio/test_timeouts.py @@ -105,6 +105,30 @@ class TimeoutTests(unittest.IsolatedAsyncioTestCase): self.assertLess(t1-t0, 2) self.assertTrue(t0 <= cm.when() <= t1) + async def test_timeout_zero_sleep_zero(self): + loop = asyncio.get_running_loop() + t0 = loop.time() + with self.assertRaises(TimeoutError): + async with asyncio.timeout(0) as cm: + await asyncio.sleep(0) + t1 = loop.time() + self.assertTrue(cm.expired()) + # 2 sec for slow CI boxes + self.assertLess(t1-t0, 2) + self.assertTrue(t0 <= cm.when() <= t1) + + async def test_timeout_in_the_past_sleep_zero(self): + loop = asyncio.get_running_loop() + t0 = loop.time() + with self.assertRaises(TimeoutError): + async with asyncio.timeout(-11) as cm: + await asyncio.sleep(0) + t1 = loop.time() + self.assertTrue(cm.expired()) + # 2 sec for slow CI boxes + self.assertLess(t1-t0, 2) + self.assertTrue(t0 >= cm.when() <= t1) + async def test_foreign_exception_passed(self): with self.assertRaises(KeyError): async with asyncio.timeout(0.01) as cm: diff --git a/Misc/NEWS.d/next/Library/2022-07-21-22-59-22.gh-issue-95109.usxA9r.rst b/Misc/NEWS.d/next/Library/2022-07-21-22-59-22.gh-issue-95109.usxA9r.rst new file mode 100644 index 00000000000..40196dd214a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-07-21-22-59-22.gh-issue-95109.usxA9r.rst @@ -0,0 +1 @@ +Ensure that timeouts scheduled with :class:`asyncio.Timeout` that have already expired are delivered promptly.