fix(vertex): serialize credentials.refresh() across threads via _sync_refresh_lock

refresh_auth is invoked from three call sites that can run on different
threads (sync get_access_token, async slow path via asyncify, and the
background proactive refresh task). Only the sync path was protected
by _sync_refresh_lock, so a concurrent sync + async/background call
could invoke google-auth's Credentials.refresh() on the same object
from two threads simultaneously, mutating internal credential state.

Move the lock acquisition into refresh_auth itself; the lock is an
RLock so reentrant acquisition from the sync path remains safe.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-20 20:53:44 +00:00
parent 0570b16b75
commit 1bc0cf375a
No known key found for this signature in database

View file

@ -366,7 +366,17 @@ class VertexBase:
except ImportError:
raise ImportError(GOOGLE_IMPORT_ERROR_MESSAGE)
credentials.refresh(Request())
# Serialize all refreshes on this VertexBase across threads.
# ``credentials.refresh()`` is not safe to call concurrently on the
# same credentials object, and this method is invoked from three
# places that can run on different threads:
# - sync ``get_access_token`` (already holds ``_sync_refresh_lock``)
# - the async slow path (via ``asyncify`` in a worker thread)
# - the background proactive refresh task (via ``asyncify``)
# ``_sync_refresh_lock`` is an ``RLock`` so reentrant acquisition
# from the sync path is safe.
with self._sync_refresh_lock:
credentials.refresh(Request())
def _acquire_async_refresh_lock(self, credential_cache_key: tuple) -> asyncio.Lock:
"""Increment the refcount and return the lock for ``credential_cache_key``.