Repository: tornadoweb/tornado

Issue:
public way to disable atexit callback for AddThreadSelectorEventLoop
In `distributed` we run a Client application that uses an asyncio event loop in a demon thread, we register an atexit callback to close the Client. Closing the Client sends a goodbye message, cancels the asyncio event loop and joins its thread. This causes a problem when using the `ProactorEventLoop` and the `AddThreadSelectorEventLoop` because the `AddThreadSelectorEventLoop` atexit hook shuts down the loop before our goodbye message can be sent.

Because we control the loop we don't need the tornado atexit hook, so would like to disable it

Currently I plan to do something like:

```python
 if sys.platform == "win32":
    def _disable_selector_eventloop_atexit(loop):
        if not isinstance(loop.asyncio_loop, asyncio.ProactorEventLoop):
            return

        from tornado.platform.asyncio import _selector_loops
        _selector_loops.discard(loop.selector_loop)
else:
    def _disable_selector_eventloop_atexit(loop):
        pass

class LoopRunner:
        ...
        async def amain() -> None:
            nonlocal loop
            loop = IOLoop.current()
            _disable_selector_eventloop_atexit(loop)
            start_evt.set()
            await self._stop_event.wait()

        def run_loop() -> None:
            nonlocal start_exc
            try:
                asyncio.run(amain())
            except BaseException as e:
                if start_evt.is_set():
                    raise
                start_exc = e
                start_evt.set()
```



Discussion:
Try the recent changes in the master branch (specifically #3272). This will shut down the selector thread cleanly (without leaving it for `atexit`) as long as you're using `asyncio.run`.

I wonder if the `atexit` hook is even still necessary. As the comment says, I was never able to reproduce or understand it properly (it doesn't make sense for this to be necessary in a daemon thread). Now that we have a cleaner shutdown method, I'm not sure we really need the `atexit` hook as a fallback (but because it's so poorly understood I'm not confident we can remove it either). 
> Try the recent changes in the master branch (specifically https://github.com/tornadoweb/tornado/pull/3272). This will shut down the selector thread cleanly (without leaving it for atexit) as long as you're using asyncio.run.

I don't expect this to work because my asyncio.run call needs to outlive the atexit callbacks 
Ah, OK. So the existing changes won't help you and we either need to get rid of the atexit callback or give you a way to opt out. I'm leaning towards the former; it's really hard for a library to safely use an atexit callback and it doesn't feel necessary anymore. But we'll see if CI is happy when I make the change...

Resolving patch:
diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py
--- a/tornado/platform/asyncio.py
+++ b/tornado/platform/asyncio.py
@@ -23,7 +23,6 @@
 """
 
 import asyncio
-import atexit
 import concurrent.futures
 import errno
 import functools
@@ -61,31 +60,6 @@ def fileno(self) -> int:
 _T = TypeVar("_T")
 
 
-# Collection of selector thread event loops to shut down on exit.
-_selector_loops: Set["SelectorThread"] = set()
-
-
-def _atexit_callback() -> None:
-    for loop in _selector_loops:
-        with loop._select_cond:
-            loop._closing_selector = True
-            loop._select_cond.notify()
-        try:
-            loop._waker_w.send(b"a")
-        except BlockingIOError:
-            pass
-        if loop._thread is not None:
-            # If we don't join our (daemon) thread here, we may get a deadlock
-            # during interpreter shutdown. I don't really understand why. This
-            # deadlock happens every time in CI (both travis and appveyor) but
-            # I've never been able to reproduce locally.
-            loop._thread.join()
-    _selector_loops.clear()
-
-
-atexit.register(_atexit_callback)
-
-
 class BaseAsyncIOLoop(IOLoop):
     def initialize(  # type: ignore
         self, asyncio_loop: asyncio.AbstractEventLoop, **kwargs: Any
@@ -480,7 +454,6 @@ async def thread_manager_anext() -> None:
         self._waker_r, self._waker_w = socket.socketpair()
         self._waker_r.setblocking(False)
         self._waker_w.setblocking(False)
-        _selector_loops.add(self)
         self.add_reader(self._waker_r, self._consume_waker)
 
     def close(self) -> None:
@@ -492,7 +465,6 @@ def close(self) -> None:
         self._wake_selector()
         if self._thread is not None:
             self._thread.join()
-        _selector_loops.discard(self)
         self.remove_reader(self._waker_r)
         self._waker_r.close()
         self._waker_w.close()


Test patch: