openai_token_provider.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "math/rand/v2"
  7. "strings"
  8. "sync/atomic"
  9. "time"
  10. )
  11. const (
  12. openAITokenRefreshSkew = 3 * time.Minute
  13. openAITokenCacheSkew = 5 * time.Minute
  14. openAILockInitialWait = 20 * time.Millisecond
  15. openAILockMaxWait = 120 * time.Millisecond
  16. openAILockMaxAttempts = 5
  17. openAILockJitterRatio = 0.2
  18. openAILockWarnThresholdMs = 250
  19. )
  20. // OpenAITokenRuntimeMetrics is a snapshot of refresh and lock contention metrics.
  21. type OpenAITokenRuntimeMetrics struct {
  22. RefreshRequests int64
  23. RefreshSuccess int64
  24. RefreshFailure int64
  25. LockAcquireFailure int64
  26. LockContention int64
  27. LockWaitSamples int64
  28. LockWaitTotalMs int64
  29. LockWaitHit int64
  30. LockWaitMiss int64
  31. LastObservedUnixMs int64
  32. }
  33. type openAITokenRuntimeMetricsStore struct {
  34. refreshRequests atomic.Int64
  35. refreshSuccess atomic.Int64
  36. refreshFailure atomic.Int64
  37. lockAcquireFailure atomic.Int64
  38. lockContention atomic.Int64
  39. lockWaitSamples atomic.Int64
  40. lockWaitTotalMs atomic.Int64
  41. lockWaitHit atomic.Int64
  42. lockWaitMiss atomic.Int64
  43. lastObservedUnixMs atomic.Int64
  44. }
  45. func (m *openAITokenRuntimeMetricsStore) snapshot() OpenAITokenRuntimeMetrics {
  46. if m == nil {
  47. return OpenAITokenRuntimeMetrics{}
  48. }
  49. return OpenAITokenRuntimeMetrics{
  50. RefreshRequests: m.refreshRequests.Load(),
  51. RefreshSuccess: m.refreshSuccess.Load(),
  52. RefreshFailure: m.refreshFailure.Load(),
  53. LockAcquireFailure: m.lockAcquireFailure.Load(),
  54. LockContention: m.lockContention.Load(),
  55. LockWaitSamples: m.lockWaitSamples.Load(),
  56. LockWaitTotalMs: m.lockWaitTotalMs.Load(),
  57. LockWaitHit: m.lockWaitHit.Load(),
  58. LockWaitMiss: m.lockWaitMiss.Load(),
  59. LastObservedUnixMs: m.lastObservedUnixMs.Load(),
  60. }
  61. }
  62. func (m *openAITokenRuntimeMetricsStore) touchNow() {
  63. if m == nil {
  64. return
  65. }
  66. m.lastObservedUnixMs.Store(time.Now().UnixMilli())
  67. }
  68. // OpenAITokenCache token cache interface.
  69. type OpenAITokenCache = GeminiTokenCache
  70. // OpenAITokenProvider manages access_token for OpenAI OAuth accounts.
  71. type OpenAITokenProvider struct {
  72. accountRepo AccountRepository
  73. tokenCache OpenAITokenCache
  74. openAIOAuthService *OpenAIOAuthService
  75. runtimeBlocker AccountRuntimeBlocker
  76. metrics *openAITokenRuntimeMetricsStore
  77. refreshAPI *OAuthRefreshAPI
  78. executor OAuthRefreshExecutor
  79. refreshPolicy ProviderRefreshPolicy
  80. }
  81. func NewOpenAITokenProvider(
  82. accountRepo AccountRepository,
  83. tokenCache OpenAITokenCache,
  84. openAIOAuthService *OpenAIOAuthService,
  85. ) *OpenAITokenProvider {
  86. return &OpenAITokenProvider{
  87. accountRepo: accountRepo,
  88. tokenCache: tokenCache,
  89. openAIOAuthService: openAIOAuthService,
  90. metrics: &openAITokenRuntimeMetricsStore{},
  91. refreshPolicy: OpenAIProviderRefreshPolicy(),
  92. }
  93. }
  94. // SetRefreshAPI injects unified OAuth refresh API and executor.
  95. func (p *OpenAITokenProvider) SetRefreshAPI(api *OAuthRefreshAPI, executor OAuthRefreshExecutor) {
  96. p.refreshAPI = api
  97. p.executor = executor
  98. }
  99. // SetRefreshPolicy injects caller-side refresh policy.
  100. func (p *OpenAITokenProvider) SetRefreshPolicy(policy ProviderRefreshPolicy) {
  101. p.refreshPolicy = policy
  102. }
  103. func (p *OpenAITokenProvider) SetAccountRuntimeBlocker(blocker AccountRuntimeBlocker) {
  104. p.runtimeBlocker = blocker
  105. }
  106. func (p *OpenAITokenProvider) SnapshotRuntimeMetrics() OpenAITokenRuntimeMetrics {
  107. if p == nil {
  108. return OpenAITokenRuntimeMetrics{}
  109. }
  110. p.ensureMetrics()
  111. return p.metrics.snapshot()
  112. }
  113. func (p *OpenAITokenProvider) ensureMetrics() {
  114. if p != nil && p.metrics == nil {
  115. p.metrics = &openAITokenRuntimeMetricsStore{}
  116. }
  117. }
  118. // GetAccessToken returns a valid access_token.
  119. func (p *OpenAITokenProvider) GetAccessToken(ctx context.Context, account *Account) (string, error) {
  120. p.ensureMetrics()
  121. if account == nil {
  122. return "", errors.New("account is nil")
  123. }
  124. if account.Platform != PlatformOpenAI || account.Type != AccountTypeOAuth {
  125. return "", errors.New("not an openai oauth account")
  126. }
  127. cacheKey := OpenAITokenCacheKey(account)
  128. // 1) Try cache first.
  129. if p.tokenCache != nil {
  130. if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
  131. slog.Debug("openai_token_cache_hit", "account_id", account.ID)
  132. return token, nil
  133. } else if err != nil {
  134. slog.Warn("openai_token_cache_get_failed", "account_id", account.ID, "error", err)
  135. }
  136. }
  137. slog.Debug("openai_token_cache_miss", "account_id", account.ID)
  138. // 2) Refresh if needed (pre-expiry skew).
  139. expiresAt := account.GetCredentialAsTime("expires_at")
  140. needsRefresh := !account.IsOpenAIPersonalAccessToken() && (expiresAt == nil || time.Until(*expiresAt) <= openAITokenRefreshSkew)
  141. if needsRefresh && strings.TrimSpace(account.GetOpenAIRefreshToken()) == "" {
  142. if expiresAt != nil && !time.Now().Before(*expiresAt) {
  143. const reason = "openai access_token expired and refresh_token is missing"
  144. // 永久故障:缺失 refresh_token 时账号无法自愈,必须立即从调度池剔除,
  145. // 否则会被反复选中、每次都在 token 阶段直接返回错误,对用户呈现持续 502。
  146. p.disableAccountMissingRefreshToken(account, reason)
  147. return "", errors.New(reason)
  148. }
  149. needsRefresh = false
  150. }
  151. refreshFailed := false
  152. if needsRefresh && p.refreshAPI != nil && p.executor != nil {
  153. p.metrics.refreshRequests.Add(1)
  154. p.metrics.touchNow()
  155. result, err := p.refreshAPI.RefreshIfNeeded(ctx, account, p.executor, openAITokenRefreshSkew)
  156. if err != nil {
  157. if p.refreshPolicy.OnRefreshError == ProviderRefreshErrorReturn {
  158. return "", err
  159. }
  160. slog.Warn("openai_token_refresh_failed", "account_id", account.ID, "error", err)
  161. p.metrics.refreshFailure.Add(1)
  162. refreshFailed = true
  163. } else if result.LockHeld {
  164. if p.refreshPolicy.OnLockHeld == ProviderLockHeldWaitForCache {
  165. p.metrics.lockContention.Add(1)
  166. p.metrics.touchNow()
  167. token, waitErr := p.waitForTokenAfterLockRace(ctx, cacheKey)
  168. if waitErr != nil {
  169. return "", waitErr
  170. }
  171. if strings.TrimSpace(token) != "" {
  172. slog.Debug("openai_token_cache_hit_after_wait", "account_id", account.ID)
  173. return token, nil
  174. }
  175. }
  176. } else if result.Refreshed {
  177. p.metrics.refreshSuccess.Add(1)
  178. account = result.Account
  179. expiresAt = account.GetCredentialAsTime("expires_at")
  180. } else {
  181. account = result.Account
  182. expiresAt = account.GetCredentialAsTime("expires_at")
  183. }
  184. } else if needsRefresh && p.tokenCache != nil {
  185. // Backward-compatible test path when refreshAPI is not injected.
  186. p.metrics.refreshRequests.Add(1)
  187. p.metrics.touchNow()
  188. locked, lockErr := p.tokenCache.AcquireRefreshLock(ctx, cacheKey, 30*time.Second)
  189. if lockErr == nil && locked {
  190. defer func() { _ = p.tokenCache.ReleaseRefreshLock(ctx, cacheKey) }()
  191. } else if lockErr != nil {
  192. p.metrics.lockAcquireFailure.Add(1)
  193. p.metrics.touchNow()
  194. slog.Warn("openai_token_lock_failed", "account_id", account.ID, "error", lockErr)
  195. } else {
  196. p.metrics.lockContention.Add(1)
  197. p.metrics.touchNow()
  198. token, waitErr := p.waitForTokenAfterLockRace(ctx, cacheKey)
  199. if waitErr != nil {
  200. return "", waitErr
  201. }
  202. if strings.TrimSpace(token) != "" {
  203. slog.Debug("openai_token_cache_hit_after_wait", "account_id", account.ID)
  204. return token, nil
  205. }
  206. }
  207. }
  208. accessToken := account.GetCredential("access_token")
  209. if strings.TrimSpace(accessToken) == "" {
  210. return "", errors.New("access_token not found in credentials")
  211. }
  212. // 3) Populate cache with TTL.
  213. if p.tokenCache != nil {
  214. latestAccount, isStale := CheckTokenVersion(ctx, account, p.accountRepo)
  215. if isStale && latestAccount != nil {
  216. slog.Debug("openai_token_version_stale_use_latest", "account_id", account.ID)
  217. accessToken = latestAccount.GetOpenAIAccessToken()
  218. if strings.TrimSpace(accessToken) == "" {
  219. return "", errors.New("access_token not found after version check")
  220. }
  221. } else {
  222. ttl := 30 * time.Minute
  223. if refreshFailed {
  224. if p.refreshPolicy.FailureTTL > 0 {
  225. ttl = p.refreshPolicy.FailureTTL
  226. } else {
  227. ttl = time.Minute
  228. }
  229. slog.Debug("openai_token_cache_short_ttl", "account_id", account.ID, "reason", "refresh_failed")
  230. } else if expiresAt != nil {
  231. until := time.Until(*expiresAt)
  232. switch {
  233. case until > openAITokenCacheSkew:
  234. ttl = until - openAITokenCacheSkew
  235. case until > 0:
  236. ttl = until
  237. default:
  238. ttl = time.Minute
  239. }
  240. }
  241. if err := p.tokenCache.SetAccessToken(ctx, cacheKey, accessToken, ttl); err != nil {
  242. slog.Warn("openai_token_cache_set_failed", "account_id", account.ID, "error", err)
  243. }
  244. }
  245. }
  246. return accessToken, nil
  247. }
  248. // disableAccountMissingRefreshToken 在请求路径上发现 OpenAI OAuth 账号
  249. // 凭证已过期且 refresh_token 缺失时,将账号标记为 error 状态。
  250. // 这是一种永久性故障:仅靠后续请求或 TokenRefreshService 不会自愈
  251. // (NeedsRefresh 也会因 refresh_token 为空直接跳过),
  252. // 必须主动剔除以避免账号被持续选中导致用户端反复 502。
  253. // 使用 background context 是因为请求 context 可能很快结束。
  254. func (p *OpenAITokenProvider) disableAccountMissingRefreshToken(account *Account, reason string) {
  255. if p == nil || p.accountRepo == nil || account == nil {
  256. return
  257. }
  258. if p.runtimeBlocker != nil {
  259. p.runtimeBlocker.BlockAccountScheduling(account, time.Time{}, "missing_refresh_token")
  260. }
  261. bgCtx := context.Background()
  262. if err := p.accountRepo.SetError(bgCtx, account.ID, reason); err != nil {
  263. slog.Warn("openai_token_provider.set_error_failed",
  264. "account_id", account.ID,
  265. "error", err,
  266. )
  267. return
  268. }
  269. if p.tokenCache != nil {
  270. cacheKey := OpenAITokenCacheKey(account)
  271. if err := p.tokenCache.DeleteAccessToken(bgCtx, cacheKey); err != nil {
  272. slog.Warn("openai_token_provider.cache_delete_failed",
  273. "account_id", account.ID,
  274. "error", err,
  275. )
  276. }
  277. }
  278. slog.Warn("openai_token_provider.account_disabled_missing_refresh_token",
  279. "account_id", account.ID,
  280. "reason", reason,
  281. )
  282. }
  283. func (p *OpenAITokenProvider) waitForTokenAfterLockRace(ctx context.Context, cacheKey string) (string, error) {
  284. wait := openAILockInitialWait
  285. totalWaitMs := int64(0)
  286. for i := 0; i < openAILockMaxAttempts; i++ {
  287. actualWait := jitterLockWait(wait)
  288. timer := time.NewTimer(actualWait)
  289. select {
  290. case <-ctx.Done():
  291. if !timer.Stop() {
  292. select {
  293. case <-timer.C:
  294. default:
  295. }
  296. }
  297. return "", ctx.Err()
  298. case <-timer.C:
  299. }
  300. waitMs := actualWait.Milliseconds()
  301. if waitMs < 0 {
  302. waitMs = 0
  303. }
  304. totalWaitMs += waitMs
  305. p.metrics.lockWaitSamples.Add(1)
  306. p.metrics.lockWaitTotalMs.Add(waitMs)
  307. p.metrics.touchNow()
  308. token, err := p.tokenCache.GetAccessToken(ctx, cacheKey)
  309. if err == nil && strings.TrimSpace(token) != "" {
  310. p.metrics.lockWaitHit.Add(1)
  311. if totalWaitMs >= openAILockWarnThresholdMs {
  312. slog.Warn("openai_token_lock_wait_high", "wait_ms", totalWaitMs, "attempts", i+1)
  313. }
  314. return token, nil
  315. }
  316. if wait < openAILockMaxWait {
  317. wait *= 2
  318. if wait > openAILockMaxWait {
  319. wait = openAILockMaxWait
  320. }
  321. }
  322. }
  323. p.metrics.lockWaitMiss.Add(1)
  324. if totalWaitMs >= openAILockWarnThresholdMs {
  325. slog.Warn("openai_token_lock_wait_high", "wait_ms", totalWaitMs, "attempts", openAILockMaxAttempts)
  326. }
  327. return "", nil
  328. }
  329. func jitterLockWait(base time.Duration) time.Duration {
  330. if base <= 0 {
  331. return 0
  332. }
  333. minFactor := 1 - openAILockJitterRatio
  334. maxFactor := 1 + openAILockJitterRatio
  335. factor := minFactor + rand.Float64()*(maxFactor-minFactor)
  336. return time.Duration(float64(base) * factor)
  337. }