Skip to content

Conversation

@pvaneck
Copy link
Member

@pvaneck pvaneck commented Nov 8, 2025

Motivation

In high-concurrency asynchronous applications, it's common for multiple coroutines to simultaneously request an access token for the same scope (e.g., https://cosmos.azure.com/.default). When a token is expired or not yet cached, this can trigger multiple, identical, and concurrent network requests to Entra ID. This "thundering herd" can put unnecessary load on both the client app and the Entra ID service, and it can also result in redundant network calls when only one is ultimately needed.

Changes

This pull request implements asynchronous locking within the async GetTokenMixin to ensure that for any given set of token request arguments, only one network request is in-flight at any given time,

  1. Asynchronous Locking: When get_token/get_token_info is called asynchronously, the credential now first checks for an existing lock associated with the specific combination of scopes and token request arguments.
  2. First Requester Fetches: If no lock exists, the current coroutine acquires one, proceeds with the network request to fetch the token, caches the result, and then releases the lock.
  3. Concurrent Requesters Wait: Any other coroutines that request a token for the same scope + arguments while the lock is active will asynchronously await the lock's release instead of initiating their own network requests.
  4. Cached Token Retrieval: Once the lock is released by the first requester, the waiting coroutines will wake up, find the token in the cache with a double-check, and return it immediately without a network call.

Results

  • Reduced network I/O and throttle risk in scenarios where users asynchronously create and use several clients with the same credential instance.
  • No more superfluous identical requests.
  • Independent token requests with different scopes do not block each other.

Implementation Notes

  • We use a WeakRefDictionary to store and automatically-clean locks. Garbage collection will delete locks once they stop being used, helping us avoid unbounded growth of the lock dictionary.
  • Supports both asyncio and trio event loops by determining at runtime which Lock class to use.

Supplemental gist: https://gist.github.com/pvaneck/133ab2cc1fe57d4aeb0f31fd00c0a77d

@github-actions
Copy link

github-actions bot commented Nov 10, 2025

API Change Check

APIView identified API level changes in this PR and created the following API reviews

azure-identity

@pvaneck pvaneck force-pushed the identity-async-lock branch from 1c2203d to efe5b16 Compare November 10, 2025 21:55
@pvaneck pvaneck requested a review from Copilot November 10, 2025 23:05
Copilot finished reviewing on behalf of pvaneck November 10, 2025 23:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment on lines +176 to +186
try:
token = await self._request_token(
*scopes, claims=claims, tenant_id=tenant_id, enable_cae=enable_cae, **kwargs
)
except Exception: # pylint:disable=broad-except
self._last_request_time = int(time.time())
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _last_request_time update should occur before calling _request_token (on success path) as well, not only in the exception handler. Currently, when _request_token succeeds, _last_request_time is never updated, which could bypass the retry delay check in _should_refresh. This inconsistency means the retry delay only applies after failures, not after successful token requests.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to modify this logic to only set the _last_request_time throttle when a _request_token call fails.

This still prevents rapid retries on failures, without allowing a successful request for one scope to block a necessary refresh for another (which would be the case if we set this on success as well).

Implements per-scope lock mechanism to prevent concurrent token requests
for the same token request argument combination, reducing unnecessary network calls
and improving performance in high-concurrency async scenarios.

Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
@pvaneck pvaneck force-pushed the identity-async-lock branch from efe5b16 to c764476 Compare November 10, 2025 23:53
from typing import Type


def get_running_async_lock_class() -> Type:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this into azure-core?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core does already have a similar function, but it returns a Lock instance instead of a class/type. An instance is fine for most cases where only one lock needs to be instantiated, but in this particular scenario, several locks are instantiated. I only want to check the event loop once to get the class, so I think it's fine to have a separate function here for this purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants