grok_token_refresher.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "hash/fnv"
  6. "strings"
  7. "time"
  8. )
  9. // Base warm window: refresh when access token lifetime remaining is below this.
  10. // Grok access tokens are typically ~1h; refreshing up to 1h early keeps the pool
  11. // warm for request path cache misses.
  12. const grokTokenRefreshSkew = time.Hour
  13. // Stampede spread: each account's effective warm window is reduced by a
  14. // deterministic offset in [0, grokTokenRefreshJitterMax] so co-imported accounts
  15. // do not all refresh in the same TokenRefreshService cycle (grok2api-style
  16. // RefreshDueAt scatter).
  17. const grokTokenRefreshJitterMax = 3 * time.Minute
  18. // Floor so jitter cannot shrink the window below a useful threshold.
  19. const grokTokenRefreshSkewMin = 30 * time.Minute
  20. type GrokTokenRefresher struct {
  21. grokOAuthService GrokOAuthTokenService
  22. }
  23. func NewGrokTokenRefresher(grokOAuthService GrokOAuthTokenService) *GrokTokenRefresher {
  24. return &GrokTokenRefresher{grokOAuthService: grokOAuthService}
  25. }
  26. func (r *GrokTokenRefresher) CacheKey(account *Account) string {
  27. return GrokTokenCacheKey(account)
  28. }
  29. func (r *GrokTokenRefresher) CanRefresh(account *Account) bool {
  30. return account != nil && account.Platform == PlatformGrok && account.Type == AccountTypeOAuth &&
  31. strings.TrimSpace(account.GetGrokRefreshToken()) != ""
  32. }
  33. func (r *GrokTokenRefresher) NeedsRefresh(account *Account, refreshWindow time.Duration) bool {
  34. if account == nil || strings.TrimSpace(account.GetGrokRefreshToken()) == "" {
  35. return false
  36. }
  37. if strings.TrimSpace(account.GetGrokAccessToken()) == "" {
  38. return true
  39. }
  40. expiresAt := account.GetCredentialAsTime("expires_at")
  41. if expiresAt == nil {
  42. return true
  43. }
  44. if refreshWindow < grokTokenRefreshSkew {
  45. refreshWindow = grokTokenRefreshSkew
  46. }
  47. // Deterministic per-account jitter: spread warm refreshes without random
  48. // non-determinism in tests (hash of account id).
  49. refreshWindow = grokTokenRefreshWindowWithJitter(account.ID, refreshWindow)
  50. return time.Until(*expiresAt) < refreshWindow
  51. }
  52. // grokTokenRefreshWindowWithJitter returns refreshWindow minus a stable offset
  53. // in [0, jitterMax] based on accountID. Result is never below grokTokenRefreshSkewMin
  54. // when the base window is at least that large.
  55. func grokTokenRefreshWindowWithJitter(accountID int64, refreshWindow time.Duration) time.Duration {
  56. if accountID <= 0 || refreshWindow <= grokTokenRefreshSkewMin {
  57. return refreshWindow
  58. }
  59. h := fnv.New32a()
  60. var b [8]byte
  61. id := uint64(accountID)
  62. for i := 0; i < 8; i++ {
  63. b[i] = byte(id >> (8 * i))
  64. }
  65. _, _ = h.Write(b[:])
  66. // Jitter in [0, grokTokenRefreshJitterMax).
  67. jitter := time.Duration(h.Sum32()%uint32(grokTokenRefreshJitterMax/time.Second)) * time.Second
  68. out := refreshWindow - jitter
  69. if out < grokTokenRefreshSkewMin {
  70. return grokTokenRefreshSkewMin
  71. }
  72. return out
  73. }
  74. func (r *GrokTokenRefresher) Refresh(ctx context.Context, account *Account) (map[string]any, error) {
  75. if r == nil || r.grokOAuthService == nil {
  76. return nil, errors.New("grok oauth service is not configured")
  77. }
  78. tokenInfo, err := r.grokOAuthService.RefreshAccountToken(ctx, account)
  79. if err != nil {
  80. return nil, err
  81. }
  82. newCredentials := r.grokOAuthService.BuildAccountCredentials(tokenInfo)
  83. newCredentials = MergeCredentials(account.Credentials, newCredentials)
  84. if baseURL := strings.TrimSpace(account.GetCredential("base_url")); baseURL != "" {
  85. newCredentials["base_url"] = baseURL
  86. }
  87. return newCredentials, nil
  88. }