66
77The ASGI application invokes ``await endpoint.function(...)`` for every
88unary RPC, so each handler must be a coroutine function.
9- ``AsyncServiceAdapter`` exposes a sync service's methods as async by
10- wrapping each sync method in ``asyncio.to_thread``. Methods that are
11- already coroutine functions pass through untouched.
9+ ``AsyncServiceAdapter`` exposes a sync service's methods as async:
10+
11+ - methods marked with :func:`on_loop` are wrapped in an ``async def`` that
12+ calls the sync body inline on the event loop. Use this only for short,
13+ non-blocking handlers (in-memory dicts, very cheap SQL reads) — a long
14+ handler running inline blocks every other RPC.
15+ - every other sync method gets an ``asyncio.to_thread`` wrapper.
16+ - methods that are already coroutine functions pass through untouched.
1217
1318Interceptors are not adapted here — each interceptor that participates in
1419an ASGI chain implements ``async intercept_unary`` directly.
1924import asyncio
2025import functools
2126import inspect
22- from typing import Any
27+ from collections .abc import Callable
28+ from typing import Any , TypeVar
29+
30+ F = TypeVar ("F" , bound = Callable [..., Any ])
31+
32+ _ON_LOOP_ATTR = "__iris_rpc_on_loop__"
33+
34+
35+ def on_loop (fn : F ) -> F :
36+ """Mark a sync RPC handler as safe to run directly on the event loop.
37+
38+ The handler must be short and non-blocking. A handler that blocks the
39+ loop for tens of milliseconds will queue every other RPC behind it.
40+ """
41+ setattr (fn , _ON_LOOP_ATTR , True )
42+ return fn
43+
44+
45+ def _is_on_loop (fn : Callable [..., Any ]) -> bool :
46+ return getattr (fn , _ON_LOOP_ATTR , False )
2347
2448
2549class AsyncServiceAdapter :
@@ -36,6 +60,13 @@ def __getattr__(self, name: str) -> Any:
3660 return attr
3761 if inspect .iscoroutinefunction (attr ):
3862 return attr
63+ if _is_on_loop (attr ):
64+
65+ @functools .wraps (attr )
66+ async def _on_loop_call (* args : Any , ** kwargs : Any ) -> Any :
67+ return attr (* args , ** kwargs )
68+
69+ return _on_loop_call
3970
4071 @functools .wraps (attr )
4172 async def _threaded_call (* args : Any , ** kwargs : Any ) -> Any :
0 commit comments