Skip to content
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

Merged
merged 5 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Unreleased
- Add route decorators for common HTTP methods. For example,
``@app.post("/login")`` is a shortcut for
``@app.route("/login", methods=["POST"])``. :pr:`3907`
- Support async views, error handlers, before and after request, and
teardown functions. :pr:`3412`


Version 1.1.2
Expand Down
81 changes: 81 additions & 0 deletions docs/async-await.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.. _async_await:

Using ``async`` and ``await``
=============================

.. versionadded:: 2.0

Routes, error handlers, before request, after request, and teardown
functions can all be coroutine functions if Flask is installed with the
``async`` extra (``pip install flask[async]``). This allows views to be
defined with ``async def`` and use ``await``.

.. code-block:: python

@app.route("/get-data")
async def get_data():
data = await async_db_query(...)
return jsonify(data)


Performance
-----------

Async functions require an event loop to run. Flask, as a WSGI
application, uses one worker to handle one request/response cycle.
When a request comes in to an async view, Flask will start an event loop
in a thread, run the view function there, then return the result.

Each request still ties up one worker, even for async views. The upside
is that you can run async code within a view, for example to make
multiple concurrent database queries, HTTP requests to an external API,
etc. However, the number of requests your application can handle at one
time will remain the same.

**Async is not inherently faster than sync code.** Async is beneficial
when performing concurrent IO-bound tasks, but will probably not improve
CPU-bound tasks. Traditional Flask views will still be appropriate for
most use cases, but Flask's async support enables writing and using
code that wasn't possible natively before.


When to use Quart instead
-------------------------

Flask's async support is less performant than async-first frameworks due
to the way it is implemented. If you have a mainly async codebase it
would make sense to consider `Quart`_. Quart is a reimplementation of
Flask based on the `ASGI`_ standard instead of WSGI. This allows it to
handle many concurrent requests, long running requests, and websockets
without requiring individual worker processes or threads.

It has also already been possible to run Flask with Gevent or Eventlet
to get many of the benefits of async request handling. These libraries
patch low-level Python functions to accomplish this, whereas ``async``/
``await`` and ASGI use standard, modern Python capabilities. Deciding
whether you should use Flask, Quart, or something else is ultimately up
to understanding the specific needs of your project.

.. _Quart: https://gitlab.com/pgjones/quart
.. _ASGI: https://asgi.readthedocs.io/en/latest/


Extensions
----------

Existing Flask extensions only expect views to be synchronous. If they
provide decorators to add functionality to views, those will probably
not work with async views because they will not await the function or be
awaitable. Other functions they provide will not be awaitable either and
will probably be blocking if called within an async view.

Check the changelog of the extension you want to use to see if they've
implemented async support, or make a feature request or PR to them.


Other event loops
-----------------

At the moment Flask only supports :mod:`asyncio`. It's possible to
override :meth:`flask.Flask.ensure_sync` to change how async functions
are wrapped to use a different library.
19 changes: 19 additions & 0 deletions docs/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,25 @@ Also see the :doc:`/becomingbig` section of the documentation for some
inspiration for larger applications based on Flask.


Async/await and ASGI support
----------------------------

Flask supports ``async`` coroutines for view functions by executing the
coroutine on a separate thread instead of using an event loop on the
main thread as an async-first (ASGI) framework would. This is necessary
for Flask to remain backwards compatible with extensions and code built
before ``async`` was introduced into Python. This compromise introduces
a performance cost compared with the ASGI frameworks, due to the
overhead of the threads.

Due to how tied to WSGI Flask's code is, it's not clear if it's possible
to make the ``Flask`` class support ASGI and WSGI at the same time. Work
is currently being done in Werkzeug to work with ASGI, which may
eventually enable support in Flask as well.

See :doc:`/async-await` for more discussion.


What Flask is, What Flask is Not
--------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ instructions for web development with Flask.
patterns/index
deploying/index
becomingbig
async-await


API Reference
Expand Down
1 change: 1 addition & 0 deletions requirements/tests.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pytest
asgiref
blinker
greenlet
python-dotenv
2 changes: 2 additions & 0 deletions requirements/tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#
# pip-compile requirements/tests.in
#
asgiref==3.2.10
# via -r requirements/tests.in
attrs==20.3.0
# via pytest
blinker==1.4
Expand Down
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
"itsdangerous>=0.24",
"click>=5.1",
],
extras_require={"dotenv": ["python-dotenv"]},
extras_require={
"async": ["asgiref>=3.2"],
"dotenv": ["python-dotenv"],
},
)
22 changes: 19 additions & 3 deletions src/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import weakref
from datetime import timedelta
from inspect import iscoroutinefunction
from itertools import chain
from threading import Lock

Expand Down Expand Up @@ -34,6 +35,7 @@
from .helpers import get_flashed_messages
from .helpers import get_load_dotenv
from .helpers import locked_cached_property
from .helpers import run_async
from .helpers import url_for
from .json import jsonify
from .logging import create_logger
Expand Down Expand Up @@ -1050,7 +1052,7 @@ def add_url_rule(
"View function mapping is overwriting an existing"
f" endpoint function: {endpoint}"
)
self.view_functions[endpoint] = view_func
self.view_functions[endpoint] = self.ensure_sync(view_func)

@setupmethod
def template_filter(self, name=None):
Expand Down Expand Up @@ -1165,7 +1167,7 @@ def before_first_request(self, f):

.. versionadded:: 0.8
"""
self.before_first_request_funcs.append(f)
self.before_first_request_funcs.append(self.ensure_sync(f))
return f

@setupmethod
Expand Down Expand Up @@ -1198,7 +1200,7 @@ def teardown_appcontext(self, f):

.. versionadded:: 0.9
"""
self.teardown_appcontext_funcs.append(f)
self.teardown_appcontext_funcs.append(self.ensure_sync(f))
return f

@setupmethod
Expand Down Expand Up @@ -1517,6 +1519,20 @@ def should_ignore_error(self, error):
"""
return False

def ensure_sync(self, func):
"""Ensure that the function is synchronous for WSGI workers.
Plain ``def`` functions are returned as-is. ``async def``
functions are wrapped to run and wait for the response.

Override this method to change how the app runs async views.

.. versionadded:: 2.0
"""
if iscoroutinefunction(func):
return run_async(func)

return func

def make_response(self, rv):
"""Convert the return value from a view function to an instance of
:attr:`response_class`.
Expand Down
64 changes: 51 additions & 13 deletions src/flask/blueprints.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict
from functools import update_wrapper

from .scaffold import _endpoint_from_view_func
Expand Down Expand Up @@ -235,24 +236,44 @@ def register(self, app, options, first_registration=False):
# Merge blueprint data into parent.
if first_registration:

def extend(bp_dict, parent_dict):
def extend(bp_dict, parent_dict, ensure_sync=False):
for key, values in bp_dict.items():
key = self.name if key is None else f"{self.name}.{key}"

if ensure_sync:
values = [app.ensure_sync(func) for func in values]

parent_dict[key].extend(values)

def update(bp_dict, parent_dict):
for key, value in bp_dict.items():
key = self.name if key is None else f"{self.name}.{key}"
parent_dict[key] = value
for key, value in self.error_handler_spec.items():
key = self.name if key is None else f"{self.name}.{key}"
value = defaultdict(
dict,
{
code: {
exc_class: app.ensure_sync(func)
for exc_class, func in code_values.items()
}
for code, code_values in value.items()
},
)
app.error_handler_spec[key] = value

app.view_functions.update(self.view_functions)
extend(self.before_request_funcs, app.before_request_funcs)
extend(self.after_request_funcs, app.after_request_funcs)
extend(self.teardown_request_funcs, app.teardown_request_funcs)
for endpoint, func in self.view_functions.items():
app.view_functions[endpoint] = app.ensure_sync(func)

extend(
self.before_request_funcs, app.before_request_funcs, ensure_sync=True
)
extend(self.after_request_funcs, app.after_request_funcs, ensure_sync=True)
extend(
self.teardown_request_funcs,
app.teardown_request_funcs,
ensure_sync=True,
)
extend(self.url_default_functions, app.url_default_functions)
extend(self.url_value_preprocessors, app.url_value_preprocessors)
extend(self.template_context_processors, app.template_context_processors)
update(self.error_handler_spec, app.error_handler_spec)

for deferred in self.deferred_functions:
deferred(state)
Expand Down Expand Up @@ -380,23 +401,29 @@ def before_app_request(self, f):
before each request, even if outside of a blueprint.
"""
self.record_once(
lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
lambda s: s.app.before_request_funcs.setdefault(None, []).append(
s.app.ensure_sync(f)
)
)
return f

def before_app_first_request(self, f):
"""Like :meth:`Flask.before_first_request`. Such a function is
executed before the first request to the application.
"""
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
self.record_once(
lambda s: s.app.before_first_request_funcs.append(s.app.ensure_sync(f))
)
return f

def after_app_request(self, f):
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
is executed after each request, even if outside of the blueprint.
"""
self.record_once(
lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
lambda s: s.app.after_request_funcs.setdefault(None, []).append(
s.app.ensure_sync(f)
)
)
return f

Expand Down Expand Up @@ -443,3 +470,14 @@ def app_url_defaults(self, f):
lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
)
return f

def ensure_sync(self, f):
"""Ensure the function is synchronous.

Override if you would like custom async to sync behaviour in
this blueprint. Otherwise the app's
:meth:`~flask.Flask.ensure_sync` is used.

.. versionadded:: 2.0
"""
return f
49 changes: 49 additions & 0 deletions src/flask/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
import socket
import warnings
from functools import update_wrapper
from functools import wraps
from threading import RLock

import werkzeug.utils
from werkzeug.exceptions import NotFound
from werkzeug.local import ContextVar
from werkzeug.routing import BuildError
from werkzeug.urls import url_quote

Expand Down Expand Up @@ -729,3 +731,50 @@ def is_ip(value):
return True

return False


def run_async(func):
"""Return a sync function that will run the coroutine function *func*."""
try:
from asgiref.sync import async_to_sync
except ImportError:
raise RuntimeError(
"Install Flask with the 'async' extra in order to use async views."
)

pgjones marked this conversation as resolved.
Show resolved Hide resolved
# Check that Werkzeug isn't using its fallback ContextVar class.
if ContextVar.__module__ == "werkzeug.local":
raise RuntimeError(
"Async cannot be used with this combination of Python & Greenlet versions."
)

@wraps(func)
def outer(*args, **kwargs):
"""This function grabs the current context for the inner function.

This is similar to the copy_current_xxx_context functions in the
ctx module, except it has an async inner.
"""
ctx = None

if _request_ctx_stack.top is not None:
davidism marked this conversation as resolved.
Show resolved Hide resolved
ctx = _request_ctx_stack.top.copy()

@wraps(func)
async def inner(*a, **k):
"""This restores the context before awaiting the func.

This is required as the function must be awaited within the
context. Only calling ``func`` (as per the
``copy_current_xxx_context`` functions) doesn't work as the
with block will close before the coroutine is awaited.
"""
if ctx is not None:
with ctx:
return await func(*a, **k)
else:
return await func(*a, **k)

return async_to_sync(inner)(*args, **kwargs)

return outer
Loading