| |
| package token |
|
|
| import ( |
| "sync" |
| "time" |
|
|
| log "github.com/sirupsen/logrus" |
| ) |
|
|
| |
| type CachedToken struct { |
| Token interface{} |
| LoadedAt time.Time |
| ExpiresAt time.Time |
| RefreshAt time.Time |
| Provider string |
| Key string |
| mu sync.RWMutex |
| } |
|
|
| |
| func (ct *CachedToken) IsExpired() bool { |
| ct.mu.RLock() |
| defer ct.mu.RUnlock() |
| return time.Now().After(ct.ExpiresAt) |
| } |
|
|
| |
| func (ct *CachedToken) NeedsRefresh() bool { |
| ct.mu.RLock() |
| defer ct.mu.RUnlock() |
| return time.Now().After(ct.RefreshAt) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| type RefreshTask struct { |
| Provider string |
| Key string |
| Token *CachedToken |
| } |
|
|
| |
| type Cache struct { |
| tokens sync.Map |
| refreshCh chan RefreshTask |
| stopCh chan struct{} |
| refreshBefore time.Duration |
| cleanupInterval time.Duration |
| enabled bool |
| mu sync.RWMutex |
| closed bool |
|
|
| |
| hits int64 |
| misses int64 |
| } |
|
|
| |
| type Config struct { |
| Enabled bool |
| PreemptiveRefresh bool |
| RefreshBeforeSeconds int |
| CleanupIntervalSeconds int |
| } |
|
|
| |
| func DefaultConfig() *Config { |
| return &Config{ |
| Enabled: true, |
| PreemptiveRefresh: true, |
| RefreshBeforeSeconds: 600, |
| CleanupIntervalSeconds: 60, |
| } |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| 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 |
|
|
| |
| if val, ok := c.tokens.Load(cacheKey); ok { |
| cached := val.(*CachedToken) |
| if !cached.IsExpired() { |
| c.mu.Lock() |
| c.hits++ |
| c.mu.Unlock() |
|
|
| |
| if cached.NeedsRefresh() { |
| select { |
| case c.refreshCh <- RefreshTask{Provider: provider, Key: key, Token: cached}: |
| default: |
| |
| } |
| } |
|
|
| return cached.Token, nil |
| } |
| } |
|
|
| c.mu.Lock() |
| c.misses++ |
| c.mu.Unlock() |
|
|
| |
| token, expiresAt, err := loader() |
| if err != nil { |
| return nil, err |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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) |
| } |
|
|
| |
| func (c *Cache) Invalidate(provider, key string) { |
| if c == nil { |
| return |
| } |
| cacheKey := provider + ":" + key |
| c.tokens.Delete(cacheKey) |
| } |
|
|
| |
| 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 |
| }) |
| } |
|
|
| |
| func (c *Cache) Clear() { |
| if c == nil { |
| return |
| } |
| c.tokens.Range(func(k, v interface{}) bool { |
| c.tokens.Delete(k) |
| return true |
| }) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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") |
| } |
| } |
|
|
| |
| func (c *Cache) IsEnabled() bool { |
| return c != nil && c.enabled |
| } |
|
|