Skip to content

Commit

Permalink
Cache construction of middleware handlers (#9158)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco authored Sep 17, 2024
1 parent 8ad118b commit bf022b3
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGES/9158.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Significantly improved performance of middlewares -- by :user:`bdraco`.

The construction of the middleware wrappers is now cached and is built once per handler instead of on every request.
25 changes: 19 additions & 6 deletions aiohttp/web_app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import logging
import warnings
from functools import partial, update_wrapper
from functools import cache, partial, update_wrapper
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -16,6 +16,7 @@
MutableMapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
Expand All @@ -30,7 +31,7 @@
from . import hdrs
from .helpers import AppKey
from .log import web_logger
from .typedefs import Middleware
from .typedefs import Handler, Middleware
from .web_exceptions import NotAppKeyWarning
from .web_middlewares import _fix_request_current_app
from .web_request import Request
Expand Down Expand Up @@ -69,6 +70,18 @@
_Resource = TypeVar("_Resource", bound=AbstractResource)


@cache
def _build_middlewares(
handler: Handler, apps: Tuple["Application", ...]
) -> Callable[[Request], Awaitable[StreamResponse]]:
"""Apply middlewares to handler."""
for app in apps:
assert app.pre_frozen, "middleware handlers are not ready"
for m in app._middlewares_handlers:
handler = update_wrapper(partial(m, handler=handler), handler)
return handler


@final
class Application(MutableMapping[Union[str, AppKey[Any]], Any]):
__slots__ = (
Expand Down Expand Up @@ -186,6 +199,9 @@ def __len__(self) -> int:
def __iter__(self) -> Iterator[Union[str, AppKey[Any]]]:
return iter(self._state)

def __hash__(self) -> int:
return id(self)

@overload # type: ignore[override]
def get(self, key: AppKey[_T], default: None = ...) -> Optional[_T]: ...

Expand Down Expand Up @@ -380,10 +396,7 @@ async def _handle(self, request: Request) -> StreamResponse:
handler = match_info.handler

if self._run_middlewares:
for app in match_info.apps[::-1]:
assert app.pre_frozen, "middleware handlers are not ready"
for m in app._middlewares_handlers:
handler = update_wrapper(partial(m, handler=handler), handler)
handler = _build_middlewares(handler, match_info.apps[::-1])

resp = await handler(request)

Expand Down
22 changes: 14 additions & 8 deletions tests/test_web_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ async def middleware(request, handler: Handler):
app.middlewares.append(middleware)
app.router.add_route("GET", "/", handler)
client = await aiohttp_client(app)
resp = await client.get("/")
assert 201 == resp.status
txt = await resp.text()
assert "OK[MIDDLEWARE]" == txt

# Call twice to verify cache works
for _ in range(2):
resp = await client.get("/")
assert 201 == resp.status
txt = await resp.text()
assert "OK[MIDDLEWARE]" == txt


async def test_middleware_handles_exception(loop: Any, aiohttp_client: Any) -> None:
Expand All @@ -42,10 +45,13 @@ async def middleware(request, handler: Handler):
app.middlewares.append(middleware)
app.router.add_route("GET", "/", handler)
client = await aiohttp_client(app)
resp = await client.get("/")
assert 501 == resp.status
txt = await resp.text()
assert "Error text[MIDDLEWARE]" == txt

# Call twice to verify cache works
for _ in range(2):
resp = await client.get("/")
assert 501 == resp.status
txt = await resp.text()
assert "Error text[MIDDLEWARE]" == txt


async def test_middleware_chain(loop: Any, aiohttp_client: Any) -> None:
Expand Down

0 comments on commit bf022b3

Please sign in to comment.