token_refresh_service.go 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  12. "github.com/Wei-Shaw/sub2api/internal/util/logredact"
  13. )
  14. // tokenRefreshTempUnschedDuration token 刷新重试耗尽后临时不可调度的持续时间
  15. const tokenRefreshTempUnschedDuration = 10 * time.Minute
  16. const (
  17. defaultTokenRefreshCandidatePageSize = 200
  18. maxTokenRefreshCandidatePageSize = 1000
  19. defaultTokenRefreshProviderConcurrency = 4
  20. maxTokenRefreshProviderConcurrency = 32
  21. defaultTokenRefreshProviderQPS = 2
  22. maxTokenRefreshProviderQPS = 100
  23. defaultTokenRefreshProviderFailureThreshold = 3
  24. maxTokenRefreshProviderFailureThreshold = 100
  25. defaultTokenRefreshMaxRetries = 1
  26. maxTokenRefreshMaxRetries = 10
  27. maxTokenRefreshRetryBackoff = 30 * time.Second
  28. defaultTokenRefreshAttemptTimeout = 15 * time.Second
  29. maxTokenRefreshAttemptTimeout = 5 * time.Minute
  30. maxTokenRefreshLockSafetyMargin = 5 * time.Second
  31. defaultTokenRefreshCycleTimeout = 4 * time.Minute
  32. maxTokenRefreshCycleTimeout = time.Hour
  33. defaultTokenRefreshCleanupTimeout = 2 * time.Second
  34. )
  35. type tokenRefreshRegistration struct {
  36. platform string
  37. refresher TokenRefresher
  38. executor OAuthRefreshExecutor
  39. }
  40. // GrokOAuthRefreshMutationRepository protects background refresh failure
  41. // mutations with the exact credential document used by the upstream attempt.
  42. // This contract is intentionally Grok-only; existing provider behavior remains
  43. // unchanged.
  44. type GrokOAuthRefreshMutationRepository interface {
  45. SetGrokOAuthRefreshErrorIfCredentialsUnchanged(ctx context.Context, id int64, expectedCredentials map[string]any, expectedProxyID *int64, errorMsg string) (bool, error)
  46. SetGrokOAuthRefreshTempUnschedulableIfCredentialsUnchanged(ctx context.Context, id int64, expectedCredentials map[string]any, expectedProxyID *int64, until time.Time, reason string) (bool, error)
  47. }
  48. // TokenRefreshService OAuth token自动刷新服务
  49. // 定期检查并刷新即将过期的token
  50. type TokenRefreshService struct {
  51. accountRepo AccountRepository
  52. candidatePager OAuthRefreshCandidatePager
  53. registrations []tokenRefreshRegistration
  54. refreshPolicy BackgroundRefreshPolicy
  55. cfg *config.TokenRefreshConfig
  56. cacheInvalidator TokenCacheInvalidator
  57. schedulerCache SchedulerCache // 用于同步更新调度器缓存,解决 token 刷新后缓存不一致问题
  58. tempUnschedCache TempUnschedCache // 用于清除 Redis 中的临时不可调度缓存
  59. refreshAPI *OAuthRefreshAPI // 统一刷新 API
  60. runtimeBlocker AccountRuntimeBlocker
  61. // OpenAI privacy: 刷新成功后检查并设置 training opt-out
  62. privacyClientFactory PrivacyClientFactory
  63. proxyRepo ProxyRepository
  64. stopCh chan struct{}
  65. stopOnce sync.Once
  66. wg sync.WaitGroup
  67. runCtx context.Context
  68. runCancel context.CancelFunc
  69. candidateMu sync.Mutex
  70. afterID int64
  71. providerMu sync.Mutex
  72. providerGates map[string]*tokenRefreshRateGate
  73. providerPools map[string]*tokenRefreshConcurrencyGate
  74. // Test-only duration seam; production uses TokenRefreshConfig seconds.
  75. attemptTimeoutOverride time.Duration
  76. }
  77. // NewTokenRefreshService 创建token刷新服务
  78. func NewTokenRefreshService(
  79. accountRepo AccountRepository,
  80. oauthService *OAuthService,
  81. openaiOAuthService *OpenAIOAuthService,
  82. geminiOAuthService *GeminiOAuthService,
  83. antigravityOAuthService *AntigravityOAuthService,
  84. cacheInvalidator TokenCacheInvalidator,
  85. schedulerCache SchedulerCache,
  86. cfg *config.Config,
  87. tempUnschedCache TempUnschedCache,
  88. grokOAuthServices ...*GrokOAuthService,
  89. ) *TokenRefreshService {
  90. refreshCfg := &config.TokenRefreshConfig{}
  91. if cfg != nil {
  92. refreshCfg = &cfg.TokenRefresh
  93. }
  94. runCtx, runCancel := context.WithCancel(context.Background())
  95. s := &TokenRefreshService{
  96. accountRepo: accountRepo,
  97. refreshPolicy: DefaultBackgroundRefreshPolicy(),
  98. cfg: refreshCfg,
  99. cacheInvalidator: cacheInvalidator,
  100. schedulerCache: schedulerCache,
  101. tempUnschedCache: tempUnschedCache,
  102. stopCh: make(chan struct{}),
  103. runCtx: runCtx,
  104. runCancel: runCancel,
  105. }
  106. if pager, ok := accountRepo.(OAuthRefreshCandidatePager); ok {
  107. s.candidatePager = pager
  108. }
  109. openAIRefresher := NewOpenAITokenRefresher(openaiOAuthService, accountRepo)
  110. claudeRefresher := NewClaudeTokenRefresher(oauthService)
  111. geminiRefresher := NewGeminiTokenRefresher(geminiOAuthService)
  112. agRefresher := NewAntigravityTokenRefresher(antigravityOAuthService)
  113. var grokOAuthService *GrokOAuthService
  114. if len(grokOAuthServices) > 0 {
  115. grokOAuthService = grokOAuthServices[0]
  116. }
  117. grokRefresher := NewGrokTokenRefresher(grokOAuthService)
  118. // Each provider is registered exactly once. The same registry supplies both
  119. // execution and repository eligibility, preventing future platform drift.
  120. s.registrations = []tokenRefreshRegistration{
  121. {platform: PlatformAnthropic, refresher: claudeRefresher, executor: claudeRefresher},
  122. {platform: PlatformOpenAI, refresher: openAIRefresher, executor: openAIRefresher},
  123. {platform: PlatformGemini, refresher: geminiRefresher, executor: geminiRefresher},
  124. {platform: PlatformAntigravity, refresher: agRefresher, executor: agRefresher},
  125. {platform: PlatformGrok, refresher: grokRefresher, executor: grokRefresher},
  126. }
  127. return s
  128. }
  129. func (s *TokenRefreshService) eligiblePlatforms() []string {
  130. platforms := make([]string, 0, len(s.registrations))
  131. for _, registration := range s.registrations {
  132. if registration.platform != "" && registration.refresher != nil {
  133. platforms = append(platforms, registration.platform)
  134. }
  135. }
  136. return platforms
  137. }
  138. func (s *TokenRefreshService) candidateAfterID() int64 {
  139. s.candidateMu.Lock()
  140. defer s.candidateMu.Unlock()
  141. return s.afterID
  142. }
  143. func (s *TokenRefreshService) setCandidateAfterID(afterID int64) {
  144. s.candidateMu.Lock()
  145. s.afterID = afterID
  146. s.candidateMu.Unlock()
  147. }
  148. // SetPrivacyDeps 注入 OpenAI privacy opt-out 所需依赖
  149. func (s *TokenRefreshService) SetPrivacyDeps(factory PrivacyClientFactory, proxyRepo ProxyRepository) {
  150. s.privacyClientFactory = factory
  151. s.proxyRepo = proxyRepo
  152. }
  153. // SetRefreshAPI 注入统一的 OAuth 刷新 API
  154. func (s *TokenRefreshService) SetRefreshAPI(api *OAuthRefreshAPI) {
  155. s.refreshAPI = api
  156. }
  157. // SetRefreshPolicy 注入后台刷新调用侧策略(用于显式化平台/场景差异行为)。
  158. func (s *TokenRefreshService) SetRefreshPolicy(policy BackgroundRefreshPolicy) {
  159. s.refreshPolicy = policy
  160. }
  161. func (s *TokenRefreshService) SetAccountRuntimeBlocker(blocker AccountRuntimeBlocker) {
  162. s.runtimeBlocker = blocker
  163. }
  164. func (s *TokenRefreshService) notifyAccountSchedulingBlocked(account *Account, until time.Time, reason string) {
  165. if s == nil || s.runtimeBlocker == nil || account == nil {
  166. return
  167. }
  168. s.runtimeBlocker.BlockAccountScheduling(account, until, reason)
  169. }
  170. func (s *TokenRefreshService) notifyAccountSchedulingBlockCleared(accountID int64) {
  171. if s == nil || s.runtimeBlocker == nil || accountID <= 0 {
  172. return
  173. }
  174. s.runtimeBlocker.ClearAccountSchedulingBlock(accountID)
  175. }
  176. // Start 启动后台刷新服务
  177. func (s *TokenRefreshService) Start() {
  178. if s.cfg == nil || !s.cfg.Enabled {
  179. slog.Info("token_refresh.service_disabled")
  180. return
  181. }
  182. s.wg.Add(1)
  183. go s.refreshLoop()
  184. slog.Info("token_refresh.service_started",
  185. "check_interval_minutes", s.cfg.CheckIntervalMinutes,
  186. "refresh_before_expiry_hours", s.cfg.RefreshBeforeExpiryHours,
  187. )
  188. }
  189. // Stop 停止刷新服务(可安全多次调用)
  190. func (s *TokenRefreshService) Stop() {
  191. s.stopOnce.Do(func() {
  192. if s.runCancel != nil {
  193. s.runCancel()
  194. }
  195. close(s.stopCh)
  196. })
  197. s.wg.Wait()
  198. slog.Info("token_refresh.service_stopped")
  199. }
  200. // refreshLoop 刷新循环
  201. func (s *TokenRefreshService) refreshLoop() {
  202. defer s.wg.Done()
  203. ctx := s.runCtx
  204. if ctx == nil {
  205. ctx = context.Background()
  206. }
  207. // 计算检查间隔
  208. checkInterval := time.Duration(s.cfg.CheckIntervalMinutes) * time.Minute
  209. if checkInterval < time.Minute {
  210. checkInterval = 5 * time.Minute
  211. }
  212. ticker := time.NewTicker(checkInterval)
  213. defer ticker.Stop()
  214. // 启动时立即执行一次检查
  215. s.processRefreshContext(ctx)
  216. for {
  217. select {
  218. case <-ticker.C:
  219. s.processRefreshContext(ctx)
  220. case <-ctx.Done():
  221. return
  222. case <-s.stopCh:
  223. return
  224. }
  225. }
  226. }
  227. type tokenRefreshPageStats struct {
  228. total int
  229. oauth int
  230. needsRefresh int
  231. refreshed int
  232. skipped int
  233. failed int
  234. }
  235. type tokenRefreshProviderState struct {
  236. service *TokenRefreshService
  237. registration tokenRefreshRegistration
  238. rateGate refreshAttemptGate
  239. poolGate *tokenRefreshConcurrencyGate
  240. mu sync.Mutex
  241. consecutiveFailures int
  242. tripped bool
  243. }
  244. type tokenRefreshRateGate struct {
  245. mu sync.Mutex
  246. next time.Time
  247. interval time.Duration
  248. }
  249. type tokenRefreshConcurrencyGate struct {
  250. slots chan struct{}
  251. }
  252. type refreshAttemptGate interface {
  253. acquire(ctx context.Context) (release func(), err error)
  254. }
  255. type providerRefreshAttemptGate interface {
  256. refreshAttemptGate
  257. acquireRate(ctx context.Context) (release func(), err error)
  258. }
  259. type rateLimitedOAuthRefreshExecutor struct {
  260. OAuthRefreshExecutor
  261. acquireRate func(context.Context) (func(), error)
  262. }
  263. func (e *rateLimitedOAuthRefreshExecutor) Refresh(ctx context.Context, account *Account) (map[string]any, error) {
  264. if e == nil || e.OAuthRefreshExecutor == nil {
  265. return nil, errors.New("OAuth refresh executor is not configured")
  266. }
  267. release := func() {}
  268. if e.acquireRate != nil {
  269. var err error
  270. release, err = e.acquireRate(ctx)
  271. if err != nil {
  272. return nil, err
  273. }
  274. }
  275. defer release()
  276. return e.OAuthRefreshExecutor.Refresh(ctx, account)
  277. }
  278. func newTokenRefreshRateGate(qps int) *tokenRefreshRateGate {
  279. if qps <= 0 {
  280. return &tokenRefreshRateGate{}
  281. }
  282. return newTokenRefreshRateGateWithInterval(time.Second / time.Duration(qps))
  283. }
  284. // newTokenRefreshRateGateWithInterval is a narrow duration seam used to test
  285. // slot reservation and cancellation without waiting on production-scale QPS.
  286. func newTokenRefreshRateGateWithInterval(interval time.Duration) *tokenRefreshRateGate {
  287. return &tokenRefreshRateGate{interval: interval}
  288. }
  289. func (g *tokenRefreshRateGate) reserveSlot(now time.Time) time.Time {
  290. g.mu.Lock()
  291. defer g.mu.Unlock()
  292. if g.next.Before(now) {
  293. g.next = now
  294. }
  295. slot := g.next
  296. g.next = g.next.Add(g.interval)
  297. return slot
  298. }
  299. func (g *tokenRefreshRateGate) wait(ctx context.Context) error {
  300. if g == nil || g.interval <= 0 {
  301. return nil
  302. }
  303. slot := g.reserveSlot(time.Now())
  304. wait := time.Until(slot)
  305. if wait <= 0 {
  306. return nil
  307. }
  308. timer := time.NewTimer(wait)
  309. defer timer.Stop()
  310. select {
  311. case <-ctx.Done():
  312. return ctx.Err()
  313. case <-timer.C:
  314. return nil
  315. }
  316. }
  317. func (g *tokenRefreshRateGate) acquire(ctx context.Context) (func(), error) {
  318. if err := g.wait(ctx); err != nil {
  319. return nil, err
  320. }
  321. return func() {}, nil
  322. }
  323. func newTokenRefreshConcurrencyGate(concurrency int) *tokenRefreshConcurrencyGate {
  324. if concurrency < 1 {
  325. concurrency = 1
  326. }
  327. return &tokenRefreshConcurrencyGate{slots: make(chan struct{}, concurrency)}
  328. }
  329. func (g *tokenRefreshConcurrencyGate) acquire(ctx context.Context) (func(), error) {
  330. if g == nil {
  331. return func() {}, nil
  332. }
  333. select {
  334. case g.slots <- struct{}{}:
  335. return func() { <-g.slots }, nil
  336. case <-ctx.Done():
  337. return nil, ctx.Err()
  338. }
  339. }
  340. func (p *tokenRefreshProviderState) isTripped() bool {
  341. p.mu.Lock()
  342. defer p.mu.Unlock()
  343. return p.tripped
  344. }
  345. func (p *tokenRefreshProviderState) acquire(ctx context.Context) (func(), error) {
  346. if p == nil || p.isTripped() {
  347. return nil, errRefreshSkipped
  348. }
  349. release, err := p.poolGate.acquire(ctx)
  350. if err != nil {
  351. return nil, err
  352. }
  353. if p.isTripped() {
  354. release()
  355. return nil, errRefreshSkipped
  356. }
  357. return release, nil
  358. }
  359. func (p *tokenRefreshProviderState) acquireRate(ctx context.Context) (func(), error) {
  360. if p == nil || p.isTripped() {
  361. return nil, errRefreshSkipped
  362. }
  363. release := func() {}
  364. if p.rateGate != nil {
  365. var err error
  366. release, err = p.rateGate.acquire(ctx)
  367. if err != nil {
  368. return nil, err
  369. }
  370. }
  371. if p.isTripped() {
  372. release()
  373. return nil, errRefreshSkipped
  374. }
  375. return release, nil
  376. }
  377. func (p *tokenRefreshProviderState) recordResult(err error) {
  378. p.mu.Lock()
  379. defer p.mu.Unlock()
  380. if err == nil {
  381. p.consecutiveFailures = 0
  382. return
  383. }
  384. if errors.Is(err, errRefreshSkipped) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
  385. var attemptTimeoutErr *refreshAttemptTimeoutError
  386. if !errors.As(err, &attemptTimeoutErr) {
  387. return
  388. }
  389. }
  390. var attemptTimeoutErr *refreshAttemptTimeoutError
  391. if errors.As(err, &attemptTimeoutErr) {
  392. p.consecutiveFailures++
  393. if p.consecutiveFailures >= p.service.providerFailureThreshold() {
  394. p.tripped = true
  395. }
  396. return
  397. }
  398. var providerErr *providerConfigurationRefreshError
  399. if errors.As(err, &providerErr) {
  400. p.tripped = true
  401. return
  402. }
  403. var containmentErr *providerCycleContainmentRefreshError
  404. if errors.As(err, &containmentErr) {
  405. p.tripped = true
  406. return
  407. }
  408. var permanentErr *accountPermanentRefreshError
  409. if errors.As(err, &permanentErr) {
  410. p.consecutiveFailures = 0
  411. return
  412. }
  413. if isNonRetryableRefreshError(err) {
  414. // A permanent account credential failure is isolated to that account and
  415. // does not imply the provider is unhealthy.
  416. p.consecutiveFailures = 0
  417. return
  418. }
  419. p.consecutiveFailures++
  420. if p.consecutiveFailures >= p.service.providerFailureThreshold() {
  421. p.tripped = true
  422. }
  423. }
  424. // processRefresh preserves the existing test/internal call surface while the
  425. // production loop supplies a cancelable parent context.
  426. func (s *TokenRefreshService) processRefresh() {
  427. s.processRefreshContext(context.Background())
  428. }
  429. // processRefreshContext executes one bounded, cursor-resumable refresh cycle.
  430. func (s *TokenRefreshService) processRefreshContext(parent context.Context) {
  431. if parent == nil {
  432. parent = context.Background()
  433. }
  434. ctx, cancel := context.WithTimeout(parent, s.cycleTimeout())
  435. defer cancel()
  436. pager := s.candidatePager
  437. if pager == nil {
  438. pager, _ = s.accountRepo.(OAuthRefreshCandidatePager)
  439. }
  440. if pager == nil {
  441. slog.Error("token_refresh.candidate_pager_missing")
  442. return
  443. }
  444. platforms := s.eligiblePlatforms()
  445. if len(platforms) == 0 {
  446. slog.Error("token_refresh.provider_registry_empty")
  447. return
  448. }
  449. refreshWindow := time.Duration(s.cfg.RefreshBeforeExpiryHours * float64(time.Hour))
  450. pageSize := s.candidatePageSize()
  451. providerStates := make(map[string]*tokenRefreshProviderState, len(s.registrations))
  452. for i := range s.registrations {
  453. registration := s.registrations[i]
  454. providerStates[registration.platform] = &tokenRefreshProviderState{
  455. service: s,
  456. registration: registration,
  457. rateGate: s.providerRateGate(registration.platform),
  458. poolGate: s.providerConcurrencyGate(registration.platform),
  459. }
  460. }
  461. stats := tokenRefreshPageStats{}
  462. afterID := s.candidateAfterID()
  463. for {
  464. if ctx.Err() != nil {
  465. slog.Warn("token_refresh.cycle_stopped", "error", ctx.Err(), "resume_after_id", afterID)
  466. break
  467. }
  468. page, err := pager.ListOAuthRefreshCandidatePage(ctx, OAuthRefreshPageOptions{
  469. Platforms: platforms,
  470. AfterID: afterID,
  471. Limit: pageSize,
  472. ActiveOnly: true,
  473. IncludeSetupToken: true,
  474. RequireRefreshToken: true,
  475. ExcludeRetryCooldown: true,
  476. })
  477. if err != nil {
  478. slog.Error("token_refresh.list_accounts_failed", "error", err, "after_id", afterID)
  479. break
  480. }
  481. if page == nil {
  482. slog.Error("token_refresh.nil_candidate_page", "after_id", afterID)
  483. break
  484. }
  485. accounts := page.Accounts
  486. if !page.HasMore && page.NextAfterID == 0 && len(accounts) == 0 {
  487. s.setCandidateAfterID(0)
  488. break
  489. }
  490. if page.NextAfterID <= afterID {
  491. slog.Error("token_refresh.invalid_candidate_page_metadata", "after_id", afterID)
  492. break
  493. }
  494. if !isStrictlyIncreasingAccountPage(accounts, afterID) {
  495. slog.Error("token_refresh.invalid_candidate_page", "after_id", afterID, "count", len(accounts))
  496. break
  497. }
  498. pageStats := s.processCandidatePage(ctx, accounts, providerStates, refreshWindow)
  499. stats.total += pageStats.total
  500. stats.oauth += pageStats.oauth
  501. stats.needsRefresh += pageStats.needsRefresh
  502. stats.refreshed += pageStats.refreshed
  503. stats.skipped += pageStats.skipped
  504. stats.failed += pageStats.failed
  505. // Never advance past a partially processed page. Re-reading a page is
  506. // safe because OAuthRefreshAPI re-reads DB state and checks expiry again.
  507. if ctx.Err() != nil {
  508. break
  509. }
  510. afterID = page.NextAfterID
  511. s.setCandidateAfterID(afterID)
  512. if !page.HasMore {
  513. s.setCandidateAfterID(0)
  514. break
  515. }
  516. }
  517. if stats.needsRefresh == 0 && stats.failed == 0 {
  518. slog.Debug("token_refresh.cycle_completed",
  519. "total", stats.total, "oauth", stats.oauth,
  520. "needs_refresh", stats.needsRefresh, "refreshed", stats.refreshed,
  521. "skipped", stats.skipped, "failed", stats.failed)
  522. } else {
  523. slog.Info("token_refresh.cycle_completed",
  524. "total", stats.total, "oauth", stats.oauth,
  525. "needs_refresh", stats.needsRefresh, "refreshed", stats.refreshed,
  526. "skipped", stats.skipped, "failed", stats.failed)
  527. }
  528. }
  529. func isStrictlyIncreasingAccountPage(accounts []Account, afterID int64) bool {
  530. previous := afterID
  531. for i := range accounts {
  532. if accounts[i].ID <= previous {
  533. return false
  534. }
  535. previous = accounts[i].ID
  536. }
  537. return true
  538. }
  539. func (s *TokenRefreshService) processCandidatePage(
  540. ctx context.Context,
  541. accounts []Account,
  542. providerStates map[string]*tokenRefreshProviderState,
  543. refreshWindow time.Duration,
  544. ) tokenRefreshPageStats {
  545. stats := tokenRefreshPageStats{total: len(accounts)}
  546. groups := make(map[string][]*Account)
  547. for i := range accounts {
  548. account := &accounts[i]
  549. state := providerStates[account.Platform]
  550. if state == nil || state.registration.refresher == nil || !state.registration.refresher.CanRefresh(account) {
  551. continue
  552. }
  553. stats.oauth++
  554. if !state.registration.refresher.NeedsRefresh(account, refreshWindow) {
  555. continue
  556. }
  557. stats.needsRefresh++
  558. groups[account.Platform] = append(groups[account.Platform], account)
  559. }
  560. type providerResult struct {
  561. refreshed int
  562. skipped int
  563. failed int
  564. }
  565. results := make(chan providerResult, len(groups))
  566. var wg sync.WaitGroup
  567. for platform, group := range groups {
  568. state := providerStates[platform]
  569. wg.Add(1)
  570. go func() {
  571. defer wg.Done()
  572. refreshed, skipped, failed := s.processProviderAccounts(ctx, state, group, refreshWindow)
  573. results <- providerResult{refreshed: refreshed, skipped: skipped, failed: failed}
  574. }()
  575. }
  576. wg.Wait()
  577. close(results)
  578. for result := range results {
  579. stats.refreshed += result.refreshed
  580. stats.skipped += result.skipped
  581. stats.failed += result.failed
  582. }
  583. return stats
  584. }
  585. func (s *TokenRefreshService) processProviderAccounts(
  586. ctx context.Context,
  587. state *tokenRefreshProviderState,
  588. accounts []*Account,
  589. refreshWindow time.Duration,
  590. ) (refreshed, skipped, failed int) {
  591. if state == nil || len(accounts) == 0 {
  592. return 0, 0, 0
  593. }
  594. type refreshResult struct {
  595. accountID int64
  596. err error
  597. }
  598. jobs := make(chan *Account, len(accounts))
  599. results := make(chan refreshResult, len(accounts))
  600. workerCount := s.providerConcurrency()
  601. if workerCount > len(accounts) {
  602. workerCount = len(accounts)
  603. }
  604. var wg sync.WaitGroup
  605. for i := 0; i < workerCount; i++ {
  606. wg.Add(1)
  607. go func() {
  608. defer wg.Done()
  609. for account := range jobs {
  610. if ctx.Err() != nil || state.isTripped() {
  611. results <- refreshResult{accountID: account.ID, err: errRefreshSkipped}
  612. continue
  613. }
  614. if state.isTripped() {
  615. results <- refreshResult{accountID: account.ID, err: errRefreshSkipped}
  616. continue
  617. }
  618. err := s.refreshWithRetryWithRateGate(ctx, account, state.registration.refresher, state.registration.executor, refreshWindow, state)
  619. state.recordResult(err)
  620. results <- refreshResult{accountID: account.ID, err: err}
  621. }
  622. }()
  623. }
  624. for _, account := range accounts {
  625. jobs <- account
  626. }
  627. close(jobs)
  628. wg.Wait()
  629. close(results)
  630. for result := range results {
  631. switch {
  632. case result.err == nil:
  633. refreshed++
  634. slog.Info("token_refresh.account_refreshed", "account_id", result.accountID, "platform", state.registration.platform)
  635. case errors.Is(result.err, errRefreshSkipped):
  636. skipped++
  637. default:
  638. failed++
  639. slog.Warn("token_refresh.account_refresh_failed", "account_id", result.accountID, "platform", state.registration.platform, "error", logredact.RedactText(result.err.Error()))
  640. }
  641. }
  642. return refreshed, skipped, failed
  643. }
  644. func (s *TokenRefreshService) candidatePageSize() int {
  645. if s.cfg != nil && s.cfg.CandidatePageSize > 0 {
  646. return min(s.cfg.CandidatePageSize, maxTokenRefreshCandidatePageSize)
  647. }
  648. return defaultTokenRefreshCandidatePageSize
  649. }
  650. func (s *TokenRefreshService) providerConcurrency() int {
  651. if s.cfg != nil && s.cfg.ProviderConcurrency > 0 {
  652. return min(s.cfg.ProviderConcurrency, maxTokenRefreshProviderConcurrency)
  653. }
  654. return defaultTokenRefreshProviderConcurrency
  655. }
  656. func (s *TokenRefreshService) providerQPS() int {
  657. if s.cfg != nil && s.cfg.ProviderQPS > 0 {
  658. return min(s.cfg.ProviderQPS, maxTokenRefreshProviderQPS)
  659. }
  660. return defaultTokenRefreshProviderQPS
  661. }
  662. // providerRateGate returns the process-local limiter shared by background
  663. // cycles and admin reconciliation. Sharing it prevents concurrent entry points
  664. // or retries from multiplying the configured per-provider request rate.
  665. func (s *TokenRefreshService) providerRateGate(platform string) *tokenRefreshRateGate {
  666. s.providerMu.Lock()
  667. defer s.providerMu.Unlock()
  668. if s.providerGates == nil {
  669. s.providerGates = make(map[string]*tokenRefreshRateGate)
  670. }
  671. if gate := s.providerGates[platform]; gate != nil {
  672. return gate
  673. }
  674. gate := newTokenRefreshRateGate(s.providerQPS())
  675. s.providerGates[platform] = gate
  676. return gate
  677. }
  678. // providerConcurrencyGate returns the process-local semaphore shared by every
  679. // background cycle and admin reconciliation call for a provider. It is
  680. // acquired and released around each upstream retry attempt, so parallel entry
  681. // points cannot multiply ProviderConcurrency.
  682. func (s *TokenRefreshService) providerConcurrencyGate(platform string) *tokenRefreshConcurrencyGate {
  683. s.providerMu.Lock()
  684. defer s.providerMu.Unlock()
  685. if s.providerPools == nil {
  686. s.providerPools = make(map[string]*tokenRefreshConcurrencyGate)
  687. }
  688. if gate := s.providerPools[platform]; gate != nil {
  689. return gate
  690. }
  691. gate := newTokenRefreshConcurrencyGate(s.providerConcurrency())
  692. s.providerPools[platform] = gate
  693. return gate
  694. }
  695. func (s *TokenRefreshService) providerFailureThreshold() int {
  696. if s.cfg != nil && s.cfg.ProviderFailureThreshold > 0 {
  697. return min(s.cfg.ProviderFailureThreshold, maxTokenRefreshProviderFailureThreshold)
  698. }
  699. return defaultTokenRefreshProviderFailureThreshold
  700. }
  701. func (s *TokenRefreshService) attemptTimeout() time.Duration {
  702. timeout := defaultTokenRefreshAttemptTimeout
  703. if s.attemptTimeoutOverride > 0 {
  704. timeout = s.attemptTimeoutOverride
  705. } else if s.cfg != nil && s.cfg.AttemptTimeoutSeconds > 0 {
  706. seconds := min(s.cfg.AttemptTimeoutSeconds, int(maxTokenRefreshAttemptTimeout/time.Second))
  707. timeout = time.Duration(seconds) * time.Second
  708. }
  709. if s.refreshAPI != nil && s.refreshAPI.tokenCache != nil {
  710. timeout = clampRefreshAttemptToLockLease(timeout, s.refreshAPI.lockTTL)
  711. }
  712. return timeout
  713. }
  714. func clampRefreshAttemptToLockLease(timeout, lease time.Duration) time.Duration {
  715. if timeout <= 0 || lease <= 0 {
  716. return timeout
  717. }
  718. margin := lease / 10
  719. if margin > maxTokenRefreshLockSafetyMargin {
  720. margin = maxTokenRefreshLockSafetyMargin
  721. }
  722. if margin <= 0 {
  723. margin = time.Nanosecond
  724. }
  725. leaseBudget := lease - margin
  726. if leaseBudget <= 0 {
  727. leaseBudget = lease / 2
  728. }
  729. if leaseBudget > 0 && timeout > leaseBudget {
  730. return leaseBudget
  731. }
  732. return timeout
  733. }
  734. func (s *TokenRefreshService) cycleTimeout() time.Duration {
  735. if s.cfg != nil && s.cfg.CycleTimeoutSeconds > 0 {
  736. seconds := min(s.cfg.CycleTimeoutSeconds, int(maxTokenRefreshCycleTimeout/time.Second))
  737. return time.Duration(seconds) * time.Second
  738. }
  739. return defaultTokenRefreshCycleTimeout
  740. }
  741. func (s *TokenRefreshService) maxRetries() int {
  742. if s.cfg != nil && s.cfg.MaxRetries > 0 {
  743. return min(s.cfg.MaxRetries, maxTokenRefreshMaxRetries)
  744. }
  745. return defaultTokenRefreshMaxRetries
  746. }
  747. // refreshWithRetry 带重试的刷新
  748. func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Account, refresher TokenRefresher, executor OAuthRefreshExecutor, refreshWindow time.Duration) error {
  749. return s.refreshWithRetryWithRateGate(ctx, account, refresher, executor, refreshWindow, nil)
  750. }
  751. func (s *TokenRefreshService) refreshWithRetryWithRateGate(
  752. ctx context.Context,
  753. account *Account,
  754. refresher TokenRefresher,
  755. executor OAuthRefreshExecutor,
  756. refreshWindow time.Duration,
  757. gate refreshAttemptGate,
  758. ) error {
  759. var lastErr error
  760. maxRetries := s.maxRetries()
  761. for attempt := 1; attempt <= maxRetries; attempt++ {
  762. if err := ctx.Err(); err != nil {
  763. return err
  764. }
  765. releaseAttempt := func() {}
  766. var acquireRate func(context.Context) (func(), error)
  767. if gate != nil {
  768. if providerGate, ok := gate.(providerRefreshAttemptGate); ok {
  769. var err error
  770. releaseAttempt, err = providerGate.acquire(ctx)
  771. if err != nil {
  772. return err
  773. }
  774. acquireRate = providerGate.acquireRate
  775. } else {
  776. // Compatibility gates are rate-admission gates. Acquire them only
  777. // when an upstream Refresh call is actually about to start.
  778. acquireRate = gate.acquire
  779. }
  780. }
  781. attemptCtx, cancelAttempt := context.WithTimeout(ctx, s.attemptTimeout())
  782. var newCredentials map[string]any
  783. var err error
  784. shortCircuit := false
  785. credentialsPersisted := false
  786. // 优先使用统一 API(带分布式锁 + DB 重读保护)
  787. if s.refreshAPI != nil && executor != nil {
  788. actualExecutor := executor
  789. if acquireRate != nil {
  790. actualExecutor = &rateLimitedOAuthRefreshExecutor{
  791. OAuthRefreshExecutor: executor,
  792. acquireRate: acquireRate,
  793. }
  794. }
  795. result, refreshErr := s.refreshAPI.RefreshIfNeeded(attemptCtx, account, actualExecutor, refreshWindow)
  796. if result != nil && result.Account != nil {
  797. account = result.Account
  798. }
  799. if refreshErr != nil {
  800. err = refreshErr
  801. } else if result.LockHeld {
  802. // 锁被其他 worker 持有,由调用侧策略决定如何计数
  803. err = s.refreshPolicy.handleLockHeld()
  804. shortCircuit = true
  805. } else if !result.Refreshed {
  806. // 已被其他路径刷新,由调用侧策略决定如何计数
  807. err = s.refreshPolicy.handleAlreadyRefreshed()
  808. shortCircuit = true
  809. } else {
  810. credentialsPersisted = result.NewCredentials != nil
  811. _ = result.NewCredentials // 统一 API 已设置 _token_version 并更新 DB,无需重复操作
  812. }
  813. } else {
  814. // 降级:直接调用 refresher(兼容旧路径)
  815. releaseRate := func() {}
  816. if acquireRate != nil {
  817. releaseRate, err = acquireRate(attemptCtx)
  818. }
  819. if err == nil {
  820. newCredentials, err = refresher.Refresh(attemptCtx, account)
  821. }
  822. if releaseRate != nil {
  823. releaseRate()
  824. }
  825. attemptTimedOut := errors.Is(attemptCtx.Err(), context.DeadlineExceeded) && ctx.Err() == nil
  826. if err == nil && newCredentials != nil && !attemptTimedOut {
  827. newCredentials["_token_version"] = time.Now().UnixMilli()
  828. if saveErr := persistAccountCredentials(attemptCtx, s.accountRepo, account, newCredentials); saveErr != nil {
  829. err = fmt.Errorf("failed to save credentials: %w", saveErr)
  830. } else {
  831. credentialsPersisted = true
  832. }
  833. }
  834. }
  835. attemptTimedOut := errors.Is(attemptCtx.Err(), context.DeadlineExceeded) && ctx.Err() == nil
  836. cancelAttempt()
  837. releaseAttempt()
  838. persistedAfterAttemptDeadline := attemptTimedOut && credentialsPersisted && err == nil
  839. if attemptTimedOut && !persistedAfterAttemptDeadline && !isProviderScopedTerminalRefreshError(err) {
  840. cause := err
  841. if cause == nil {
  842. cause = context.DeadlineExceeded
  843. }
  844. err = &refreshAttemptTimeoutError{err: cause}
  845. shortCircuit = false
  846. if credentialsPersisted {
  847. s.postRefreshStateSyncWithCleanup(ctx, account)
  848. }
  849. }
  850. if shortCircuit {
  851. return err
  852. }
  853. if err == nil {
  854. if ctxErr := ctx.Err(); ctxErr != nil {
  855. if credentialsPersisted {
  856. s.postRefreshStateSyncWithCleanup(ctx, account)
  857. }
  858. return ctxErr
  859. }
  860. if persistedAfterAttemptDeadline {
  861. // The provider result and exact-state CAS are already durable. Only
  862. // the internal attempt budget elapsed while bounded detached cleanup
  863. // completed; do not convert that success into retry/cooldown/breaker
  864. // evidence. Publish cache state with a fresh cleanup context and stop.
  865. s.postRefreshStateSyncWithCleanup(ctx, account)
  866. return nil
  867. }
  868. s.postRefreshActions(ctx, account)
  869. return nil
  870. }
  871. if ctxErr := ctx.Err(); ctxErr != nil {
  872. if credentialsPersisted {
  873. s.postRefreshStateSyncWithCleanup(ctx, account)
  874. }
  875. return ctxErr
  876. }
  877. if errors.Is(err, errRefreshSkipped) {
  878. return errRefreshSkipped
  879. }
  880. if isProviderScopedTerminalRefreshError(err) {
  881. return err
  882. }
  883. var stateUnavailableErr *oauthRefreshStateUnavailableError
  884. if errors.As(err, &stateUnavailableErr) {
  885. return &providerCycleContainmentRefreshError{err: err}
  886. }
  887. if isAmbiguousGrokEntitlementRefreshError(account, err) {
  888. // The current Grok client labels every token-endpoint 403 as an
  889. // entitlement denial. Without explicit entitlement evidence, contain
  890. // the provider for this cycle instead of disabling an account on a
  891. // possible WAF or shared provider failure.
  892. return &providerCycleContainmentRefreshError{err: err}
  893. }
  894. // Provider-wide OAuth client/scope failures are not evidence that every
  895. // account is invalid. Return a typed internal signal so the cycle contains
  896. // the provider without mutating account state.
  897. if isSharedProviderRefreshError(err) {
  898. return &providerConfigurationRefreshError{err: err}
  899. }
  900. // 不可重试错误(invalid_grant/invalid_client 等)直接标记 error 状态并返回
  901. if isNonRetryableRefreshError(err) {
  902. errorMsg := "Token refresh failed (non-retryable): " + logredact.RedactText(err.Error())
  903. isGrokOAuth := account.IsGrokOAuth()
  904. if !isGrokOAuth {
  905. s.notifyAccountSchedulingBlocked(account, time.Time{}, "token_refresh_non_retryable")
  906. }
  907. s.clearAntigravityForceTokenRefresh(ctx, account, "non_retryable")
  908. persistentlyBlocked := false
  909. var setErr error
  910. if isGrokOAuth {
  911. conditionalRepo, ok := s.accountRepo.(GrokOAuthRefreshMutationRepository)
  912. if !ok {
  913. return &providerConfigurationRefreshError{
  914. err: errors.New("grok OAuth conditional refresh mutation repository is not configured"),
  915. }
  916. } else {
  917. persistentlyBlocked, setErr = conditionalRepo.SetGrokOAuthRefreshErrorIfCredentialsUnchanged(
  918. ctx,
  919. account.ID,
  920. account.Credentials,
  921. account.ProxyID,
  922. errorMsg,
  923. )
  924. if setErr == nil && !persistentlyBlocked {
  925. slog.Info("token_refresh.grok_error_status_skipped_stale_credentials", "account_id", account.ID)
  926. return errRefreshSkipped
  927. }
  928. }
  929. } else {
  930. setErr = s.accountRepo.SetError(ctx, account.ID, errorMsg)
  931. persistentlyBlocked = setErr == nil
  932. }
  933. if setErr != nil {
  934. slog.Error("token_refresh.set_error_status_failed",
  935. "account_id", account.ID,
  936. "error", setErr,
  937. )
  938. if isGrokOAuth {
  939. return &providerCycleContainmentRefreshError{
  940. err: fmt.Errorf("failed to conditionally persist Grok OAuth refresh failure: %w", setErr),
  941. }
  942. }
  943. } else if isGrokOAuth && persistentlyBlocked {
  944. s.notifyAccountSchedulingBlocked(account, time.Time{}, "token_refresh_non_retryable")
  945. }
  946. cacheInvalidationFailed := false
  947. if account.Type == AccountTypeOAuth && (!isGrokOAuth || persistentlyBlocked) {
  948. if s.cacheInvalidator == nil {
  949. cacheInvalidationFailed = true
  950. } else if invalidateErr := s.cacheInvalidator.InvalidateToken(ctx, account); invalidateErr != nil {
  951. cacheInvalidationFailed = true
  952. slog.Warn("token_refresh.invalidate_failed_token_cache_failed",
  953. "account_id", account.ID,
  954. "error", logredact.RedactText(invalidateErr.Error()),
  955. )
  956. }
  957. }
  958. return &accountPermanentRefreshError{
  959. err: err,
  960. persistentlyBlocked: persistentlyBlocked,
  961. cacheInvalidationFailed: cacheInvalidationFailed,
  962. }
  963. }
  964. lastErr = err
  965. slog.Warn("token_refresh.retry_attempt_failed",
  966. "account_id", account.ID,
  967. "attempt", attempt,
  968. "max_retries", maxRetries,
  969. "error", logredact.RedactText(err.Error()),
  970. )
  971. // 如果还有重试机会,等待后重试
  972. if attempt < maxRetries {
  973. backoff := s.retryBackoff(account.ID, attempt)
  974. if backoff > 0 {
  975. timer := time.NewTimer(backoff)
  976. select {
  977. case <-ctx.Done():
  978. timer.Stop()
  979. return ctx.Err()
  980. case <-timer.C:
  981. }
  982. }
  983. }
  984. }
  985. if err := ctx.Err(); err != nil {
  986. return err
  987. }
  988. // 可重试错误耗尽:临时标记账号不可调度,避免请求路径反复命中已知失败的账号
  989. slog.Warn("token_refresh.retry_exhausted",
  990. "account_id", account.ID,
  991. "platform", account.Platform,
  992. "max_retries", maxRetries,
  993. "error", logredact.RedactText(lastErr.Error()),
  994. )
  995. // 设置临时不可调度 10 分钟(不标记 error,保持 status=active 让下个刷新周期能继续尝试)
  996. until := time.Now().Add(tokenRefreshTempUnschedDuration)
  997. reason := "token refresh retry exhausted"
  998. if lastErr != nil {
  999. reason += ": " + logredact.RedactText(lastErr.Error())
  1000. }
  1001. if account.IsGrokOAuth() {
  1002. conditionalRepo, ok := s.accountRepo.(GrokOAuthRefreshMutationRepository)
  1003. if !ok {
  1004. return &providerConfigurationRefreshError{
  1005. err: errors.New("grok OAuth conditional refresh mutation repository is not configured"),
  1006. }
  1007. }
  1008. applied, setErr := conditionalRepo.SetGrokOAuthRefreshTempUnschedulableIfCredentialsUnchanged(
  1009. ctx,
  1010. account.ID,
  1011. account.Credentials,
  1012. account.ProxyID,
  1013. until,
  1014. reason,
  1015. )
  1016. if setErr != nil {
  1017. slog.Warn("token_refresh.set_temp_unschedulable_failed",
  1018. "account_id", account.ID,
  1019. "error", setErr,
  1020. )
  1021. return &providerCycleContainmentRefreshError{
  1022. err: fmt.Errorf("failed to conditionally persist Grok OAuth refresh cooldown: %w", setErr),
  1023. }
  1024. } else if !applied {
  1025. slog.Info("token_refresh.grok_temp_unschedulable_skipped_stale_credentials", "account_id", account.ID)
  1026. return errRefreshSkipped
  1027. } else {
  1028. s.notifyAccountSchedulingBlocked(account, until, "token_refresh_retry_exhausted")
  1029. slog.Info("token_refresh.temp_unschedulable_set",
  1030. "account_id", account.ID,
  1031. "until", until.Format(time.RFC3339),
  1032. )
  1033. }
  1034. return lastErr
  1035. }
  1036. s.notifyAccountSchedulingBlocked(account, until, "token_refresh_retry_exhausted")
  1037. if setErr := s.accountRepo.SetTempUnschedulable(ctx, account.ID, until, reason); setErr != nil {
  1038. slog.Warn("token_refresh.set_temp_unschedulable_failed",
  1039. "account_id", account.ID,
  1040. "error", setErr,
  1041. )
  1042. } else {
  1043. slog.Info("token_refresh.temp_unschedulable_set",
  1044. "account_id", account.ID,
  1045. "until", until.Format(time.RFC3339),
  1046. )
  1047. }
  1048. return lastErr
  1049. }
  1050. func (s *TokenRefreshService) retryBackoff(accountID int64, attempt int) time.Duration {
  1051. if s.cfg == nil || s.cfg.RetryBackoffSeconds <= 0 {
  1052. return 0
  1053. }
  1054. shift := attempt - 1
  1055. if shift > 10 {
  1056. shift = 10
  1057. }
  1058. baseSeconds := min(s.cfg.RetryBackoffSeconds, int(maxTokenRefreshRetryBackoff/time.Second))
  1059. base := time.Duration(baseSeconds) * time.Second * time.Duration(1<<shift)
  1060. // Stable 75-125% jitter prevents synchronized replicas from retrying on the
  1061. // same boundaries without making tests or operations nondeterministic.
  1062. jitterPercent := int64(75) + (accountID+int64(attempt*17))%51
  1063. backoff := base * time.Duration(jitterPercent) / 100
  1064. return min(backoff, maxTokenRefreshRetryBackoff)
  1065. }
  1066. // postRefreshActions 刷新成功后的后续动作(清除错误状态、缓存失效、调度器同步等)
  1067. func (s *TokenRefreshService) postRefreshActions(ctx context.Context, account *Account) {
  1068. s.clearAntigravityForceTokenRefresh(ctx, account, "success")
  1069. // Antigravity 账户:如果之前是因为缺少 project_id 而标记为 error,现在成功获取到了,清除错误状态
  1070. if account.Platform == PlatformAntigravity &&
  1071. account.Status == StatusError &&
  1072. strings.Contains(account.ErrorMessage, "missing_project_id:") {
  1073. if clearErr := s.accountRepo.ClearError(ctx, account.ID); clearErr != nil {
  1074. slog.Warn("token_refresh.clear_account_error_failed",
  1075. "account_id", account.ID,
  1076. "error", clearErr,
  1077. )
  1078. } else {
  1079. slog.Info("token_refresh.cleared_missing_project_id_error", "account_id", account.ID)
  1080. s.notifyAccountSchedulingBlockCleared(account.ID)
  1081. }
  1082. }
  1083. // 刷新成功后清除临时不可调度状态(处理 OAuth 401 恢复场景)
  1084. if account.TempUnschedulableUntil != nil && time.Now().Before(*account.TempUnschedulableUntil) {
  1085. if clearErr := s.accountRepo.ClearTempUnschedulable(ctx, account.ID); clearErr != nil {
  1086. slog.Warn("token_refresh.clear_temp_unschedulable_failed",
  1087. "account_id", account.ID,
  1088. "error", clearErr,
  1089. )
  1090. } else {
  1091. slog.Info("token_refresh.cleared_temp_unschedulable", "account_id", account.ID)
  1092. s.notifyAccountSchedulingBlockCleared(account.ID)
  1093. }
  1094. // 同步清除 Redis 缓存,避免调度器读到过期的临时不可调度状态
  1095. if s.tempUnschedCache != nil {
  1096. if clearErr := s.tempUnschedCache.DeleteTempUnsched(ctx, account.ID); clearErr != nil {
  1097. slog.Warn("token_refresh.clear_temp_unsched_cache_failed",
  1098. "account_id", account.ID,
  1099. "error", clearErr,
  1100. )
  1101. }
  1102. }
  1103. }
  1104. s.postRefreshStateSync(ctx, account)
  1105. // OpenAI OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则尝试关闭训练数据共享
  1106. s.ensureOpenAIPrivacy(ctx, account)
  1107. // Antigravity OAuth: 刷新成功后,检查是否已设置 privacy_mode,未设置则调用 setUserSettings
  1108. s.ensureAntigravityPrivacy(ctx, account)
  1109. // Grok: clear soft reauth flag after a successful credential refresh.
  1110. if account != nil && account.Platform == PlatformGrok && accountGrokNeedsReauth(account) {
  1111. clearGrokNeedsReauthExtra(ctx, s.accountRepo, account.ID)
  1112. }
  1113. }
  1114. func (s *TokenRefreshService) postRefreshStateSyncWithCleanup(parent context.Context, account *Account) {
  1115. cleanupParent := context.Background()
  1116. if parent != nil {
  1117. cleanupParent = context.WithoutCancel(parent)
  1118. }
  1119. ctx, cancel := context.WithTimeout(cleanupParent, defaultTokenRefreshCleanupTimeout)
  1120. defer cancel()
  1121. s.postRefreshStateSync(ctx, account)
  1122. }
  1123. func (s *TokenRefreshService) postRefreshStateSync(ctx context.Context, account *Account) {
  1124. // 对所有 OAuth 账号调用缓存失效(InvalidateToken 内部根据平台判断是否需要处理)
  1125. if s.cacheInvalidator != nil && account.Type == AccountTypeOAuth {
  1126. if err := s.cacheInvalidator.InvalidateToken(ctx, account); err != nil {
  1127. slog.Warn("token_refresh.invalidate_token_cache_failed",
  1128. "account_id", account.ID,
  1129. "error", err,
  1130. )
  1131. } else {
  1132. slog.Debug("token_refresh.token_cache_invalidated", "account_id", account.ID)
  1133. }
  1134. }
  1135. // 同步更新调度器缓存,确保调度获取的 Account 对象包含最新的 credentials
  1136. if s.schedulerCache != nil {
  1137. if err := s.schedulerCache.SetAccount(ctx, account); err != nil {
  1138. slog.Warn("token_refresh.sync_scheduler_cache_failed",
  1139. "account_id", account.ID,
  1140. "error", err,
  1141. )
  1142. } else {
  1143. slog.Debug("token_refresh.scheduler_cache_synced", "account_id", account.ID)
  1144. }
  1145. }
  1146. }
  1147. func (s *TokenRefreshService) clearAntigravityForceTokenRefresh(ctx context.Context, account *Account, outcome string) {
  1148. if s == nil || account == nil || !accountNeedsAntigravityForceTokenRefresh(account) {
  1149. return
  1150. }
  1151. updates := clearAntigravityForceTokenRefreshExtra()
  1152. if err := s.accountRepo.UpdateExtra(ctx, account.ID, updates); err != nil {
  1153. slog.Warn("token_refresh.clear_antigravity_force_refresh_failed",
  1154. "account_id", account.ID,
  1155. "outcome", outcome,
  1156. "error", err,
  1157. )
  1158. return
  1159. }
  1160. if account.Extra != nil {
  1161. for k, v := range updates {
  1162. account.Extra[k] = v
  1163. }
  1164. }
  1165. slog.Info("token_refresh.cleared_antigravity_force_refresh",
  1166. "account_id", account.ID,
  1167. "outcome", outcome,
  1168. )
  1169. }
  1170. // errRefreshSkipped 表示刷新被跳过(锁竞争或已被其他路径刷新),不计入 failed 或 refreshed
  1171. var errRefreshSkipped = fmt.Errorf("refresh skipped")
  1172. type providerConfigurationRefreshError struct {
  1173. err error
  1174. }
  1175. type providerCycleContainmentRefreshError struct {
  1176. err error
  1177. }
  1178. type accountPermanentRefreshError struct {
  1179. err error
  1180. persistentlyBlocked bool
  1181. cacheInvalidationFailed bool
  1182. }
  1183. type refreshAttemptTimeoutError struct {
  1184. err error
  1185. }
  1186. func isProviderScopedTerminalRefreshError(err error) bool {
  1187. if err == nil {
  1188. return false
  1189. }
  1190. var containmentErr *providerCycleContainmentRefreshError
  1191. if errors.As(err, &containmentErr) {
  1192. return true
  1193. }
  1194. var configurationErr *providerConfigurationRefreshError
  1195. return errors.As(err, &configurationErr)
  1196. }
  1197. func (e *refreshAttemptTimeoutError) Error() string {
  1198. return "OAuth refresh attempt timed out"
  1199. }
  1200. func (e *refreshAttemptTimeoutError) Unwrap() error {
  1201. if e == nil || e.err == nil {
  1202. return context.DeadlineExceeded
  1203. }
  1204. return e.err
  1205. }
  1206. func (e *providerConfigurationRefreshError) Error() string {
  1207. return "provider OAuth configuration rejected"
  1208. }
  1209. func (e *providerCycleContainmentRefreshError) Error() string {
  1210. return "provider OAuth failure contained for this cycle"
  1211. }
  1212. func (e *providerCycleContainmentRefreshError) Unwrap() error {
  1213. if e == nil {
  1214. return nil
  1215. }
  1216. return e.err
  1217. }
  1218. func (e *accountPermanentRefreshError) Error() string {
  1219. return "account OAuth credentials permanently rejected"
  1220. }
  1221. func (e *accountPermanentRefreshError) Unwrap() error {
  1222. if e == nil {
  1223. return nil
  1224. }
  1225. return e.err
  1226. }
  1227. func isAmbiguousGrokEntitlementRefreshError(account *Account, err error) bool {
  1228. if account == nil || !account.IsGrokOAuth() || err == nil {
  1229. return false
  1230. }
  1231. msg := strings.ToLower(err.Error())
  1232. coarseEntitlementLabel := strings.EqualFold(infraerrors.Reason(err), "GROK_OAUTH_ENTITLEMENT_DENIED") ||
  1233. strings.Contains(msg, "grok_oauth_entitlement_denied")
  1234. if !coarseEntitlementLabel {
  1235. return false
  1236. }
  1237. for _, evidence := range []string{
  1238. "subscription required",
  1239. "no active grok subscription",
  1240. "no active subscription",
  1241. "grok subscription required",
  1242. "account is not entitled",
  1243. "not entitled",
  1244. "entitlement required",
  1245. "subscription inactive",
  1246. "subscription expired",
  1247. "upgrade your plan",
  1248. } {
  1249. if strings.Contains(msg, evidence) {
  1250. return false
  1251. }
  1252. }
  1253. if bodyIndex := strings.Index(msg, "body:"); bodyIndex >= 0 {
  1254. body := msg[bodyIndex+len("body:"):]
  1255. for _, evidence := range []string{
  1256. "entitlement_denied",
  1257. "entitlement denied",
  1258. "subscription_required",
  1259. "no_active_subscription",
  1260. } {
  1261. if strings.Contains(body, evidence) {
  1262. return false
  1263. }
  1264. }
  1265. }
  1266. return true
  1267. }
  1268. func (e *providerConfigurationRefreshError) Unwrap() error {
  1269. if e == nil {
  1270. return nil
  1271. }
  1272. return e.err
  1273. }
  1274. func isSharedProviderRefreshError(err error) bool {
  1275. if err == nil {
  1276. return false
  1277. }
  1278. msg := strings.ToLower(err.Error())
  1279. for _, needle := range []string{
  1280. "invalid_client",
  1281. "unauthorized_client",
  1282. "invalid_scope",
  1283. "unknown scope",
  1284. } {
  1285. if strings.Contains(msg, needle) {
  1286. return true
  1287. }
  1288. }
  1289. return false
  1290. }
  1291. // isNonRetryableRefreshError 判断是否为不可重试的刷新错误
  1292. // 这些错误通常表示凭证已失效或配置确实缺失,需要用户重新授权
  1293. // 注意:missing_project_id 错误只在真正缺失(从未获取过)时返回,临时获取失败不会返回此错误
  1294. func isNonRetryableRefreshError(err error) bool {
  1295. if err == nil {
  1296. return false
  1297. }
  1298. msg := strings.ToLower(err.Error())
  1299. nonRetryable := []string{
  1300. "invalid_grant", // refresh_token 已失效
  1301. "invalid_refresh_token", // refresh_token 无效, team 账号工作区被删除会出现
  1302. "token_expired", // OpenAI refresh_token 已过期,需要重新授权
  1303. "app_session_terminated", // refresh_token team 账号工作区被删除
  1304. "refresh_token_reused", // OpenAI refresh_token 已被使用,必须重新授权
  1305. "refresh_token_invalidated", // OpenAI session ended; refresh token invalidated
  1306. "invalid_client", // 客户端配置错误
  1307. "unauthorized_client", // 客户端未授权
  1308. "access_denied", // 访问被拒绝
  1309. "missing_project_id", // 缺少 project_id
  1310. "no refresh token available",
  1311. "grok_oauth_entitlement_denied",
  1312. "entitlement_denied",
  1313. "invalid_scope",
  1314. "unknown scope",
  1315. "subscription required",
  1316. "no active grok subscription",
  1317. }
  1318. for _, needle := range nonRetryable {
  1319. if strings.Contains(msg, needle) {
  1320. return true
  1321. }
  1322. }
  1323. return false
  1324. }
  1325. // ensureOpenAIPrivacy 检查 OpenAI OAuth 账号是否已设置 privacy_mode,
  1326. // 未设置则调用 disableOpenAITraining 并持久化结果到 Extra。
  1327. func (s *TokenRefreshService) ensureOpenAIPrivacy(ctx context.Context, account *Account) {
  1328. if account.Platform != PlatformOpenAI || account.Type != AccountTypeOAuth {
  1329. return
  1330. }
  1331. if s.privacyClientFactory == nil {
  1332. return
  1333. }
  1334. if shouldSkipOpenAIPrivacyEnsure(account.Extra) {
  1335. return
  1336. }
  1337. token, _ := account.Credentials["access_token"].(string)
  1338. if token == "" {
  1339. return
  1340. }
  1341. var proxyURL string
  1342. if account.ProxyID != nil && s.proxyRepo != nil {
  1343. if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
  1344. proxyURL = p.URL()
  1345. }
  1346. }
  1347. mode := disableOpenAITraining(ctx, s.privacyClientFactory, token, proxyURL)
  1348. if mode == "" {
  1349. return
  1350. }
  1351. if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
  1352. slog.Warn("token_refresh.update_privacy_mode_failed",
  1353. "account_id", account.ID,
  1354. "error", err,
  1355. )
  1356. } else {
  1357. slog.Info("token_refresh.privacy_mode_set",
  1358. "account_id", account.ID,
  1359. "privacy_mode", mode,
  1360. )
  1361. }
  1362. }
  1363. // ensureAntigravityPrivacy 后台刷新中检查 Antigravity OAuth 账号隐私状态。
  1364. // 仅当 privacy_mode 已成功设置("privacy_set")时跳过;
  1365. // 未设置或之前失败("privacy_set_failed")均会重试。
  1366. func (s *TokenRefreshService) ensureAntigravityPrivacy(ctx context.Context, account *Account) {
  1367. if account.Platform != PlatformAntigravity || account.Type != AccountTypeOAuth {
  1368. return
  1369. }
  1370. if account.Extra != nil {
  1371. if mode, ok := account.Extra["privacy_mode"].(string); ok && mode == AntigravityPrivacySet {
  1372. return
  1373. }
  1374. }
  1375. token, _ := account.Credentials["access_token"].(string)
  1376. if token == "" {
  1377. return
  1378. }
  1379. projectID, _ := account.Credentials["project_id"].(string)
  1380. var proxyURL string
  1381. if account.ProxyID != nil && s.proxyRepo != nil {
  1382. if p, err := s.proxyRepo.GetByID(ctx, *account.ProxyID); err == nil && p != nil {
  1383. proxyURL = p.URL()
  1384. }
  1385. }
  1386. mode := setAntigravityPrivacy(ctx, token, projectID, proxyURL)
  1387. if mode == "" {
  1388. return
  1389. }
  1390. if err := s.accountRepo.UpdateExtra(ctx, account.ID, map[string]any{"privacy_mode": mode}); err != nil {
  1391. slog.Warn("token_refresh.update_antigravity_privacy_mode_failed",
  1392. "account_id", account.ID,
  1393. "error", err,
  1394. )
  1395. } else {
  1396. applyAntigravityPrivacyMode(account, mode)
  1397. slog.Info("token_refresh.antigravity_privacy_mode_set",
  1398. "account_id", account.ID,
  1399. "privacy_mode", mode,
  1400. )
  1401. }
  1402. }