Repository: tornadoweb/tornado

Issue:
websocket: Task was destroyed but it is pending!
[E 191029 10:54:51 base_events:1608] Task was destroyed but it is pending!
    task: <Task pending coro=<WebSocketProtocol13._receive_frame_loop() done, defined at D:\AndroidControl\Python37\lib\site-packages\tornado\websocket.py:1115> wait_for=<Future pending cb=[IOLoop.add_future.<locals>.<lambda>() at D:\AndroidControl\Python37\lib\site-packages\tornado\ioloop.py:690, <TaskWakeupMethWrapper object at 0x000002AC48174468>()]> cb=[IOLoop.add_future.<locals>.<lambda>() at D:\AndroidControl\Python37\lib\site-packages\tornado\ioloop.py:690]>


Discussion:
If this message occurs while the process is shutting down, it's generally harmless - it just means that the process is only partially cleaning up after itself. I recommend ignoring it, but if you want to work towards a cleaner shutdown, you need to close all your websocket connections before stopping the event loop.

This message comes from asyncio rather than Tornado, so there's not much Tornado can do to control it. 
> If this message occurs while the process is shutting down, it's generally harmless - it just means that the process is only partially cleaning up after itself. I recommend ignoring it, but if you want to work towards a cleaner shutdown, you need to close all your websocket connections before stopping the event loop.
> 
> This message comes from asyncio rather than Tornado, so there's not much Tornado can do to control it.

I have the same RuntimeWarning, but it doesn't occur only on shut down. I get this warning constantly while my websocket is open. I know it comes from asyncio, but the Tornado is talking to asyncio. 

I am using tornado 6.0.3 with python 3.7
Hmm. There's only one `receive_frame_loop` call per websocket connection, so it shouldn't be possible to see this more than once while the connection is alive. Are you sure something's not closing and reopening the connection? But even if it is, it's not expected to see this message at any time but shutdown. Can you provide any kind of reproducible test case?

Are you also on windows? There are reports of issues with websockets on windows that are not understood: #2136. 
Hi @bdarnell , I faced the same problem as @afzoun. 

you can reproduce the error by running the cam_server.py.

Please rename the index.txt to index.html (github does not allow me upload html file)
[index.txt](https://github.com/tornadoweb/tornado/files/4012243/index.txt)

Please rename the cam_server.txt to cam_server.py (github does not allow me upload python file)
[cam_server.txt](https://github.com/tornadoweb/tornado/files/4012242/cam_server.txt)

@tankienleong in your example you call `write_message()` on `CamWSHandler` instances from the gstreamer callback `pull_frame()`, which runs in a different thread than the ioloop of the async http/websocket server. That is a problem, you are not supposed to do that. The only thing you can do to interact with tornado stuff from other threads, is call the appropriate ioloop's `add_callback()` method, and then do the other tornado related calls in that callback. I think that `AnyThreadEventLoopPolicy` does not really help you here.

Resolving patch:
diff --git a/tornado/gen.py b/tornado/gen.py
--- a/tornado/gen.py
+++ b/tornado/gen.py
@@ -840,13 +840,17 @@ def handle_exception(
             return False
 
 
-# Convert Awaitables into Futures.
-try:
-    _wrap_awaitable = asyncio.ensure_future
-except AttributeError:
-    # asyncio.ensure_future was introduced in Python 3.4.4, but
-    # Debian jessie still ships with 3.4.2 so try the old name.
-    _wrap_awaitable = getattr(asyncio, "async")
+def _wrap_awaitable(awaitable: Awaitable) -> Future:
+    # Convert Awaitables into Futures.
+    # Note that we use ensure_future, which handles both awaitables
+    # and coroutines, rather than create_task, which only accepts
+    # coroutines. (ensure_future calls create_task if given a coroutine)
+    fut = asyncio.ensure_future(awaitable)
+    # See comments on IOLoop._pending_tasks.
+    loop = IOLoop.current()
+    loop._register_task(fut)
+    fut.add_done_callback(lambda f: loop._unregister_task(f))
+    return fut
 
 
 def convert_yielded(yielded: _Yieldable) -> Future:
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -50,7 +50,7 @@
 from typing import Union, Any, Type, Optional, Callable, TypeVar, Tuple, Awaitable
 
 if typing.TYPE_CHECKING:
-    from typing import Dict, List  # noqa: F401
+    from typing import Dict, List, Set  # noqa: F401
 
     from typing_extensions import Protocol
 else:
@@ -159,6 +159,18 @@ async def main():
     # In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops.
     _ioloop_for_asyncio = dict()  # type: Dict[asyncio.AbstractEventLoop, IOLoop]
 
+    # Maintain a set of all pending tasks to follow the warning in the docs
+    # of asyncio.create_tasks:
+    # https://docs.python.org/3.11/library/asyncio-task.html#asyncio.create_task
+    # This ensures that all pending tasks have a strong reference so they
+    # will not be garbage collected before they are finished.
+    # (Thus avoiding "task was destroyed but it is pending" warnings)
+    # An analogous change has been proposed in cpython for 3.13:
+    # https://github.com/python/cpython/issues/91887
+    # If that change is accepted, this can eventually be removed.
+    # If it is not, we will consider the rationale and may remove this.
+    _pending_tasks = set()  # type: Set[Future]
+
     @classmethod
     def configure(
         cls, impl: "Union[None, str, Type[Configurable]]", **kwargs: Any
@@ -805,6 +817,12 @@ def close_fd(self, fd: Union[int, _Selectable]) -> None:
         except OSError:
             pass
 
+    def _register_task(self, f: Future) -> None:
+        self._pending_tasks.add(f)
+
+    def _unregister_task(self, f: Future) -> None:
+        self._pending_tasks.discard(f)
+
 
 class _Timeout(object):
     """An IOLoop timeout, a UNIX timestamp and a callback"""


Test patch: