121 lines
3.0 KiB
Go
121 lines
3.0 KiB
Go
package routing
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// RuleCache is an in-memory, per-tenant cache of routing rules backed by a RuleStore.
|
|
// It performs lazy loading on first access and refreshes all loaded tenants every TTL.
|
|
// Mutations (Create/Update/Delete via admin API) should call Invalidate() for immediate
|
|
// consistency without waiting for the next refresh cycle.
|
|
type RuleCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]cacheEntry // tenantID → cached rules
|
|
|
|
store RuleStore
|
|
ttl time.Duration
|
|
stopCh chan struct{}
|
|
logger *zap.Logger
|
|
}
|
|
|
|
type cacheEntry struct {
|
|
rules []RoutingRule
|
|
loadedAt time.Time
|
|
}
|
|
|
|
// NewRuleCache creates a RuleCache wrapping store with the given TTL.
|
|
func NewRuleCache(store RuleStore, ttl time.Duration, logger *zap.Logger) *RuleCache {
|
|
if ttl <= 0 {
|
|
ttl = 30 * time.Second
|
|
}
|
|
return &RuleCache{
|
|
entries: make(map[string]cacheEntry),
|
|
store: store,
|
|
ttl: ttl,
|
|
stopCh: make(chan struct{}),
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Start launches the background refresh goroutine.
|
|
// Call Stop() to shut it down gracefully.
|
|
func (c *RuleCache) Start() {
|
|
go c.refreshLoop()
|
|
}
|
|
|
|
// Stop signals the background goroutine to stop.
|
|
func (c *RuleCache) Stop() {
|
|
close(c.stopCh)
|
|
}
|
|
|
|
// Get returns active rules for tenantID, loading from the store on cache miss
|
|
// or when the entry is older than the TTL.
|
|
func (c *RuleCache) Get(ctx context.Context, tenantID string) ([]RoutingRule, error) {
|
|
c.mu.RLock()
|
|
entry, ok := c.entries[tenantID]
|
|
c.mu.RUnlock()
|
|
|
|
if ok && time.Since(entry.loadedAt) < c.ttl {
|
|
return entry.rules, nil
|
|
}
|
|
return c.load(ctx, tenantID)
|
|
}
|
|
|
|
// Invalidate removes the cached rules for tenantID, forcing a reload on next Get().
|
|
// Call this after any Create/Update/Delete operation on routing rules.
|
|
func (c *RuleCache) Invalidate(tenantID string) {
|
|
c.mu.Lock()
|
|
delete(c.entries, tenantID)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// load fetches rules from the store and updates the cache entry.
|
|
func (c *RuleCache) load(ctx context.Context, tenantID string) ([]RoutingRule, error) {
|
|
rules, err := c.store.ListActive(ctx, tenantID)
|
|
if err != nil {
|
|
c.logger.Error("rule cache: failed to load rules", zap.String("tenant_id", tenantID), zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
c.mu.Lock()
|
|
c.entries[tenantID] = cacheEntry{rules: rules, loadedAt: time.Now()}
|
|
c.mu.Unlock()
|
|
|
|
c.logger.Debug("rule cache: loaded rules", zap.String("tenant_id", tenantID), zap.Int("count", len(rules)))
|
|
return rules, nil
|
|
}
|
|
|
|
// refreshLoop periodically reloads all cached tenants.
|
|
func (c *RuleCache) refreshLoop() {
|
|
ticker := time.NewTicker(c.ttl)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
c.refreshAll()
|
|
case <-c.stopCh:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *RuleCache) refreshAll() {
|
|
c.mu.RLock()
|
|
tenants := make([]string, 0, len(c.entries))
|
|
for tid := range c.entries {
|
|
tenants = append(tenants, tid)
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
for _, tid := range tenants {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
_, _ = c.load(ctx, tid) //nolint:errcheck — logged inside load()
|
|
cancel()
|
|
}
|
|
}
|