fix: add token expiry buffer to prevent expired token usage and lock concurrent refreshes#283
fix: add token expiry buffer to prevent expired token usage and lock concurrent refreshes#283SoulPancake wants to merge 6 commits intomainfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #283 +/- ##
==========================================
+ Coverage 69.91% 69.96% +0.04%
==========================================
Files 140 140
Lines 10764 10779 +15
==========================================
+ Hits 7526 7541 +15
Misses 3238 3238 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughThis PR implements proactive token expiry handling in both async and synchronous OAuth2 clients with configurable buffer and randomized jitter. Concurrency control via asyncio.Lock and threading.Lock prevents redundant token refreshes when multiple coroutines or threads request authentication headers simultaneously. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR updates the SDK’s OAuth2 client (async + sync) to refresh access tokens before they reach their exact expiry time (using a proactive buffer with jitter) and to prevent concurrent refresh stampedes via locking, mirroring a prior fix in the JS SDK.
Changes:
- Add a proactive “near-expiry” buffer (with jitter) to token validity checks.
- Add double-checked locking (asyncio.Lock / threading.Lock) so only one refresh happens under concurrency.
- Extend unit tests to cover near-expiry refresh behavior and concurrent refresh scenarios.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| openfga_sdk/oauth2.py | Adds proactive expiry buffer + asyncio lock around token refresh. |
| openfga_sdk/sync/oauth2.py | Adds proactive expiry buffer + threading lock around token refresh. |
| test/oauth2_test.py | Adds async tests for near-expiry refresh and concurrent refresh behavior. |
| test/sync/oauth2_test.py | Adds sync tests for near-expiry refresh and concurrent refresh behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
openfga_sdk/oauth2.py (1)
146-153:⚠️ Potential issue | 🟠 MajorClamp the proactive buffer to the token lifetime.
This window is always 300–600 seconds, so a provider that returns a shorter-lived token is considered invalid again immediately after refresh. The next
get_authentication_header()call will fetch a new token instead of reusing the one it just obtained. Bound the buffer toexpires_inbefore storing it.Suggested fix
- self._access_expiry_time = datetime.now() + timedelta( - seconds=int(api_response.get("expires_in")) - ) - self._access_token = api_response.get("access_token") - self._access_token_expiry_buffer = ( - TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC - + random.random() * TOKEN_EXPIRY_JITTER_IN_SEC - ) + expires_in = int(api_response.get("expires_in")) + expiry_buffer = min( + TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC + + random.random() * TOKEN_EXPIRY_JITTER_IN_SEC, + max(0, expires_in - 1), + ) + self._access_expiry_time = datetime.now() + timedelta( + seconds=expires_in + ) + self._access_token_expiry_buffer = expiry_buffer + self._access_token = api_response.get("access_token")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openfga_sdk/oauth2.py` around lines 146 - 153, The proactive expiry buffer is currently set to TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC + random.random()*TOKEN_EXPIRY_JITTER_IN_SEC regardless of the token's lifetime, so clamp that buffer to the actual expires_in from api_response before storing _access_token_expiry_buffer; compute expires = int(api_response.get("expires_in")), compute desired_buffer = TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC + random.random()*TOKEN_EXPIRY_JITTER_IN_SEC, then set _access_token_expiry_buffer = min(desired_buffer, max(0, expires - 1)) (or another small floor) so get_authentication_header() reuses tokens shorter than the jittered window and _access_expiry_time/_access_token remain consistent.openfga_sdk/sync/oauth2.py (1)
147-154:⚠️ Potential issue | 🟠 MajorCompute a bounded buffer before publishing the refreshed token.
This buffer is always 300–600 seconds, so any provider that returns a shorter-lived token becomes “expired” again immediately and every later header lookup re-hits the token endpoint. In the sync client there is also a race here: another thread can observe Lines 147-150 before Line 151 updates the buffer, because
_token_valid()runs outside the lock. Compute a clamped buffer first and publish_access_tokenlast so readers never see a stale validity window.Suggested fix
- self._access_expiry_time = datetime.now() + timedelta( - seconds=int(api_response.get("expires_in")) - ) - self._access_token = api_response.get("access_token") - self._access_token_expiry_buffer = ( - TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC - + random.random() * TOKEN_EXPIRY_JITTER_IN_SEC - ) + expires_in = int(api_response.get("expires_in")) + expiry_buffer = min( + TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC + + random.random() * TOKEN_EXPIRY_JITTER_IN_SEC, + max(0, expires_in - 1), + ) + self._access_expiry_time = datetime.now() + timedelta( + seconds=expires_in + ) + self._access_token_expiry_buffer = expiry_buffer + self._access_token = api_response.get("access_token")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openfga_sdk/sync/oauth2.py` around lines 147 - 154, The code sets _access_expiry_time and _access_token before computing _access_token_expiry_buffer which can cause immediate re-expiry and a race where _token_valid() (called outside the lock) sees an inconsistent window; fix by computing a clamped buffer value first (use TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC and TOKEN_EXPIRY_JITTER_IN_SEC, clamp it so it does not exceed the token lifetime from api_response["expires_in"]), then set _access_expiry_time, set _access_token_expiry_buffer to that precomputed value, and finally publish _access_token (all within the same synchronized block) so readers using _token_valid() never observe a stale validity window.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/sync/oauth2_test.py`:
- Around line 502-517: The test currently only asserts token fetch happened once
and that all collected results are the expected header, but it doesn't ensure
all worker threads completed successfully; update the test around
oauth_client.get_authentication_header, mock_obtain_token, obtain_calls and
results to assert that all 5 threads produced a result (e.g., assert
len(results) == 5) or capture and re-raise thread exceptions so a failing thread
will fail the test, ensuring the concurrency path is actually exercised by all
threads.
---
Outside diff comments:
In `@openfga_sdk/oauth2.py`:
- Around line 146-153: The proactive expiry buffer is currently set to
TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC +
random.random()*TOKEN_EXPIRY_JITTER_IN_SEC regardless of the token's lifetime,
so clamp that buffer to the actual expires_in from api_response before storing
_access_token_expiry_buffer; compute expires =
int(api_response.get("expires_in")), compute desired_buffer =
TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC +
random.random()*TOKEN_EXPIRY_JITTER_IN_SEC, then set _access_token_expiry_buffer
= min(desired_buffer, max(0, expires - 1)) (or another small floor) so
get_authentication_header() reuses tokens shorter than the jittered window and
_access_expiry_time/_access_token remain consistent.
In `@openfga_sdk/sync/oauth2.py`:
- Around line 147-154: The code sets _access_expiry_time and _access_token
before computing _access_token_expiry_buffer which can cause immediate re-expiry
and a race where _token_valid() (called outside the lock) sees an inconsistent
window; fix by computing a clamped buffer value first (use
TOKEN_EXPIRY_THRESHOLD_BUFFER_IN_SEC and TOKEN_EXPIRY_JITTER_IN_SEC, clamp it so
it does not exceed the token lifetime from api_response["expires_in"]), then set
_access_expiry_time, set _access_token_expiry_buffer to that precomputed value,
and finally publish _access_token (all within the same synchronized block) so
readers using _token_valid() never observe a stale validity window.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e84e7f2a-99b7-466d-b203-7a8a9dac093b
📒 Files selected for processing (4)
openfga_sdk/oauth2.pyopenfga_sdk/sync/oauth2.pytest/oauth2_test.pytest/sync/oauth2_test.py
Replace three separate mutable fields with a single frozen dataclass assigned atomically, ensuring concurrent threads always see a complete token state snapshot.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Description
Mirrors the same bug fix as in the JS sdk openfga/js-sdk#331
Tokens were being used until the exact moment of expiry. Added an expiry buffer (with jitter) so tokens are refreshed before they expire, preventing in-flight requests from hitting the server with an expired token.
Under burst concurrency, multiple coroutines/threads could simultaneously see the token as invalid and all fetch a new one. Added asyncio.Lock (async) and threading.Lock (sync) with double-checked locking so only one refresh happens and the rest reuse the result.
What problem is being solved?
How is it being solved?
What changes are made to solve it?
References
Review Checklist
mainSummary by CodeRabbit
Release Notes
Bug Fixes
Tests