fix(token-cache): 版本过时时使用最新token而非旧token

上次修复(2665230)只阻止了写入缓存,但仍返回旧token导致403。
现在版本过时时直接使用DB中的最新token返回。

- 重构 IsTokenVersionStale 为 CheckTokenVersion,返回最新account
- 消除重复DB查询,复用版本检查时已获取的account
This commit is contained in:
shaw
2026-01-23 10:29:52 +08:00
parent 4bd3dbf2ce
commit dac6bc2228
5 changed files with 118 additions and 70 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"log"
"log/slog"
"strconv"
"strings"
"time"
@@ -102,7 +103,17 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
}
// 3. 存入缓存(验证版本后再写入,避免异步刷新任务与请求线程的竞态条件)
if p.tokenCache != nil && !IsTokenVersionStale(ctx, account, p.accountRepo) {
if p.tokenCache != nil {
latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
if isStale && latestAccount != nil {
// 版本过时,使用 DB 中的最新 token
slog.Debug("antigravity_token_version_stale_use_latest", "account_id", account.ID)
accessToken = latestAccount.GetCredential("access_token")
if strings.TrimSpace(accessToken) == "" {
return "", errors.New("access_token not found after version check")
}
// 不写入缓存,让下次请求重新处理
} else {
ttl := 30 * time.Minute
if expiresAt != nil {
until := time.Until(*expiresAt)
@@ -117,6 +128,7 @@ func (p *AntigravityTokenProvider) GetAccessToken(ctx context.Context, account *
}
_ = p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl)
}
}
return accessToken, nil
}

View File

@@ -182,7 +182,17 @@ func (p *ClaudeTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
// 3. 存入缓存(验证版本后再写入,避免异步刷新任务与请求线程的竞态条件)
if p.tokenCache != nil && !IsTokenVersionStale(ctx, account, p.accountRepo) {
if p.tokenCache != nil {
latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
if isStale && latestAccount != nil {
// 版本过时,使用 DB 中的最新 token
slog.Debug("claude_token_version_stale_use_latest", "account_id", account.ID)
accessToken = latestAccount.GetCredential("access_token")
if strings.TrimSpace(accessToken) == "" {
return "", errors.New("access_token not found after version check")
}
// 不写入缓存,让下次请求重新处理
} else {
ttl := 30 * time.Minute
if refreshFailed {
// 刷新失败时使用短 TTL避免失效 token 长时间缓存导致 401 抖动
@@ -203,6 +213,7 @@ func (p *ClaudeTokenProvider) GetAccessToken(ctx context.Context, account *Accou
slog.Warn("claude_token_cache_set_failed", "account_id", account.ID, "error", err)
}
}
}
return accessToken, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"log"
"log/slog"
"strconv"
"strings"
"time"
@@ -132,7 +133,17 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
// 3) Populate cache with TTL验证版本后再写入避免异步刷新任务与请求线程的竞态条件
if p.tokenCache != nil && !IsTokenVersionStale(ctx, account, p.accountRepo) {
if p.tokenCache != nil {
latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
if isStale && latestAccount != nil {
// 版本过时,使用 DB 中的最新 token
slog.Debug("gemini_token_version_stale_use_latest", "account_id", account.ID)
accessToken = latestAccount.GetCredential("access_token")
if strings.TrimSpace(accessToken) == "" {
return "", errors.New("access_token not found after version check")
}
// 不写入缓存,让下次请求重新处理
} else {
ttl := 30 * time.Minute
if expiresAt != nil {
until := time.Until(*expiresAt)
@@ -147,6 +158,7 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
_ = p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl)
}
}
return accessToken, nil
}

View File

@@ -163,7 +163,17 @@ func (p *OpenAITokenProvider) GetAccessToken(ctx context.Context, account *Accou
}
// 3. 存入缓存(验证版本后再写入,避免异步刷新任务与请求线程的竞态条件)
if p.tokenCache != nil && !IsTokenVersionStale(ctx, account, p.accountRepo) {
if p.tokenCache != nil {
latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
if isStale && latestAccount != nil {
// 版本过时,使用 DB 中的最新 token
slog.Debug("openai_token_version_stale_use_latest", "account_id", account.ID)
accessToken = latestAccount.GetOpenAIAccessToken()
if strings.TrimSpace(accessToken) == "" {
return "", errors.New("access_token not found after version check")
}
// 不写入缓存,让下次请求重新处理
} else {
ttl := 30 * time.Minute
if refreshFailed {
// 刷新失败时使用短 TTL避免失效 token 长时间缓存导致 401 抖动
@@ -184,6 +194,7 @@ func (p *OpenAITokenProvider) GetAccessToken(ctx context.Context, account *Accou
slog.Warn("openai_token_cache_set_failed", "account_id", account.ID, "error", err)
}
}
}
return accessToken, nil
}

View File

@@ -65,22 +65,24 @@ func (c *CompositeTokenCacheInvalidator) InvalidateToken(ctx context.Context, ac
return nil
}
// IsTokenVersionStale 检查 account 的 token 版本是否已过时
// CheckTokenVersion 检查 account 的 token 版本是否已过时,并返回最新的 account
// 用于解决异步刷新任务与请求线程的竞态条件:
// 如果刷新任务已更新 token 并删除缓存,此时请求线程的旧 account 对象不应写入缓存
//
// 返回 true 表示 token 已过时不应缓存false 表示可以缓存
func IsTokenVersionStale(ctx context.Context, account *Account, repo AccountRepository) bool {
// 返回值:
// - latestAccount: 从 DB 获取的最新 account如果查询失败则返回 nil
// - isStale: true 表示 token 已过时(应使用 latestAccountfalse 表示可以使用当前 account
func CheckTokenVersion(ctx context.Context, account *Account, repo AccountRepository) (latestAccount *Account, isStale bool) {
if account == nil || repo == nil {
return false
return nil, false
}
currentVersion := account.GetCredentialAsInt64("_token_version")
latestAccount, err := repo.GetByID(ctx, account.ID)
if err != nil || latestAccount == nil {
// 查询失败,默认允许缓存
return false
// 查询失败,默认允许缓存,不返回 latestAccount
return nil, false
}
latestVersion := latestAccount.GetCredentialAsInt64("_token_version")
@@ -91,12 +93,12 @@ func IsTokenVersionStale(ctx context.Context, account *Account, repo AccountRepo
slog.Debug("token_version_stale_no_current_version",
"account_id", account.ID,
"latest_version", latestVersion)
return true
return latestAccount, true
}
// 情况2: 两边都没有版本号,说明从未被异步刷新过,允许缓存
if currentVersion == 0 && latestVersion == 0 {
return false
return latestAccount, false
}
// 情况3: 比较版本号,如果 DB 中的版本更新,当前 account 已过时
@@ -105,8 +107,8 @@ func IsTokenVersionStale(ctx context.Context, account *Account, repo AccountRepo
"account_id", account.ID,
"current_version", currentVersion,
"latest_version", latestVersion)
return true
return latestAccount, true
}
return false
return latestAccount, false
}