|
| 1 | +"""Async middleware utilities for bridging sync and async middleware execution.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import asyncio |
| 6 | +import inspect |
| 7 | +import threading |
| 8 | +from typing import TYPE_CHECKING, Any |
| 9 | + |
| 10 | +if TYPE_CHECKING: |
| 11 | + from collections.abc import Callable |
| 12 | + |
| 13 | + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, Response |
| 14 | + |
| 15 | + |
| 16 | +def wrap_middleware_async(middleware: Callable, next_handler: Callable) -> Callable: |
| 17 | + """Wrap a middleware to work in an async context. |
| 18 | +
|
| 19 | + For async middlewares, delegates directly with ``await``. |
| 20 | +
|
| 21 | + For sync middlewares, runs the middleware in a background thread and uses |
| 22 | + ``asyncio.Event`` / ``threading.Event`` to coordinate the ``next()`` call |
| 23 | + so the async handler can be awaited on the main event-loop while the sync |
| 24 | + middleware blocks its own thread waiting for the result. |
| 25 | +
|
| 26 | + Parameters |
| 27 | + ---------- |
| 28 | + middleware : Callable |
| 29 | + A sync or async middleware ``(app, next_middleware) -> Response``. |
| 30 | + next_handler : Callable |
| 31 | + The next (async) handler in the chain. |
| 32 | +
|
| 33 | + Returns |
| 34 | + ------- |
| 35 | + Callable |
| 36 | + An async callable ``(app) -> Response`` that executes *middleware* |
| 37 | + followed by *next_handler*. |
| 38 | + """ |
| 39 | + |
| 40 | + async def wrapped(app: ApiGatewayResolver) -> Response: |
| 41 | + if inspect.iscoroutinefunction(middleware): |
| 42 | + return await middleware(app, next_handler) |
| 43 | + |
| 44 | + return await _run_sync_middleware_in_thread(middleware, next_handler, app) |
| 45 | + |
| 46 | + return wrapped |
| 47 | + |
| 48 | + |
| 49 | +async def _run_sync_middleware_in_thread( |
| 50 | + middleware: Callable, |
| 51 | + next_handler: Callable, |
| 52 | + app: Any, |
| 53 | +) -> Any: |
| 54 | + """Execute a **sync** middleware inside a daemon thread. |
| 55 | +
|
| 56 | + The sync middleware calls ``sync_next(app)`` which: |
| 57 | +
|
| 58 | + 1. Signals the async side that the middleware is ready for the next handler. |
| 59 | + 2. Blocks the thread until the async handler has produced a response. |
| 60 | + 3. Returns the response so the middleware can do post-processing. |
| 61 | +
|
| 62 | + Meanwhile the async side awaits *next_handler*, feeds the response back, |
| 63 | + and waits for the thread to finish. |
| 64 | + """ |
| 65 | + middleware_called_next = asyncio.Event() |
| 66 | + next_app_holder: list = [] |
| 67 | + real_response_holder: list = [] |
| 68 | + middleware_result_holder: list = [] |
| 69 | + middleware_error_holder: list = [] |
| 70 | + |
| 71 | + def sync_next(app: Any) -> Any: |
| 72 | + next_app_holder.append(app) |
| 73 | + middleware_called_next.set() |
| 74 | + # Block this thread until the async handler resolves |
| 75 | + event = threading.Event() |
| 76 | + next_app_holder.append(event) |
| 77 | + event.wait() |
| 78 | + return real_response_holder[0] |
| 79 | + |
| 80 | + def run_middleware() -> None: |
| 81 | + try: |
| 82 | + result = middleware(app, sync_next) |
| 83 | + middleware_result_holder.append(result) |
| 84 | + except Exception as e: |
| 85 | + middleware_error_holder.append(e) |
| 86 | + |
| 87 | + thread = threading.Thread(target=run_middleware, daemon=True) |
| 88 | + thread.start() |
| 89 | + |
| 90 | + # Wait for the middleware to call next() |
| 91 | + await middleware_called_next.wait() |
| 92 | + |
| 93 | + # Resolve the async next_handler on the event-loop |
| 94 | + real_response = await next_handler(next_app_holder[0]) |
| 95 | + real_response_holder.append(real_response) |
| 96 | + |
| 97 | + # Unblock the middleware thread |
| 98 | + threading_event = next_app_holder[1] |
| 99 | + threading_event.set() |
| 100 | + |
| 101 | + # Wait for the middleware thread to complete post-processing |
| 102 | + thread.join() |
| 103 | + |
| 104 | + if middleware_error_holder: |
| 105 | + raise middleware_error_holder[0] |
| 106 | + |
| 107 | + return middleware_result_holder[0] |
0 commit comments