Skip to content

Conversation

@pgjones
Copy link
Member

@pgjones pgjones commented Nov 1, 2019

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 async support it does not do so throughout and specifically does not support ASGI. This means that Flask is constrained (when using async) to have worse performance than the ASGI equivalents. This is necessary as I don't think Flask can be async throughout (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,

@app.route("/")
async def index():
    return "Async hello"

this comes with a cost though of poorer performance than using the
sync equivalent.

@miguelgrinberg
Copy link
Contributor

miguelgrinberg commented Nov 1, 2019

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.

@pgjones
Copy link
Member Author

pgjones commented Nov 1, 2019

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.

@miguelgrinberg
Copy link
Contributor

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.

@davidism
Copy link
Member

davidism commented Nov 1, 2019

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 @route() async def at all. Later improvements could build on this.

Please continue to discuss, I wanted to add some context for why this implementation was submitted.

@davidism
Copy link
Member

davidism commented Nov 1, 2019

@pgjones if it's helpful, I don't mind an optional dependency on asgiref, as they have some nice async/sync helpers already.

@lepture
Copy link
Contributor

lepture commented Nov 2, 2019

I suggest we create a v2 branch and putting async into v2. And in v2, we don't support Python 2.7 anymore.

@pgjones
Copy link
Member Author

pgjones commented Nov 4, 2019

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.

@davidism
Copy link
Member

davidism commented Apr 6, 2020

@pgjones master is Python 3 only now, this can be rebased.

@pgjones pgjones force-pushed the async branch 2 times, most recently from b54cbd4 to b0adaa1 Compare April 11, 2020 15:27
@pgjones
Copy link
Member Author

pgjones commented Apr 11, 2020

@davidism I think this is good to review and potentially merge.

@davidism davidism added this to the 2.0.0 milestone Apr 12, 2020
@pgjones pgjones force-pushed the async branch 2 times, most recently from 2856ce6 to 28a77d5 Compare April 18, 2020 14:07
@pgjones
Copy link
Member Author

pgjones commented Apr 18, 2020

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?

@Leechael
Copy link

AnyIO is better choice than asgiref?

@pallets pallets deleted a comment from magiskboy Jul 3, 2020
@davidism
Copy link
Member

davidism commented Jul 6, 2020

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

@miguelgrinberg
Copy link
Contributor

miguelgrinberg commented Jul 6, 2020

@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:

  • await for an async function in a normal Flask view function (without blocking, obviously):
@app.route('/foo')
def foo():
    await_(asyncio.sleep(1))
    return 'Hello!'

The await_ helper function uses asyncio.create_task() to launch the passed coroutine as a normal asyncio function, and then uses a greenlet based event to wait for it to complete w/o blocking.

  • use an async function as a Flask view function by decorating it with the same await_ helper:
@app.route('/foo')
@await_
async def foo():
    await asyncio.sleep(1)
    return 'Hello!'
  • await a non-blocking standard function inside an async function. Here is requests.get() with eventlet monkey-patching:
@app.route('/baz')
@await_
async def baz():
    rv = await make_awaitable(requests.get, 'https://google.com')
    print(rv)
    return 'Hello!'

The make_awaitable() helper function starts a greenlet on the requested function and waits for it to complete using a asyncio.Future.

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 greenlet.switch(). And the eventlet monkey-patching allows blocking functions in the Python standard library to run on top of asyncio and become awaitables.

I hope I will have something that people can test and poke holes at soon.

@jab
Copy link
Contributor

jab commented Jul 6, 2020

@miguelgrinberg et al. quoting @oremanj in https://twitter.com/oremanj/status/1279994426538905600

I packaged this a few months ago as https://github.com/oremanj/greenback — it is indeed a useful tool to have in one’s toolbox!

@pgjones
Copy link
Member Author

pgjones commented Jul 6, 2020

As I understand the greenlet based solutions they work by switching back to a (prime) coroutine in order to await i.e. every await_ switches to the prime, awaits normally and then switches back. This then requires an event loop to run and await the prime coroutine. Running the event loop is the problem that this PR solves using a thread.

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?

@miguelgrinberg
Copy link
Contributor

miguelgrinberg commented Jul 6, 2020

would I be correct in thinking that the Eventlet hub acts as an asyncio event loop substitute

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.

Actually reading further, I think you are running Flask within an asyncio loop?

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 await_ helper function then there is a switch to the eventlet hub's greenlet, which is an async function that has full access to await on things. And if you issue any I/O using eventlet functions, those are sent to the hub, which calls asyncio.add_reader or asyncio.add_writer to wait on the file descriptors using asyncio support.

@davidism
Copy link
Member

@pgjones I'm not 100% sure, but I think there's an issue with contexts. Are mutations to request, session, and g visible for the entire request? For example, a before_request function sets something that a decorator on the view function looks at, such as Flask-Login. Flask-Login also has its own context local, current_user. Each async function copies the request context and runs in a separate thread. Not sure how that will change (for better or worse) with ContextVar in pallets/werkzeug#1778.

@pgjones
Copy link
Member Author

pgjones commented Jan 11, 2021

I think mutation would be fine, isn't it fine when you use copy_current_request_context? I believe the contexts are copied, but the request, session, app, and g, objects referenced by the contexts are the same. (Are you thinking about the copy or something else?)

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.

@pallets pallets locked and limited conversation to collaborators Mar 8, 2021
@davidism davidism force-pushed the async branch 3 times, most recently from 0889434 to 42cae88 Compare March 10, 2021 20:38
@pgjones pgjones force-pushed the async branch 2 times, most recently from 3f41322 to 5e11471 Compare March 24, 2021 19:52
@pgjones
Copy link
Member Author

pgjones commented Mar 24, 2021

Note that @davidism and I have discussed that this implementation must support extensions overriding the Flask.ensure_sync method to change the usage throughout. This turns out to be difficult given the Scaffold changes, and the best solution we've found so far results in the slightly unpleasant empty Blueprint.ensure_sync method.

pgjones added 2 commits April 6, 2021 09:35
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).
pgjones and others added 3 commits April 6, 2021 15:33
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.
@pgjones
Copy link
Member Author

pgjones commented Apr 7, 2021

Updated docs are look good to me.

@davidism davidism merged commit 1a79d2d into pallets:master Apr 7, 2021
@pgjones pgjones deleted the async branch April 7, 2021 12:47
@pgjones
Copy link
Member Author

pgjones commented Apr 7, 2021

🎉

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.