]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
ioloop: current() and friends delegate to asyncio
authorBen Darnell <ben@bendarnell.com>
Sun, 21 Jan 2018 18:51:59 +0000 (13:51 -0500)
committerBen Darnell <ben@bendarnell.com>
Sat, 27 Jan 2018 01:34:29 +0000 (20:34 -0500)
Instead of a redundant IOLoop._current thread-local, pass through
directly to asyncio and maintain a one-to-one mapping of asyncio loops
to IOLoops. This brings us a bit closer to the asyncio-only future.

tornado/ioloop.py
tornado/platform/asyncio.py
tornado/test/ioloop_test.py

index b17f24bd9db09d858baf008dad4fa4ef823cc87e..2e09b32b9a63033f4af84ad503b75e66086ade45 100644 (file)
@@ -47,6 +47,7 @@ import threading
 import time
 import traceback
 import math
+import weakref
 
 from tornado.concurrent import Future, is_future, chain_future, future_set_exc_info, future_add_done_callback  # noqa: E501
 from tornado.log import app_log, gen_log
@@ -180,11 +181,12 @@ class IOLoop(Configurable):
     WRITE = _EPOLLOUT
     ERROR = _EPOLLERR | _EPOLLHUP
 
-    # Global lock for creating global IOLoop instance
-    _instance_lock = threading.Lock()
-
+    # In Python 2, _current.instance points to the current IOLoop.
     _current = threading.local()
 
+    # In Python 3, _ioloop_for_asyncio maps from asyncio loops to IOLoops.
+    _ioloop_for_asyncio = weakref.WeakKeyDictionary()
+
     @classmethod
     def configure(cls, impl, **kwargs):
         if asyncio is not None:
@@ -232,7 +234,15 @@ class IOLoop(Configurable):
            This method only knows about `IOLoop` objects (and not, for
            example, `asyncio` event loops), so it is of limited use.
         """
-        return IOLoop.current(instance=False) is not None
+        if asyncio is None:
+            return IOLoop.current(instance=False) is not None
+        else:
+            # Asyncio gives us no way to ask whether an instance
+            # exists without creating one. Even calling
+            # `IOLoop.current(instance=False)` will create the asyncio
+            # loop (but not the Tornado loop, which would break the
+            # ability to use this function for "is it safe to fork?".
+            return False
 
     def install(self):
         """Deprecated alias for `make_current()`.
@@ -276,22 +286,36 @@ class IOLoop(Configurable):
            Added ``instance`` argument to control the fallback to
            `IOLoop.instance()`.
         .. versionchanged:: 5.0
+           On Python 3, control of the current `IOLoop` is delegated
+           to `asyncio`, with this and other methods as pass-through accessors.
            The ``instance`` argument now controls whether an `IOLoop`
            is created automatically when there is none, instead of
            whether we fall back to `IOLoop.instance()` (which is now
-           an alias for this method)
+           an alias for this method). ``instance=False`` is deprecated,
+           since even if we do not create an `IOLoop`, this method
+           may initialize the asyncio loop.
         """
-        current = getattr(IOLoop._current, "instance", None)
-        if current is None and instance:
-            current = None
-            if asyncio is not None:
-                from tornado.platform.asyncio import AsyncIOLoop, AsyncIOMainLoop
-                if IOLoop.configured_class() is AsyncIOLoop:
-                    current = AsyncIOMainLoop()
-            if current is None:
+        if asyncio is None:
+            current = getattr(IOLoop._current, "instance", None)
+            if current is None and instance:
                 current = IOLoop()
-            if IOLoop._current.instance is not current:
-                raise RuntimeError("new IOLoop did not become current")
+                if IOLoop._current.instance is not current:
+                    raise RuntimeError("new IOLoop did not become current")
+        else:
+            try:
+                loop = asyncio.get_event_loop()
+            except RuntimeError:
+                if not instance:
+                    return None
+                raise
+            try:
+                return IOLoop._ioloop_for_asyncio[loop]
+            except KeyError:
+                if instance:
+                    from tornado.platform.asyncio import AsyncIOMainLoop
+                    current = AsyncIOMainLoop(make_current=True)
+                else:
+                    current = None
         return current
 
     def make_current(self):
@@ -310,6 +334,8 @@ class IOLoop(Configurable):
         .. versionchanged:: 5.0
            This method also sets the current `asyncio` event loop.
         """
+        # The asyncio event loops override this method.
+        assert asyncio is None
         old = getattr(IOLoop._current, "instance", None)
         if old is not None:
             old.clear_current()
@@ -324,10 +350,11 @@ class IOLoop(Configurable):
         .. versionchanged:: 5.0
            This method also clears the current `asyncio` event loop.
         """
-        old = getattr(IOLoop._current, "instance", None)
+        old = IOLoop.current(instance=False)
         if old is not None:
             old._clear_current_hook()
-        IOLoop._current.instance = None
+        if asyncio is None:
+            IOLoop._current.instance = None
 
     def _clear_current_hook(self):
         """Instance method called when an IOLoop ceases to be current.
@@ -352,7 +379,9 @@ class IOLoop(Configurable):
             if IOLoop.current(instance=False) is None:
                 self.make_current()
         elif make_current:
-            if IOLoop.current(instance=False) is not None:
+            current = IOLoop.current(instance=False)
+            # AsyncIO loops can already be current by this point.
+            if current is not None and current is not self:
                 raise RuntimeError("current IOLoop already exists")
             self.make_current()
 
index d28a491db9486a3b2dfb98cc84c984a8ee7dfae6..0d7eea3e57f4235ca5957e70dfccb2ee1caef657 100644 (file)
@@ -38,6 +38,7 @@ class BaseAsyncIOLoop(IOLoop):
         self.readers = set()
         self.writers = set()
         self.closing = False
+        IOLoop._ioloop_for_asyncio[asyncio_loop] = self
         super(BaseAsyncIOLoop, self).initialize(**kwargs)
 
     def close(self, all_fds=False):
@@ -101,16 +102,16 @@ class BaseAsyncIOLoop(IOLoop):
         handler_func(fileobj, events)
 
     def start(self):
-        old_current = IOLoop.current(instance=False)
+        try:
+            old_loop = asyncio.get_event_loop()
+        except RuntimeError:
+            old_loop = None
         try:
             self._setup_logging()
-            self.make_current()
+            asyncio.set_event_loop(self.asyncio_loop)
             self.asyncio_loop.run_forever()
         finally:
-            if old_current is None:
-                IOLoop.clear_current()
-            else:
-                old_current.make_current()
+            asyncio.set_event_loop(old_loop)
 
     def stop(self):
         self.asyncio_loop.stop()
@@ -165,6 +166,11 @@ class AsyncIOMainLoop(BaseAsyncIOLoop):
     def initialize(self, **kwargs):
         super(AsyncIOMainLoop, self).initialize(asyncio.get_event_loop(), **kwargs)
 
+    def make_current(self):
+        # AsyncIOMainLoop already refers to the current asyncio loop so
+        # nothing to do here.
+        pass
+
 
 class AsyncIOLoop(BaseAsyncIOLoop):
     """``AsyncIOLoop`` is an `.IOLoop` that runs on an ``asyncio`` event loop.
@@ -203,7 +209,6 @@ class AsyncIOLoop(BaseAsyncIOLoop):
 
     def make_current(self):
         if not self.is_current:
-            super(AsyncIOLoop, self).make_current()
             try:
                 self.old_asyncio = asyncio.get_event_loop()
             except RuntimeError:
index 7cae9f1ab77a32a1bb5fe89d8deae70dd532420f..1aa3f1e54b168b9540db36092e5a9e57535fa96b 100644 (file)
@@ -725,6 +725,8 @@ class TestIOLoopRunSync(unittest.TestCase):
         self.io_loop.run_sync(namespace['f'])
 
 
+@unittest.skipIf(asyncio is not None,
+                 'IOLoop configuration not available')
 class TestPeriodicCallback(unittest.TestCase):
     def setUp(self):
         self.io_loop = FakeTimeIOLoop()
@@ -830,6 +832,8 @@ class TestIOLoopConfiguration(unittest.TestCase):
         self.assertEqual(cls, 'AsyncIOMainLoop')
 
     @unittest.skipIf(twisted is None, "twisted module not present")
+    @unittest.skipIf(asyncio is not None,
+                     "IOLoop configuration not available")
     def test_twisted(self):
         cls = self.run_python(
             'from tornado.platform.twisted import TwistedIOLoop',