shinway / internal /cache /token /cache.go
sshinmen's picture
feat(perf): Add 4 performance features - token cache, compression, dedup, stream cache
e0c724f
Raw
History Blame Contribute Delete
6.75 kB
// 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
}