| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- package service
- import (
- "context"
- "errors"
- "hash/fnv"
- "strings"
- "time"
- )
- // Base warm window: refresh when access token lifetime remaining is below this.
- // Grok access tokens are typically ~1h; refreshing up to 1h early keeps the pool
- // warm for request path cache misses.
- const grokTokenRefreshSkew = time.Hour
- // Stampede spread: each account's effective warm window is reduced by a
- // deterministic offset in [0, grokTokenRefreshJitterMax] so co-imported accounts
- // do not all refresh in the same TokenRefreshService cycle (grok2api-style
- // RefreshDueAt scatter).
- const grokTokenRefreshJitterMax = 3 * time.Minute
- // Floor so jitter cannot shrink the window below a useful threshold.
- const grokTokenRefreshSkewMin = 30 * time.Minute
- type GrokTokenRefresher struct {
- grokOAuthService GrokOAuthTokenService
- }
- func NewGrokTokenRefresher(grokOAuthService GrokOAuthTokenService) *GrokTokenRefresher {
- return &GrokTokenRefresher{grokOAuthService: grokOAuthService}
- }
- func (r *GrokTokenRefresher) CacheKey(account *Account) string {
- return GrokTokenCacheKey(account)
- }
- func (r *GrokTokenRefresher) CanRefresh(account *Account) bool {
- return account != nil && account.Platform == PlatformGrok && account.Type == AccountTypeOAuth &&
- strings.TrimSpace(account.GetGrokRefreshToken()) != ""
- }
- func (r *GrokTokenRefresher) NeedsRefresh(account *Account, refreshWindow time.Duration) bool {
- if account == nil || strings.TrimSpace(account.GetGrokRefreshToken()) == "" {
- return false
- }
- if strings.TrimSpace(account.GetGrokAccessToken()) == "" {
- return true
- }
- expiresAt := account.GetCredentialAsTime("expires_at")
- if expiresAt == nil {
- return true
- }
- if refreshWindow < grokTokenRefreshSkew {
- refreshWindow = grokTokenRefreshSkew
- }
- // Deterministic per-account jitter: spread warm refreshes without random
- // non-determinism in tests (hash of account id).
- refreshWindow = grokTokenRefreshWindowWithJitter(account.ID, refreshWindow)
- return time.Until(*expiresAt) < refreshWindow
- }
- // grokTokenRefreshWindowWithJitter returns refreshWindow minus a stable offset
- // in [0, jitterMax] based on accountID. Result is never below grokTokenRefreshSkewMin
- // when the base window is at least that large.
- func grokTokenRefreshWindowWithJitter(accountID int64, refreshWindow time.Duration) time.Duration {
- if accountID <= 0 || refreshWindow <= grokTokenRefreshSkewMin {
- return refreshWindow
- }
- h := fnv.New32a()
- var b [8]byte
- id := uint64(accountID)
- for i := 0; i < 8; i++ {
- b[i] = byte(id >> (8 * i))
- }
- _, _ = h.Write(b[:])
- // Jitter in [0, grokTokenRefreshJitterMax).
- jitter := time.Duration(h.Sum32()%uint32(grokTokenRefreshJitterMax/time.Second)) * time.Second
- out := refreshWindow - jitter
- if out < grokTokenRefreshSkewMin {
- return grokTokenRefreshSkewMin
- }
- return out
- }
- func (r *GrokTokenRefresher) Refresh(ctx context.Context, account *Account) (map[string]any, error) {
- if r == nil || r.grokOAuthService == nil {
- return nil, errors.New("grok oauth service is not configured")
- }
- tokenInfo, err := r.grokOAuthService.RefreshAccountToken(ctx, account)
- if err != nil {
- return nil, err
- }
- newCredentials := r.grokOAuthService.BuildAccountCredentials(tokenInfo)
- newCredentials = MergeCredentials(account.Credentials, newCredentials)
- if baseURL := strings.TrimSpace(account.GetCredential("base_url")); baseURL != "" {
- newCredentials["base_url"] = baseURL
- }
- return newCredentials, nil
- }
|