Files
sub2api/backend/internal/service/refresh_policy.go
erio 1fc9dd7b68 feat: unified OAuth token refresh API with distributed locking
Introduce OAuthRefreshAPI as the single entry point for all OAuth token
refresh operations, eliminating the race condition where background
refresh and inline refresh could simultaneously use the same
refresh_token (fixes #1035).

Key changes:
- Add OAuthRefreshExecutor interface extending TokenRefresher with CacheKey
- Add OAuthRefreshAPI.RefreshIfNeeded with lock → DB re-read → double-check flow
- Add ProviderRefreshPolicy / BackgroundRefreshPolicy strategy types
- Simplify all 4 TokenProviders to delegate to OAuthRefreshAPI
- Rewrite TokenRefreshService.refreshWithRetry to use unified API path
- Add MergeCredentials and BuildClaudeAccountCredentials helpers
- Add 40 unit tests covering all new and modified code paths
2026-03-16 01:31:54 +08:00

100 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import "time"
// ProviderRefreshErrorAction 定义 provider 在刷新失败时的处理动作。
type ProviderRefreshErrorAction int
const (
// ProviderRefreshErrorReturn 失败即返回错误(不降级旧 token
ProviderRefreshErrorReturn ProviderRefreshErrorAction = iota
// ProviderRefreshErrorUseExistingToken 失败后继续使用现有 token。
ProviderRefreshErrorUseExistingToken
)
// ProviderLockHeldAction 定义 provider 在刷新锁被占用时的处理动作。
type ProviderLockHeldAction int
const (
// ProviderLockHeldUseExistingToken 直接使用现有 token。
ProviderLockHeldUseExistingToken ProviderLockHeldAction = iota
// ProviderLockHeldWaitForCache 等待后重试缓存读取。
ProviderLockHeldWaitForCache
)
// ProviderRefreshPolicy 描述 provider 的平台差异策略。
type ProviderRefreshPolicy struct {
OnRefreshError ProviderRefreshErrorAction
OnLockHeld ProviderLockHeldAction
FailureTTL time.Duration
}
func ClaudeProviderRefreshPolicy() ProviderRefreshPolicy {
return ProviderRefreshPolicy{
OnRefreshError: ProviderRefreshErrorUseExistingToken,
OnLockHeld: ProviderLockHeldWaitForCache,
FailureTTL: time.Minute,
}
}
func OpenAIProviderRefreshPolicy() ProviderRefreshPolicy {
return ProviderRefreshPolicy{
OnRefreshError: ProviderRefreshErrorUseExistingToken,
OnLockHeld: ProviderLockHeldWaitForCache,
FailureTTL: time.Minute,
}
}
func GeminiProviderRefreshPolicy() ProviderRefreshPolicy {
return ProviderRefreshPolicy{
OnRefreshError: ProviderRefreshErrorReturn,
OnLockHeld: ProviderLockHeldUseExistingToken,
FailureTTL: 0,
}
}
func AntigravityProviderRefreshPolicy() ProviderRefreshPolicy {
return ProviderRefreshPolicy{
OnRefreshError: ProviderRefreshErrorReturn,
OnLockHeld: ProviderLockHeldUseExistingToken,
FailureTTL: 0,
}
}
// BackgroundSkipAction 定义后台刷新服务在“未实际刷新”场景的计数方式。
type BackgroundSkipAction int
const (
// BackgroundSkipAsSkipped 计入 skipped保持当前默认行为
BackgroundSkipAsSkipped BackgroundSkipAction = iota
// BackgroundSkipAsSuccess 计入 success仅用于兼容旧统计口径时可选
BackgroundSkipAsSuccess
)
// BackgroundRefreshPolicy 描述后台刷新服务的调用侧策略。
type BackgroundRefreshPolicy struct {
OnLockHeld BackgroundSkipAction
OnAlreadyRefresh BackgroundSkipAction
}
func DefaultBackgroundRefreshPolicy() BackgroundRefreshPolicy {
return BackgroundRefreshPolicy{
OnLockHeld: BackgroundSkipAsSkipped,
OnAlreadyRefresh: BackgroundSkipAsSkipped,
}
}
func (p BackgroundRefreshPolicy) handleLockHeld() error {
if p.OnLockHeld == BackgroundSkipAsSuccess {
return nil
}
return errRefreshSkipped
}
func (p BackgroundRefreshPolicy) handleAlreadyRefreshed() error {
if p.OnAlreadyRefresh == BackgroundSkipAsSuccess {
return nil
}
return errRefreshSkipped
}