Skip to content

feat(auth): Support sync credentials in AsyncAuthorizedSession - #18542

Merged
daniel-sanche merged 16 commits into
mainfrom
allow_sync_creds_async_rest_gapic-generator_01_sync-creds-async-rest
Oct 2, 2026
Merged

daniel-sanche merged 16 commits into
mainfrom
allow_sync_creds_async_rest_gapic-generator_01_sync-creds-async-rest

Conversation

@daniel-sanche

Copy link
Copy Markdown
Contributor

This PR allows AsyncAuthorizedSession to support standard credential types by wrapping them in an adapter

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces _SyncCredentialsAdapter to allow AsyncAuthorizedSession to support synchronous credentials by delegating calls to a synchronous transport and running blocking operations in a worker thread via asyncio.to_thread. Corresponding unit tests have also been added. Feedback on these changes highlights a potential concurrency issue where concurrent requests could trigger non-thread-safe concurrent refreshes on the synchronous credentials; it is recommended to serialize these refreshes using an asyncio.Lock with a double-checked locking pattern.

Comment thread packages/google-auth/google/auth/aio/transport/sessions.py
@parthea parthea self-assigned this Oct 2, 2026
@daniel-sanche
daniel-sanche marked this pull request as ready for review October 2, 2026 16:21
@daniel-sanche
daniel-sanche requested review from a team as code owners October 2, 2026 16:21
]
await asyncio.to_thread(sync_credentials.refresh_started.wait, 5)
# Give the remaining tasks the opportunity to start a refresh of their own.
await asyncio.sleep(0.1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gemini highlighted that this could lead to flaky tests. Instead of a generic sleep, you can yield control or loop until sync_credentials.refresh_calls equals 1 or the pending futures are registered, ensuring reliable scheduling across all operating systems.

"""

def __init__(self, credentials: google.auth.credentials.Credentials):
super().__init__()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gemini pointed out an issue here (and I could reproduce the problem in test_delegates_to_sync_credentials

  1. super().__init__() in _BaseCredentials.__init__ sets self.token = None on the adapter instance itself. Without delegating token, expiry, valid, and expired to self._credentials . adapter.token remains None even after refresh(), and adapter.valid evaluates to False.
  2. sync_requests.Request() eagerly creates a requests.Session() even when credentials never refresh (e.g., AnonymousCredentials or pre-populated tokens). Lazily initializing _sync_request and providing a close() helper avoids leaking an unused requests.Session.
         def __init__(self, credentials: google.auth.credentials.Credentials):
            self._credentials = credentials
            # Synchronous credentials cannot use the asynchronous transport of the
            # session, so they are called with a synchronous transport instead.
            self._sync_request_instance = None
            # Synchronous credentials are not safe to refresh concurrently, which
            # concurrent requests would otherwise do from multiple worker threads.
            # Instead, at most one refresh is in flight and concurrent callers share it.
            self._pending_refresh: Optional["asyncio.Future[None]"] = None
    
        @property
        def _sync_request(self):
            if self._sync_request_instance is None:
                # Imported here because `requests` is an optional dependency of
                # google-auth. It is installed alongside `aiohttp` by the `aiohttp` extra.
                from google.auth.transport import requests as sync_requests
    
                self._sync_request_instance = sync_requests.Request()
            return self._sync_request_instance
    
        def close(self):
            if (
                self._sync_request_instance is not None
                and hasattr(self._sync_request_instance, "session")
                and self._sync_request_instance.session is not None
            ):
                self._sync_request_instance.session.close()
    
        @property
        def token(self):
            """Optional[str]: The bearer token that can be used in HTTP headers to make
            authenticated requests."""
            return self._credentials.token
    
        @property
        def expiry(self):
            """Optional[datetime]: When the token expires and is no longer valid.
            If this is None, the token is assumed to never expire."""
            return self._credentials.expiry
    
        @property
        @_helpers.copy_docstring(google.auth.credentials.Credentials)
        def valid(self):
            return self._credentials.valid
    
        @property
        @_helpers.copy_docstring(google.auth.credentials.Credentials)
        def expired(self):
            return self._credentials.expired


Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For 1: The original implementation was only focusing on the parts of the API that are accessed by AsyncAuthorizedSession, which only uses the small subset of the credentials API needed for token refresh. I wanted to keep things minimal. But yeah, might as well make it more robust, in case it's used more in the future. I'll add the changes

Comment thread packages/google-auth/google/auth/aio/transport/sessions.py
Comment thread packages/google-auth/tests/transport/aio/test_sessions.py
Comment thread packages/google-auth/tests/transport/aio/test_sessions.py
@daniel-sanche
daniel-sanche merged commit 3799568 into main Oct 2, 2026
116 checks passed
@daniel-sanche
daniel-sanche deleted the allow_sync_creds_async_rest_gapic-generator_01_sync-creds-async-rest branch October 2, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants