Skip to content
Closed
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 @@ -3,6 +3,8 @@ Version 3.2.0

Unreleased

- Optimize request dispatching by caching the result of ``ensure_sync`` for view
functions in ``_sync_view_functions``, reducing CPU overhead. :pr:`5939`
- Drop support for Python 3.9. :pr:`5730`
- Remove previously deprecated code: ``__version__``. :pr:`5648`
- ``RequestContext`` has merged with ``AppContext``. ``RequestContext`` is now
Expand Down
4 changes: 4 additions & 0 deletions docs/async-await.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ the decorated function,

return wrapper

To improve performance, consider caching the result of ``ensure_sync`` if your
extension calls it frequently on the same function. This is how Flask internally
optimizes request dispatching.

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.

Expand Down
18 changes: 17 additions & 1 deletion src/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ def __init__(
root_path=root_path,
)

self._sync_view_functions: dict[str, t.Callable[..., t.Any]] = {}

#: The Click command group for registering CLI commands for this
#: object. The commands are available from the ``flask`` command
#: once the application has been discovered and blueprints have
Expand Down Expand Up @@ -969,6 +971,10 @@ def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue:
be a response object. In order to convert the return value to a
proper response object, call :func:`make_response`.

.. versionchanged:: 3.2
The result of ``ensure_sync`` is cached in ``_sync_view_functions``
to improve performance.

.. versionchanged:: 0.7
This no longer does the exception handling, this code was
moved to the new :meth:`full_dispatch_request`.
Expand All @@ -987,7 +993,13 @@ def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue:
return self.make_default_options_response(ctx)
# otherwise dispatch to the handler for that endpoint
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]

if rule.endpoint not in self._sync_view_functions:
self._sync_view_functions[rule.endpoint] = self.ensure_sync(
self.view_functions[rule.endpoint]
)

return self._sync_view_functions[rule.endpoint](**view_args) # type: ignore[no-any-return]

def full_dispatch_request(self, ctx: AppContext) -> Response:
"""Dispatches the request and on top of that performs request
Expand Down Expand Up @@ -1069,6 +1081,10 @@ def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:

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

.. versionchanged:: 3.2
The result of this method is cached during request dispatching
to improve performance.

.. versionadded:: 2.0
"""
if iscoroutinefunction(func):
Expand Down
36 changes: 36 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1968,3 +1968,39 @@ def test_app_freed_on_zero_refcount():
assert weak() is None
finally:
gc.enable()


def test_sync_view_functions_cache(app, client):
"""Test that the _sync_view_functions cache is populated and used."""

@app.route("/test")
def test_view():
return "Hello"

import unittest.mock

with unittest.mock.patch.object(
app, "ensure_sync", wraps=app.ensure_sync
) as mock_ensure_sync:
# First request should call ensure_sync
response = client.get("/test")
assert response.status_code == 200
assert mock_ensure_sync.call_count == 1

# Second request should hit the cache and not call ensure_sync
response = client.get("/test")
assert response.status_code == 200
assert mock_ensure_sync.call_count == 1

# Direct mutation test (to verify the cache is bound to endpoint)
def new_view():
return "World"

# Simulating a user directly updating the view functions after setup
# Because it's already cached, this mutation won't affect _sync_view_functions
app.view_functions["test"] = new_view

# Ensure that it still returns the old result due to the cache
response = client.get("/test")
assert response.status_code == 200
assert response.data == b"Hello"
Loading