Error with Multiple asyncio runs #683
Replies: 1 comment
-
We found the exact same behavior (as noted in #366): the first request would succeed and subsequent requests would throw the While a few folks here have proposed different workaround to deal with the event loop itself, I wanted figure out why the SDK was hanging on to references to event loops. BackgroundUsing Async Django as the example, since it is where I ran into this issue, if you're not running a pure async stack (eg. one of your middlewares is non-async), then Django will run each request in a new thread with a new event loop. The Django debug server will also exhibit the same thread-per-request behavior even if you are top-to-bottom async. This is similar to the behavior described in this issue: calling CauseSo something, somewhere in the SDK is grabbing a reference to an event loop, and attempting to use that reference during a subsequent request. After much sleuthing, I found the pattern leading to this behavior:
To summarize all that, every Our workaroundI managed to come up with a workaround that seems to be working so far. It has been tested both in a non-pure-async Django stack (meaning there's a sync middleware causing Django shuttle requests out to threads) and in the Django debug server. from typing import Optional
from azure.identity.aio import ClientSecretCredential
from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider,
)
from kiota_http.kiota_client_factory import KiotaClientFactory
from msgraph.graph_request_adapter import GraphRequestAdapter, options
from msgraph.graph_service_client import GraphServiceClient
from msgraph_core import GraphClientFactory
def get_request_adapter(
credentials: ClientSecretCredential, scopes: Optional[list[str]] = None
) -> GraphRequestAdapter:
if scopes:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes)
else:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
return GraphRequestAdapter(
auth_provider=auth_provider,
client=GraphClientFactory.create_with_default_middleware(
options=options, client=KiotaClientFactory.get_default_client()
),
)
def create_client() -> GraphServiceClient:
return GraphServiceClient(
request_adapter=get_request_adapter(
credentials=..., scopes=['https://graph.microsoft.com/.default']
)
) What does this do?
The With this code, you could create a new client within the scope of your async code. For example: async def foo():
# Client created within each run and with it's own event loop. Good.
client = create_client()
client.users.list()
asyncio.run(foo())
asyncio.run(foo()) Note that the two calls to AddendumThere are many other instances in the SDK where clients are constructed within the scope of a method's default args, meaning, at the global scope. My workaround was the bare minimum I needed to get around the closed event loop issue for my own use case. If you run into this error in other parts of the SDK, I suggest looking for this problematic pattern. |
Beta Was this translation helpful? Give feedback.
-
Based on #82 and #366
Here, The first
asyncio.run
works correctly, I am able to retrieve two MailFolder with their ID and messages in one of these folders.The issue is for the second asyncio.run, the second call raise a SSLWantReadError exception.
Same to the snippet below:
The Workaround
Background
Using Async Django as the example, since it is where I ran into this issue, if you're not running a pure async stack (eg. one of your middlewares is non-async), then Django will run each request in a new thread with a new event loop. The Django debug server will also exhibit the same thread-per-request behavior even if you are top-to-bottom async.
This is similar to the behavior described in this issue: calling
asyncio.run
multiple times from a script. I believe this is also the same issue reported in #82.Cause
So something, somewhere in the SDK is grabbing a reference to an event loop, and attempting to use that reference during a subsequent request.
After much sleuthing, I found the pattern leading to this behavior:
GraphServiceClient
source, I found in its__init__
method that it constructs aGraphRequestAdapter
GraphRequestAdapter
, I found the following used as an argument to the__init__
:client = GraphClientFactory.create_with_default_middleware(options=options)
. That line means that it will generate a default client factory at the time this module is imported, not when it is executed.GraphClientFactory.create_with_default_middleware
method, we see the same pattern in the arg default:client: httpx.AsyncClient = KiotaClientFactory.get_default_client()
. Again, that line is executed when the module is first imported, not when the function is called. That means all instances ofGraphClientFactory
will share the same client object that was created under whichever thread/event loop the module was imported in.To summarize all that, every
GraphServiceClient
you create is going to end up sharing the same underlyinghttpx.AsyncClient
. Thathttpx.AsyncClient
instance is constructed when you import the module, not when you actually run any code. This client is what (eventually) holds on to the reference to the event loop.Our workaround
I managed to come up with a workaround that seems to be working so far. It has been tested both in a non-pure-async Django stack (meaning there's a sync middleware causing Django shuttle requests out to threads) and in the Django debug server.
What does this do?
GraphServiceClient
can accept eithercredentials
andscope
, resulting in it constructing an auth scope and request adapter for you, or it can just accept an existing request adapter.The
get_request_adapter
function essentially mimics the existing SDK code, but instead of constructing any clients at the global scope (during import), it constructs a new request adapter each time you construct a client.With this code, you could create a new client within the scope of your async code.
For example:
Beta Was this translation helpful? Give feedback.
All reactions