]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
improve error message when run_sync does not complete for reasons other than timeout...
authorMin RK <benjaminrk@gmail.com>
Sun, 30 Mar 2025 01:54:43 +0000 (03:54 +0200)
committerGitHub <noreply@github.com>
Sun, 30 Mar 2025 01:54:43 +0000 (21:54 -0400)
tornado/ioloop.py
tornado/test/ioloop_test.py

index 31874fd268033b59e5257e8c6139f95875958fe4..647b8a7dfc85e7059bfa45d0435cdb8e26514c10 100644 (file)
@@ -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.
index acd3ec868b99379c0d82d2c723509000758f15ca..37ac6deba2976937447c76739780182edc74b97d 100644 (file)
@@ -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):