From: Min RK Date: Sun, 30 Mar 2025 01:54:43 +0000 (+0200) Subject: improve error message when run_sync does not complete for reasons other than timeout... X-Git-Tag: v6.5.0b1~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0b7e700c1fe90f387602ad3305598b23833c30f9;p=thirdparty%2Ftornado.git improve error message when run_sync does not complete for reasons other than timeout (#3436) --- diff --git a/tornado/ioloop.py b/tornado/ioloop.py index 31874fd2..647b8a7d 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -50,7 +50,7 @@ import typing from typing import Union, Any, Type, Optional, Callable, TypeVar, Tuple, Awaitable if typing.TYPE_CHECKING: - from typing import Dict, List, Set # noqa: F401 + from typing import Dict, List, Set, TypedDict # noqa: F401 from typing_extensions import Protocol else: @@ -491,7 +491,11 @@ class IOLoop(Configurable): .. versionchanged:: 6.2 ``tornado.util.TimeoutError`` is now an alias to ``asyncio.TimeoutError``. """ - future_cell = [None] # type: List[Optional[Future]] + if typing.TYPE_CHECKING: + FutureCell = TypedDict( # noqa: F841 + "FutureCell", {"future": Optional[Future], "timeout_called": bool} + ) + future_cell = {"future": None, "timeout_called": False} # type: FutureCell def run() -> None: try: @@ -502,38 +506,45 @@ class IOLoop(Configurable): result = convert_yielded(result) except Exception: fut = Future() # type: Future[Any] - future_cell[0] = fut + future_cell["future"] = fut future_set_exc_info(fut, sys.exc_info()) else: if is_future(result): - future_cell[0] = result + future_cell["future"] = result else: fut = Future() - future_cell[0] = fut + future_cell["future"] = fut fut.set_result(result) - assert future_cell[0] is not None - self.add_future(future_cell[0], lambda future: self.stop()) + assert future_cell["future"] is not None + self.add_future(future_cell["future"], lambda future: self.stop()) self.add_callback(run) if timeout is not None: def timeout_callback() -> None: + # signal that timeout is triggered + future_cell["timeout_called"] = True # If we can cancel the future, do so and wait on it. If not, # Just stop the loop and return with the task still pending. # (If we neither cancel nor wait for the task, a warning # will be logged). - assert future_cell[0] is not None - if not future_cell[0].cancel(): + assert future_cell["future"] is not None + if not future_cell["future"].cancel(): self.stop() timeout_handle = self.add_timeout(self.time() + timeout, timeout_callback) self.start() if timeout is not None: self.remove_timeout(timeout_handle) - assert future_cell[0] is not None - if future_cell[0].cancelled() or not future_cell[0].done(): - raise TimeoutError("Operation timed out after %s seconds" % timeout) - return future_cell[0].result() + assert future_cell["future"] is not None + if future_cell["future"].cancelled() or not future_cell["future"].done(): + if future_cell["timeout_called"]: + raise TimeoutError("Operation timed out after %s seconds" % timeout) + else: + # timeout not called; maybe stop() was called explicitly + # or some other cancellation + raise RuntimeError("Event loop stopped before Future completed.") + return future_cell["future"].result() def time(self) -> float: """Returns the current time according to the `IOLoop`'s clock. diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py index acd3ec86..37ac6deb 100644 --- a/tornado/test/ioloop_test.py +++ b/tornado/test/ioloop_test.py @@ -627,6 +627,16 @@ class TestIOLoopRunSync(unittest.TestCase): self.io_loop.run_sync(f2) + def test_stop_no_timeout(self): + async def f(): + await asyncio.sleep(0.1) + IOLoop.current().stop() + await asyncio.sleep(10) + + with self.assertRaises(RuntimeError) as cm: + self.io_loop.run_sync(f) + assert "Event loop stopped" in str(cm.exception) + class TestPeriodicCallbackMath(unittest.TestCase): def simulate_calls(self, pc, durations):