-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve asyncio integration #1041
Conversation
It seems that sphinx-build for Python 2 fails when trying to parse |
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla - and if you have received this in error or have any questions, please drop us a line at cla@fb.com. Thanks! |
This change needs tests. We have limited control over the sphinx environment on readthedocs.org (they do have a python 3 option in beta now, but it's project-wide so it might be tricky dealing with the older doc versions, and in any case such an intrusive change should be made on its own instead of incidental to some other change), so for now the docs for asyncio-related functionality should just go in the rst file instead of using autodoc.
I had been thinking that we'd support asyncio Futures directly in tornado.gen by adding asyncio.Future to tornado.concurrent.FUTURES. Do you see any reason that wouldn't work? I'm unsure about Finally, while I'd be fine with adding a |
Yes, I'd completely missed the existence of My initial approach was to have the tornado.concurrent module automatically use The The point of the |
A decorator named tulip.task used to exist, but it was deliberately removed, so that's not a great comparison. Your proposed task decorator is not similar to either of the two Task classes that currently exist. For |
That's true, I mostly just wanted to explain the motivation behind it. I don't know about similarity, but there's certainly a connection to The thing about allowing the Of course, this can be easily solved by just moving the logic on line 200-221 in gen.py to a separate function returning a However, I still feel that it's important to enable use of
A problem with the fourth option is that # concurrent.py
class Future(tornado.util.Configurable):
@classmethod
def configurable_base(cls):
return Future
@classmethod
def configurable_default(cls):
return _Future
class _Future(Future):
# what was previously tornado.concurrent.Future goes here
...
# asyncio.py
class AsyncIOFuture(tornado.concurrent.Future, asyncio.Future):
def __init__(self, *, loop=None):
asyncio.Future.__init__(self, loop=loop)
def exc_info(self):
try:
exc = self.exception()
return type(exc), exc, exc.__traceback__
except (AttributeError, asyncio.InvalidStateError):
return None
def set_exc_info(self, exc_info):
self.set_exception(exc_info[1]) # exception object stores traceback
# main.py
tornado.concurrent.Future.configure('tornado.platform.asyncio.AsyncIOFuture') What do you think? Sorry this got overly long, I've been thinking about how best to approach this issue for a few days now. |
To expand a bit on the connection between @tornado.platform.asyncio.task
def func():
yield from asyncio.sleep(2)
print("Finished running")
t = func() Is equivalent to: @asyncio.coroutine
def func():
yield from asyncio.sleep(2)
print("Finished running")
t = asyncio.Task(func()) So I do think there is a similarity; we're basically moving the @tornado.platform.asyncio.task
@asyncio.coroutine
def func():
yield from asyncio.sleep(2)
print("Finished running")
t = func() ? |
Regarding Regarding the use of Tornado Futures from asyncio coroutines: I think this is a problem to be solved by introducing some sort of extensibility on the asyncio side. Even within the standard library, asyncio coroutines cannot use concurrent.futures.Futures without a wrapper. It's more scalable for each coroutine library to consume the others' yieldable objects than for each yieldable object to implement the union of the various interfaces that might be expected of it (also consider twisted's Deferred, which is not Future-shaped at all but would ideally be supported here). |
@BeholdMyGlory I think this is better: if not asyncio.tasks.iscoroutinefunction(func):
func = asyncio.coroutine(func) But I don't know about it should be removed or not. |
How about removing |
Conflicts: tornado/platform/asyncio.py
Sorry, I've kind of been neglecting this pull request, though I've been meaning to get back to it. I got stuck on a problem when trying to get Tornado's coroutines to accept asyncio Futures. In theory it should be easy enough, just add asyncio.Future to the FUTURES tuple in tornado.concurrent, but it seems that either the schedulers or the IO loops themselves have subtly different behaviour in Tornado and asyncio, leading to delicate timing issues that I couldn't manage to debug properly. The following code exhibits the problematic behaviour in question: import asyncio
import subprocess
import tornado.httpclient
import tornado.web
import tornado.platform.asyncio
import tornado.gen
class RequestHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
proc = yield from asyncio.create_subprocess_exec("ls", "/", stdout=subprocess.PIPE)
self.write((yield from proc.communicate())[0])
tornado.platform.asyncio.AsyncIOMainLoop().install()
application = tornado.web.Application([
(r"/?", RequestHandler)
])
application.listen(8080)
loop = asyncio.get_event_loop()
loop.run_forever() The expected behaviour is that when / is fetched on the server, the output of running
This is running on a Linux 3.16 machine with Python 3.4.1 and with the latest changes from the Tornado master branch at the time of writing merged. From what I can recall from when I was debugging the issue a while back, the problem was that the If import asyncio
import functools
import subprocess
import tornado.httpclient
import tornado.web
import tornado.platform.asyncio
import tornado.gen
def task(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return asyncio.Task(func(*args, **kwargs))
return wrapper
class RequestHandler(tornado.web.RequestHandler):
@task
@asyncio.coroutine
def get(self):
proc = yield from asyncio.create_subprocess_exec("ls", "/", stdout=subprocess.PIPE)
self.write((yield from proc.communicate())[0])
tornado.platform.asyncio.AsyncIOMainLoop().install()
application = tornado.web.Application([
(r"/?", RequestHandler)
])
application.listen(8080)
loop = asyncio.get_event_loop()
loop.run_forever() I don't expect that this problem surfaces in very many places, but I still don't think that this code should be merged until it's fixed. I don't know if the problem is in Tornado or if you could pass the blame onto Python itself. I've made some additional changes since I last commented here, and I'll try to push them either today or tomorrow for further discussion. |
All right, I've pushed my changes (note that most of the commits are a few months old and GitHub has thus placed them a bit earlier in the comment thread). The changes I've made are roughly:
I'm not sure how feasible it would be for asyncio to add support for third-party Future implementations, or how it might look (Some kind of abstract Future base class? A Note that the issue mentioned in my previous comment is still present. The code can be tested by running An important difference between Tornado's and asyncio's Future implementations—the fact that asyncio.Future runs callbacks using the event loop's
So, now that I've gone over that, I guess the question is where to go from here. I wouldn't mind reverting the changes made to the Future class(es) for the time being, basically keeping the change to the FUTURES tuple and maybe the Also worth noting regarding the |
I don't think we should make Future subclass Configurable - it's too performance-critical to use the Configurable machinery. I think the right model here is a registry of adapters instead of a common base type (in python3.4 this could use functools.singledispatch; for older pythons you could either use the backported package or maybe something simpler). This would let you adapt future-like classes that do not resemble the Future interface, like Twisted's Deferreds. |
I'm not sure I quite follow, can you give an example of what something like that might look like? |
Tornado is network programming framework and application framework. So I want to use libraries based on asyncio (or Trollius) from Tornado applications. As my understanding, there are no reason for inheriting from |
If you only care about using asyncio in Tornado applications and not the other way around then yes, you won't have to concern yourself with the |
I feel using asyncio.Future in Tornado (including tornado.websocket) on Python 3.3+ is simpler. try:
from asyncio import Future
except ImportError:
from tornado.concurrent import Future @bdarnell How do you think? |
|
Additionally, since |
The singledispatch idea is that instead of the current handle_yield code that converts lists, dicts, and YieldPoints into Futures, you'd have an extensible converter list: @functools.singledispatch
def make_future(obj):
raise BadYieldError()
@make_future.register(list)
@make_future.register(dict)
def _(obj):
return Multi(obj)
# in platform/asyncio.py
@make_future.register(asyncio.Future)
def _(obj):
f = tornado.concurrent.Future()
tornado.concurrent.chain_future(obj, f)
return f
# in platform/twisted.py
@make_future.register(twisted.Deferred)
def _(obj):
f = tornado.concurrent.Future()
# register callbacks on obj to transfer result to f
return f Even though asyncio.Future and tornado Futures are very similar, they may not be close enough that we can treat them as interchangeable. The safest way to handle this is with an explicit adapter, similar to what we'd need in the twisted case. |
To wrap this up for Tornado 4.1: I've just merged some changes that implement the singledispatch pattern so you can yield asyncio Futures (and twisted Deferreds) in a Tornado coroutine (05c3073...841b2d4). I've also documented the AsyncIOLoop.asyncio_loop attribute for public consumption (in lieu of adding a get_event_loop method) and added bidirectional conversion functions between the two kinds of Futures. I have not added an |
These changes make it easier to use asyncio features from Tornado applications. The additions include:
wrap_asyncio_future
andwrap_tornado_future
functions for wrapping anasyncio.Future
in atornado.concurrent.Future
and the other way around@task
decorator that makes it possible to use asyncio coroutines with for exampletornado.web.RequestHandler
.__iter__
method intornado.concurrent.Future
that makes it possible to useyield from
with Tornado futures from asyncio coroutines. The future will be automatically wrapped in anasyncio.Future
.get_asyncio_loop
method inBaseAsyncIOLoop
to explicitly expose the asyncio event loop used by the Tornado IOLoop. This is especially useful withAsyncIOLoop
since it creates a new event loop object otherwise not accessible outside the IOLoop object.The most intrusive change here is probably the addition of the
__iter__
method intornado.concurrent.Future
. The reasoning here is that you wouldn't normally try to iterate over futures and that theyield from
syntax is sufficiently associated withasyncio
that it makes sense to reserve it for use withasyncio
.Caveat: Since asyncio futures can be cancelled,
wrap_tornado_future
tries to account for this by runningset_exception
with aCancelledError
when theasyncio.Future
that wraps the Tornado Future is cancelled. This will however probably not work as expected seeing as the standard Tornado Future doesn't support being cancelled, so I would recommend avoiding trying to cancel any futures returned bywrap_tornado_future
.Example: