Skip to content

Preserve token invalidation across concurrent refreshes - #1841

Open
sylvesterkaczmarek wants to merge 1 commit into
anthropics:mainfrom
sylvesterkaczmarek:fix/token-cache-invalidation-race
Open

Preserve token invalidation across concurrent refreshes#1841
sylvesterkaczmarek wants to merge 1 commit into
anthropics:mainfrom
sylvesterkaczmarek:fix/token-cache-invalidation-race

Conversation

@sylvesterkaczmarek

Copy link
Copy Markdown

Summary

Make TokenCache.invalidate() safe when it races with an in-flight credential refresh.

TokenCache deliberately releases its lock while calling the credential provider. Before this change, an invalidation that happened during that provider call could be lost:

  1. a refresh starts with the current cache state;
  2. another request receives a 401 and calls invalidate(), clearing the cache and requesting force_refresh=True;
  3. the older provider call returns;
  4. the refresh publishes that result and consumes the force flag.

The cache can therefore return and retain a token from a refresh that began before the invalidation which was supposed to revoke it.

The same race also affected advisory refresh failures: an advisory refresh that failed after a concurrent invalidation could fall back to the cached token that had just been invalidated.

Fix

Introduce an invalidation generation guarded by the existing cache lock.

Each refresh snapshots the generation and force flag before leaving the lock. When the provider returns:

  • results from the current generation are published normally;
  • results from an older generation are discarded and never returned;
  • stale advisory fallback is disabled if invalidation occurred during the refresh;
  • the one-shot force_refresh flag is consumed only by a successful refresh belonging to the current generation;
  • waiters are still released before an invalidated leader loops into the next single-flight refresh.

Repeated invalidations are coalesced while the cache is already invalidated and a forced refresh is pending. This matters for concurrent requests that all receive 401 responses for the same revoked token: those duplicate invalidations must not continually invalidate the replacement refresh.

The provider call still runs outside the lock, preserving the existing non-blocking/single-flight design.

Regression coverage

Adds deterministic threaded tests for:

  • invalidation while a provider refresh is in flight;
  • two concurrent callers sharing the same refresh;
  • discarding the pre-invalidation result;
  • forcing exactly one replacement refresh;
  • coalescing duplicate invalidations while the forced refresh is running;
  • an advisory refresh failure after invalidation refusing to serve the revoked cached fallback;
  • preserving force_refresh=True for the next provider attempt after that failure.

The concurrency scenarios were also exercised independently against the synchronization logic before preparing the PR.

@sylvesterkaczmarek
sylvesterkaczmarek requested a review from a team as a code owner August 17, 2026 07:13
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.

1 participant