-
-
Notifications
You must be signed in to change notification settings - Fork 16.7k
Add async support
#3412
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
Add async support
#3412
Conversation
|
Hey Phil! I know my views are different than many on this topic, but the main (only?) reason I would use an asynchronous server over a synchronous one is to achieve much higher scaling. This solution obviously does not address this problem, so this should be noted. I think the note about the performance is warranted, but just a partial aspect, the scaling limitations should be noted as well. A second thought is that it would be nicer if multiple async view functions could run concurrently in the asyncio thread, all under the same loop. I believe your implementation would allow only one async function to run at any given time. It seems there wouldn't be any problem in using a shared loop to run all of them concurrently, and that would eliminate some of the performance penalties in using this solution, I think. |
|
Hi Miguel! I agree with you on both points (except that I think this does allow multiple async functions to run at once if the server is threaded or green threaded - each async function would get its own thread). With the latter point, I'd rather not explore how to implement running all functions under the same thread without a feeling that this is the right approach for Flask. |
|
Phil, I may be missing something. You are starting a thread pool of size 1 hardcoded, which means that you will have only one asyncio thread per Flask process. Even if you used a threaded server, the bottleneck is the single asyncio thread. You could make that size of 1 configurable, and then you can have some control over how many asyncio view functions can run concurrently, but that seems backwards to me. I think running all of them in a single loop is what makes most sense. I agree with you on that there is no point in going through the effort until we decide to go forward with a solution of this type. |
|
I've had individual discussions with @mitsuhiko, @andrewgodwin, and @pgjones that lead to this implementation as a possible first step, despite the limitations you point out. The first goal is to enable Please continue to discuss, I wanted to add some context for why this implementation was submitted. |
|
@pgjones if it's helpful, I don't mind an optional dependency on asgiref, as they have some nice async/sync helpers already. |
|
I suggest we create a v2 branch and putting async into v2. And in v2, we don't support Python 2.7 anymore. |
|
I'd prefer to avoid a v2 branch, and wait till next year and the end of Py2 before merging this. (Edit: Forget to say why). As I don't think it is worth maintaining a v2 branch as it couldn't be released. I've looked into asgiref, I'm not sure it is a good fit here - will take another look though, thanks. |
|
@pgjones master is Python 3 only now, this can be rebased. |
b54cbd4 to
b0adaa1
Compare
|
@davidism I think this is good to review and potentially merge. |
2856ce6 to
28a77d5
Compare
|
I've updated, and think it is good to merge. Just to note I see this is the start of async support in Flask rather than a complete solution - there are a number of unanswered questions e.g. what about views that return async iterators? |
|
AnyIO is better choice than asgiref? |
|
Saw @miguelgrinberg tweet about using an async SQLAlchemy demo: https://twitter.com/miguelgrinberg/status/1279894131976921088, might be relevant to consider with this. Here's the greenlet to async gist: https://gist.github.com/zzzeek/6287e28054d3baddc07fa21a7227904e |
|
@davidism So it's still somewhat early to call victory on this, but if you want some details on what I have done... What Mike Bayer's gist made me realize is that you can do greenlet context switches within an asyncio task, and this allows us to interrupt standard functions and send control back to the asyncio loop. To test this out I added a custom hub class to eventlet that does all the waiting using asyncio primitives such as asyncio.add_reader, add_writer, etc. Since Flask runs great with greenlets, I can now run Flask on top of eventlet, which runs on top of asyncio! Here is some of the crazy things I've tested:
@app.route('/foo')
def foo():
await_(asyncio.sleep(1))
return 'Hello!'The
@app.route('/foo')
@await_
async def foo():
await asyncio.sleep(1)
return 'Hello!'
@app.route('/baz')
@await_
async def baz():
rv = await make_awaitable(requests.get, 'https://google.com')
print(rv)
return 'Hello!'The None of this is Flask specific, actually. In fact I haven't had to hack on any official packages, this is all running on official Flask, greenlet and eventlet packages. What I love about this solution is that all of this works on a single OS thread, without executor tricks. It's truly an asyncio application that just has some "under the table" context switches done via I hope I will have something that people can test and poke holes at soon. |
|
@miguelgrinberg et al. quoting @oremanj in https://twitter.com/oremanj/status/1279994426538905600
|
|
As I understand the greenlet based solutions they work by switching back to a (prime) coroutine in order to await i.e. every It isn't clear to me how the greenlet solutions help, as Flask has no event loop available to it and running an event loop in a thread is equivalent to this PR. @miguelgrinberg would I be correct in thinking that the Eventlet hub acts as an asyncio event loop substitute? I'm thinking it allows the awaiting of coroutines as if the coroutine was awaited in an asyncio event loop? Actually reading further, I think you are running Flask within an asyncio loop? |
Not really. The eventlet hub is an asyncio task, running in a 100% asyncio application. It's a single OS thread for eventlet's hub with all of its greenlets plus any normal asyncio stuff.
Right. So Flask runs as a greenlet, either under Eventlet's WSGI server, or with the monkey-patched Werzeug dev server. So each request is started on a greenlet. If you call this |
|
@pgjones I'm not 100% sure, but I think there's an issue with contexts. Are mutations to |
|
I think mutation would be fine, isn't it fine when you use I don't think the Flask-Login context will work, however the extension could update the helper to copy the context as well. I don't think ContextVars affect either point here. |
ab9154c to
ed2b3a2
Compare
0889434 to
42cae88
Compare
3f41322 to
5e11471
Compare
|
Note that @davidism and I have discussed that this implementation must support extensions overriding the |
This allows for async functions to be passed to the Flask class
instance, for example as a view function,
@app.route("/")
async def index():
return "Async hello"
this comes with a cost though of poorer performance than using the
sync equivalent.
asgiref is the standard way to run async code within a sync context,
and is used in Django making it a safe and sane choice for this.
Werkzeug offers a ContextVar replacement for Python < 3.7, however it doesn't work across asyncio tasks, hence it makes sense to error out rather than find there are odd bugs. Note the docs build requires the latest (dev) Werkzeug due to this change (to import ContextVar from werkzeug.local).
This allows extensions to override the Flask.ensure_sync method and have the change apply to blueprints as well. Without this change it is possible for differing blueprints to have differing ensure_sync approaches depending on the extension used - which would likely result in event-loop blocking issues. This also allows blueprints to have a custom ensure_sync, although this is a by product rather than an expected use case.
|
Updated docs are look good to me. |
|
🎉 |
This will require Python 2 support to be dropped, which as I understand isn't likely to happen till 2020. Which is a bonus as it gives time to consider this change and the implications of it. (I also need to spend some time cleaning up this implementation, yet it serves as a proof of concept).
I've written this blog post to add some extra context, but I see a key aspect to this change is that whilst it adds
asyncsupport it does not do so throughout and specifically does not support ASGI. This means that Flask is constrained (when usingasync) to have worse performance than the ASGI equivalents. This is necessary as I don't think Flask can beasyncthroughout (or support ASGI) whilst being backwards compatible. Yet this change is good overall, in that users would be able to add some async usage to their existing codebases. I would also like to note that they can switch to Quart if their usage becomes mostly async.This allows for async functions to be passed to the Flask class
instance, for example as a view function,
this comes with a cost though of poorer performance than using the
sync equivalent.