Skip to content

fix: use singleflight and add backoff for token refresh - #56

Merged
rhuss merged 5 commits into
mainfrom
fix/token-refresh-singleflight
Aug 8, 2026
Merged

fix: use singleflight and add backoff for token refresh#56
rhuss merged 5 commits into
mainfrom
fix/token-refresh-singleflight

Conversation

@rhuss

@rhuss rhuss commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace sync.RWMutex double-checked lock with singleflight.Group so concurrent callers share a single in-flight token refresh without holding an exclusive lock during HTTP round-trips
  • Add exponential backoff (1s, 2s, 4s, ..., 30s cap) after failed refreshes to prevent token endpoint outage amplification
  • Stale cached tokens are still returned during backoff windows for graceful degradation

Closes #50

Test plan

  • All 17 existing + new refresh tests pass with -race
  • mise run lint passes (0 issues)
  • Full test suite passes
  • New tests cover: backoff skips refresh during window, backoff resets on success, backoff caps at 30s

Summary by CodeRabbit

  • Bug Fixes
    • Improved authentication token refresh reliability when multiple refreshes occur simultaneously.
    • Added automatic retry backoff for failed refresh attempts, capped at 30 seconds.
    • Continued returning cached tokens during temporary refresh backoff when available.
    • Reset refresh delays after a successful token renewal.
    • Added clearer errors when token renewal cannot provide a valid token.

Replace RWMutex double-checked lock with singleflight.Group so concurrent
callers share a single in-flight refresh without holding an exclusive lock
during the HTTP round-trip. Add exponential backoff (1s to 30s cap) after
failed refreshes to prevent token endpoint outage amplification.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rhuss, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e444af8a-19d3-43dc-91f6-fc2adb34056a

📥 Commits

Reviewing files that changed from the base of the PR and between 1f660f6 and 54bfa07.

📒 Files selected for processing (1)
  • openshell/v1/auth_refresh_test.go
📝 Walkthrough

Walkthrough

The token refresh path now shares concurrent refreshes with singleflight. Failed refreshes use capped exponential backoff. Cached tokens remain available during backoff, and successful refreshes reset the backoff state.

Changes

Token refresh behavior

Layer / File(s) Summary
Refresh coordination and backoff state
go.mod, openshell/v1/auth_refresh.go
The refresh path uses singleflight to share concurrent refreshes. Failed refreshes use capped exponential backoff and return cached tokens when available. Successful refreshes reset the backoff state.
Backoff behavior validation
openshell/v1/auth_refresh_test.go
Tests cover coordinated refreshes, stale-token reuse during backoff, reset after successful refresh, the 30-second backoff cap, and concurrent failure handling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Callers
  participant refreshableAuth
  participant singleflight
  participant TokenSource
  Callers->>refreshableAuth: Request token
  refreshableAuth->>singleflight: Share refresh operation
  singleflight->>TokenSource: Token()
  TokenSource-->>singleflight: Token or error
  singleflight-->>refreshableAuth: Shared result
  refreshableAuth-->>Callers: Cached or refreshed token
Loading

Possibly related PRs

  • rhuss/openshell-sdk-go#19: Both changes modify token refresh coordination and its tests. This PR adds singleflight and exponential backoff.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: singleflight coordination and backoff for token refresh.
Linked Issues check ✅ Passed The changes implement singleflight refresh sharing, capped exponential backoff, stale-token reuse, and related concurrency safeguards required by issue #50.
Out of Scope Changes check ✅ Passed The dependency, implementation changes, and tests are directly related to the token refresh improvements in issue #50.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/token-refresh-singleflight

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 (1)
openshell/v1/auth_refresh.go (1)

99-130: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Update retry state once for each shared refresh.

Lines 106-117 run once for every waiter after singleflight.Group.Do returns a shared error. One failed source.Token() call can therefore increase backoff from 1 second to 30 seconds when several callers wait on it.

Move the retry-state update into the group.Do closure. Recheck nextRetry in that closure before calling source.Token(). Add a concurrent failure test that verifies one source call produces one backoff increment.

Also applies to: 140-141

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openshell/v1/auth_refresh.go` around lines 99 - 130, Update the refresh flow
around group.Do so retry state is changed only inside its closure for the single
shared source.Token call. Recheck nextRetry there before invoking the source,
and remove the outer per-waiter backoff update while preserving cached-token
fallback and successful-state reset. Add a concurrent failure test verifying
multiple waiters produce one source call and one backoff increment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openshell/v1/auth_refresh_test.go`:
- Around line 337-344: Strengthen the setup assertions around
provider.GetRequestMetadata in the backoff test: require both initial calls to
return no error, then assert beforeCount equals 2 before making the final call.
Keep the existing final metadata, no-error, and unchanged-call-count assertions.

---

Outside diff comments:
In `@openshell/v1/auth_refresh.go`:
- Around line 99-130: Update the refresh flow around group.Do so retry state is
changed only inside its closure for the single shared source.Token call. Recheck
nextRetry there before invoking the source, and remove the outer per-waiter
backoff update while preserving cached-token fallback and successful-state
reset. Add a concurrent failure test verifying multiple waiters produce one
source call and one backoff increment.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d9885d9e-cb30-4c7d-97d1-e4bfb8484818

📥 Commits

Reviewing files that changed from the base of the PR and between 2cac2cf and ec830c3.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod
  • openshell/v1/auth_refresh.go
  • openshell/v1/auth_refresh_test.go

Comment thread openshell/v1/auth_refresh_test.go Outdated
rhuss added 2 commits August 8, 2026 18:42
When multiple goroutines coalesce on a failed singleflight refresh,
each would independently double the backoff. Guard the update so only
the first goroutine from a batch applies it. Add test verifying a
single coalesced failure sets backoff to initialBackoff, not 30s.
Use a channel barrier to synchronize goroutine start and increase the
mock token fetch delay to 100ms so all goroutines enter singleflight
before the first returns. Reduce goroutine count to 20 since the barrier
ensures they all arrive together.
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.20690% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.67%. Comparing base (2cac2cf) to head (54bfa07).

Files with missing lines Patch % Lines
openshell/v1/auth_refresh.go 86.20% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main      #56   +/-   ##
=======================================
  Coverage   89.67%   89.67%           
=======================================
  Files          78       78           
  Lines        4967     4989   +22     
=======================================
+ Hits         4454     4474   +20     
- Misses        346      348    +2     
  Partials      167      167           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR improves RefreshableToken concurrency and resilience by coalescing concurrent refresh attempts with singleflight and introducing an exponential backoff after refresh failures, while continuing to serve a stale cached token during backoff windows when available.

Changes:

  • Replace the prior double-checked locking refresh path with singleflight.Group to share one in-flight refresh across callers.
  • Add exponential backoff state (nextRetry, backoff) with a 30s cap after refresh failures.
  • Add/adjust unit tests for singleflight behavior and backoff semantics; add golang.org/x/sync dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 3 comments.

File Description
openshell/v1/auth_refresh.go Switch refresh coalescing to singleflight and add backoff state/logic around refresh failures.
openshell/v1/auth_refresh_test.go Update concurrency test to synchronize starts; add backoff-focused unit tests.
go.mod Add golang.org/x/sync dependency.
go.sum Add checksums for golang.org/x/sync.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread openshell/v1/auth_refresh.go Outdated
Comment thread openshell/v1/auth_refresh.go
Comment thread openshell/v1/auth_refresh_test.go Outdated
…test)

Add defensive nil/type check on singleflight return value to prevent
panic if TokenSource returns (nil, nil). Strengthen backoff test with
explicit nextRetry and precondition assertions per CodeRabbit and
Copilot review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openshell/v1/auth_refresh_test.go`:
- Around line 428-437: Update both singleflight tests in
openshell/v1/auth_refresh_test.go:134-150 and
openshell/v1/auth_refresh_test.go:428-437 to use explicit contention barriers.
Gate all goroutines before invoking the provider request, wait until every
caller reaches the release gate, and in the failed-refresh case hold Token()
until callers contend; preserve the existing assertions while ensuring
successful and failed refreshes test actual singleflight joining rather than
scheduler timing.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e92a2dc-0d89-4d23-ab06-35aa44f4238f

📥 Commits

Reviewing files that changed from the base of the PR and between ec830c3 and 1f660f6.

📒 Files selected for processing (2)
  • openshell/v1/auth_refresh.go
  • openshell/v1/auth_refresh_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openshell/v1/auth_refresh.go

Comment thread openshell/v1/auth_refresh_test.go Outdated
Use the same channel barrier pattern as the success singleflight test
to ensure deterministic goroutine contention per CodeRabbit review.
@rhuss
rhuss merged commit b4a29fd into main Aug 8, 2026
6 of 7 checks passed
@rhuss
rhuss deleted the fix/token-refresh-singleflight branch August 8, 2026 19:07
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.

Token refresh: use singleflight and add backoff on failure

2 participants