// Package token provides in-memory token caching to reduce file I/O. package token import ( "sync" "time" log "github.com/sirupsen/logrus" ) // CachedToken holds a cached token with metadata. type CachedToken struct { Token interface{} LoadedAt time.Time ExpiresAt time.Time RefreshAt time.Time Provider string Key string mu sync.RWMutex } // IsExpired returns true if the token has expired. func (ct *CachedToken) IsExpired() bool { ct.mu.RLock() defer ct.mu.RUnlock() return time.Now().After(ct.ExpiresAt) } // NeedsRefresh returns true if the token should be refreshed. func (ct *CachedToken) NeedsRefresh() bool { ct.mu.RLock() defer ct.mu.RUnlock() return time.Now().After(ct.RefreshAt) } // TTL returns the remaining time until expiration. func (ct *CachedToken) TTL() time.Duration { ct.mu.RLock() defer ct.mu.RUnlock() remaining := time.Until(ct.ExpiresAt) if remaining < 0 { return 0 } return remaining } // Update updates the cached token with new values. func (ct *CachedToken) Update(token interface{}, expiresAt time.Time, refreshBefore time.Duration) { ct.mu.Lock() defer ct.mu.Unlock() ct.Token = token ct.LoadedAt = time.Now() ct.ExpiresAt = expiresAt ct.RefreshAt = expiresAt.Add(-refreshBefore) } // RefreshTask represents a token that needs refreshing. type RefreshTask struct { Provider string Key string Token *CachedToken } // Cache provides thread-safe in-memory token caching. type Cache struct { tokens sync.Map // provider:key -> *CachedToken refreshCh chan RefreshTask stopCh chan struct{} refreshBefore time.Duration cleanupInterval time.Duration enabled bool mu sync.RWMutex closed bool // Stats hits int64 misses int64 } // Config holds token cache configuration. type Config struct { Enabled bool PreemptiveRefresh bool RefreshBeforeSeconds int CleanupIntervalSeconds int } // DefaultConfig returns default token cache configuration. func DefaultConfig() *Config { return &Config{ Enabled: true, PreemptiveRefresh: true, RefreshBeforeSeconds: 600, // 10 minutes CleanupIntervalSeconds: 60, } } // NewCache creates a new token cache. func NewCache(config *Config) *Cache { if config == nil { config = DefaultConfig() } if !config.Enabled { return nil } c := &Cache{ refreshCh: make(chan RefreshTask, 100), stopCh: make(chan struct{}), refreshBefore: time.Duration(config.RefreshBeforeSeconds) * time.Second, cleanupInterval: time.Duration(config.CleanupIntervalSeconds) * time.Second, enabled: true, } go c.cleanupLoop() log.WithFields(log.Fields{ "refresh_before": c.refreshBefore, "cleanup_interval": c.cleanupInterval, }).Info("token cache initialized") return c } // Get retrieves a token from cache, calling loader if not found or expired. // The loader function should return: (token, expiresAt, error) func (c *Cache) Get(provider, key string, loader func() (interface{}, time.Time, error)) (interface{}, error) { if c == nil || !c.enabled { token, _, err := loader() return token, err } cacheKey := provider + ":" + key // Check cache first if val, ok := c.tokens.Load(cacheKey); ok { cached := val.(*CachedToken) if !cached.IsExpired() { c.mu.Lock() c.hits++ c.mu.Unlock() // Schedule background refresh if needed if cached.NeedsRefresh() { select { case c.refreshCh <- RefreshTask{Provider: provider, Key: key, Token: cached}: default: // Channel full, skip refresh } } return cached.Token, nil } } c.mu.Lock() c.misses++ c.mu.Unlock() // Load token token, expiresAt, err := loader() if err != nil { return nil, err } // Cache it cached := &CachedToken{ Token: token, LoadedAt: time.Now(), ExpiresAt: expiresAt, RefreshAt: expiresAt.Add(-c.refreshBefore), Provider: provider, Key: key, } c.tokens.Store(cacheKey, cached) log.WithFields(log.Fields{ "provider": provider, "ttl": cached.TTL(), }).Debug("token cached") return token, nil } // GetCached retrieves a token from cache without loading. func (c *Cache) GetCached(provider, key string) (interface{}, bool) { if c == nil || !c.enabled { return nil, false } cacheKey := provider + ":" + key if val, ok := c.tokens.Load(cacheKey); ok { cached := val.(*CachedToken) if !cached.IsExpired() { return cached.Token, true } } return nil, false } // Set stores a token in the cache. func (c *Cache) Set(provider, key string, token interface{}, expiresAt time.Time) { if c == nil || !c.enabled { return } cacheKey := provider + ":" + key cached := &CachedToken{ Token: token, LoadedAt: time.Now(), ExpiresAt: expiresAt, RefreshAt: expiresAt.Add(-c.refreshBefore), Provider: provider, Key: key, } c.tokens.Store(cacheKey, cached) } // Invalidate removes a token from the cache. func (c *Cache) Invalidate(provider, key string) { if c == nil { return } cacheKey := provider + ":" + key c.tokens.Delete(cacheKey) } // InvalidateProvider removes all tokens for a provider. func (c *Cache) InvalidateProvider(provider string) { if c == nil { return } c.tokens.Range(func(k, v interface{}) bool { cached := v.(*CachedToken) if cached.Provider == provider { c.tokens.Delete(k) } return true }) } // Clear removes all tokens from the cache. func (c *Cache) Clear() { if c == nil { return } c.tokens.Range(func(k, v interface{}) bool { c.tokens.Delete(k) return true }) } // Stats returns cache statistics. func (c *Cache) Stats() (hits, misses int64, count int) { if c == nil { return 0, 0, 0 } c.mu.RLock() hits = c.hits misses = c.misses c.mu.RUnlock() c.tokens.Range(func(k, v interface{}) bool { count++ return true }) return } // Close shuts down the cache. func (c *Cache) Close() { if c == nil { return } c.mu.Lock() if c.closed { c.mu.Unlock() return } c.closed = true c.mu.Unlock() close(c.stopCh) log.Info("token cache closed") } func (c *Cache) cleanupLoop() { ticker := time.NewTicker(c.cleanupInterval) defer ticker.Stop() for { select { case <-c.stopCh: return case <-ticker.C: c.cleanup() } } } func (c *Cache) cleanup() { now := time.Now() removed := 0 c.tokens.Range(func(k, v interface{}) bool { cached := v.(*CachedToken) if now.After(cached.ExpiresAt) { c.tokens.Delete(k) removed++ } return true }) if removed > 0 { log.WithField("removed", removed).Debug("cleaned up expired tokens") } } // IsEnabled returns whether the cache is enabled. func (c *Cache) IsEnabled() bool { return c != nil && c.enabled }