fix(backend/python): don't await sync servicer behaviors in AsyncModelIdentityInterceptor#10980
fix(backend/python): don't await sync servicer behaviors in AsyncModelIdentityInterceptor#10980walcz-de wants to merge 2 commits into
Conversation
localai-org-maint-bot
left a comment
There was a problem hiding this comment.
The regression is real and the new focused suite passes locally (23/23), but this fix still bypasses an important part of grpc.aio sync-handler adaptation: the migration thread pool. original(request, context) is invoked before inspect.isawaitable, so a synchronous LoadModel or unary handler runs directly on the asyncio event-loop thread. Likewise, for response in stream calls a synchronous generator’s next() on the event loop. A slow model load/inference/stream can therefore freeze all aio RPC handling even though the TypeError is gone.
Please dispatch synchronous behavior through an executor (asyncio.to_thread / loop.run_in_executor) while continuing to await native async behavior. For a sync streaming iterator, each next() also needs executor dispatch (using a small helper that returns a done sentinel, since StopIteration must not escape through a Future). A regression test that records the handler thread ID and verifies it differs from the event-loop thread would cover this.
The awaitable-result check is useful for callable wrappers, but it must happen without first running potentially blocking sync application code on the event loop.
|
Thanks — good catch, addressed in Sync behavior now goes through Added three regression tests that record the handler thread id and assert it differs from the event-loop thread (sync LoadModel, guarded unary, sync stream). Full suite: 26/26 green. |
…lIdentityInterceptor The model-identity interceptor (added for mudler#10952) is installed on every Python backend's gRPC server. Its grpc.aio variant invokes the wrapped servicer behavior itself and awaits the result unconditionally: result = await original(request, context) # LoadModel return await original_unary(request, context) # guarded RPCs async for response in original_stream(request, context): # streaming But a backend's servicer methods may be plain sync functions. The transformers backend, for one, defines `def LoadModel` and `def Embedding` (not `async def`). grpc.aio's own dispatch adapts both shapes, but this interceptor calls the behavior directly and bypasses that. For a sync method `original(...)` returns a message object, not a coroutine, so the `await` raises: TypeError: object Result can't be used in 'await' expression The model loads, then the LoadModel RPC dies on return; the guarded sync Embedding fails the same way. It happens on every platform, not just one backend build. CI never caught it because AsyncModelIdentityInterceptor had no behavioral test -- only an "is it installed" assertion. Fix: await only when the behavior actually returned an awaitable (inspect.isawaitable), mirroring grpc.aio's own sync/async adaptation. The streaming guard iterates a sync generator with `for` and an async one with `async for`. Adds async-path coverage to model_identity_test.py exercising both sync and async LoadModel / guarded-unary / streaming behaviors. The sync cases fail on the current code with the TypeError above and pass with this fix. Signed-off-by: stefanwalcz <stefan.walcz@walcz.de>
Addresses review feedback: awaiting only awaitable results removed the TypeError, but still ran a sync LoadModel/Embedding -- and stepped a sync stream via next() -- on the asyncio event-loop thread, so a slow load/inference/stream could freeze all aio RPC handling. Route sync behavior through run_in_executor (a worker thread) while awaiting native async behavior directly. A callable wrapper that returns an awaitable is run in the thread and its awaitable awaited back on the loop. Sync streaming pulls each item via the executor with a done sentinel, so StopIteration cannot escape through a Future. Adds regression tests that record the handler thread id and assert it differs from the event-loop thread, for LoadModel, a guarded unary RPC and a sync stream. Signed-off-by: stefanwalcz <stefan.walcz@walcz.de>
de4d018 to
7754116
Compare
What
AsyncModelIdentityInterceptor(added in #10952, installed on every Python backend's gRPC server viaget_auth_interceptors) awaits the wrapped servicer behavior unconditionally. A backend's servicer methods may be plain sync functions;grpc.aionormally adapts both sync and async behaviors, but this interceptor calls the behavior itself and bypasses that adaptation:For a sync method,
original(request, context)returns a message object, not a coroutine:Impact
Any
grpc.aioPython backend with sync servicer methods breaks on the guarded RPCs, on every platform. Concretely the transformers backend definesdef LoadModelanddef Embedding(notasync def): on currentmaster, LoadModel loads the model and then dies on return; the guardedEmbeddingfails the same way.Reproduced end-to-end against the transformers backend:
CI didn't catch it because
AsyncModelIdentityInterceptorhad no behavioral test — only the "is it installed" assertion inTestInterceptorInstalled.Fix
Await the behavior's result only when it is actually awaitable (
inspect.isawaitable), mirroringgrpc.aio's own sync/async adaptation. The streaming guard iterates a sync generator withforand an async generator withasync for. The syncModelIdentityInterceptorwas already correct and is unchanged.Tests
Adds
TestAsyncInterceptorBehaviortomodel_identity_test.py, exercising the async interceptor with both sync and asyncLoadModel/ guarded-unary / streaming behaviors. The sync cases fail on currentmasterwith theTypeErrorabove and pass with this fix.