Error with Multiple asyncio runs #683
Replies: 3 comments
-
|
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.
-
|
Same as @sstoops , implementing @shemogumbe solution fixes |
Beta Was this translation helpful? Give feedback.
-
|
That will create a new So replacement for |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Based on #82 and #366
Here, The first
asyncio.runworks 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.runmultiple 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:
GraphServiceClientsource, I found in its__init__method that it constructs aGraphRequestAdapterGraphRequestAdapter, 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_middlewaremethod, 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 ofGraphClientFactorywill share the same client object that was created under whichever thread/event loop the module was imported in.To summarize all that, every
GraphServiceClientyou create is going to end up sharing the same underlyinghttpx.AsyncClient. Thathttpx.AsyncClientinstance 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?
GraphServiceClientcan accept eithercredentialsandscope, resulting in it constructing an auth scope and request adapter for you, or it can just accept an existing request adapter.The
get_request_adapterfunction 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