Skip to content

fix(copilot): guard token refresh with asyncio.Lock to prevent race condition - #4684

Merged
Re-bin merged 1 commit into
HKUDS:mainfrom
axelray-dev:fix/copilot-token-refresh-race-condition
Jul 4, 2026
Merged

Re-bin merged 1 commit into
HKUDS:mainfrom
axelray-dev:fix/copilot-token-refresh-race-condition

Conversation

@axelray-dev

Copy link
Copy Markdown
Contributor

Problem

GitHubCopilotProvider._get_copilot_access_token() has a check-then-act race condition with no synchronization. When the Copilot access token expires (~30 min), concurrent chat() / chat_stream() calls both see the expired token and independently fetch new tokens from GitHub's token endpoint. The last writer wins, and the loser's in-flight request may use a stale or mismatched token.

Closes #4677.

Root cause

The provider is constructed once and shared across all agent turns. The check-and-refresh critical section in _get_copilot_access_token() reads self._copilot_access_token / self._copilot_expires_at, then fetches and writes them back without a lock. When two concurrent requests arrive after token expiry, both pass the freshness check, both issue HTTP calls to the token endpoint, and both overwrite self._copilot_access_token, self.api_key, and client.api_key.

Fix

Add an asyncio.Lock to GitHubCopilotProvider and use double-checked locking in _get_copilot_access_token():

  1. Fast path: check token freshness without acquiring the lock (existing behavior preserved).
  2. If expired, acquire the lock, then re-check freshness. Another task may have already refreshed the token while we were waiting.
  3. Only if still expired, proceed with the HTTP fetch inside the lock.

This ensures only one token fetch happens per expiry window, even under concurrent load.

Tests

  • test_concurrent_token_refresh_fetches_once: two concurrent calls with an expired token trigger exactly one HTTP fetch
  • test_second_call_returns_cached_token_while_first_in_flight: if task A is mid-fetch, task B waits for the lock, finds the cached token, and skips the HTTP call
  • test_copilot_token_lock_exists: provider has an asyncio.Lock instance

…ondition

_get_copilot_access_token had a check-then-act race: concurrent chat()
calls after token expiry both fetched new tokens and clobbered each other.
Add asyncio.Lock with double-checked locking so only one fetch happens
per expiry window.

Closes HKUDS#4677
@axelray-dev
axelray-dev marked this pull request as ready for review July 2, 2026 20:03
@chengyongru chengyongru added priority: p2 Normal backlog: minor bug, enhancement, docs, cleanup, edge case, or unvalidated proposal. bug Something isn't working provider labels Jul 3, 2026

@Re-bin Re-bin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. This is a focused provider-local concurrency fix: Copilot token refresh now uses a lock with a second cache check after acquisition, so concurrent requests do not race into duplicate token exchanges.

Validated locally:

  • python -m pytest tests/providers/test_github_copilot_concurrent_token.py -q
  • python -m ruff check nanobot/providers/github_copilot_provider.py tests/providers/test_github_copilot_concurrent_token.py

GitHub Actions is green across the matrix.

@Re-bin
Re-bin merged commit 2801141 into HKUDS:main Jul 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority: p2 Normal backlog: minor bug, enhancement, docs, cleanup, edge case, or unvalidated proposal. provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants