88 lines
2.5 KiB
Go
88 lines
2.5 KiB
Go
package flag
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// DefaultCacheTTL ist die dokumentierte Cache-Invalidierungszeit
|
|
// (Akzeptanzkriterium 2/3): eine Aenderung wirkt spaetestens nach dieser
|
|
// Zeit auf allen Core-Instanzen, ohne dass ein Dienst neu gestartet werden
|
|
// muss (Akzeptanzkriterium 3).
|
|
const DefaultCacheTTL = 5 * time.Second
|
|
|
|
type cacheEntry struct {
|
|
flag Flag
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// Service ist die Auswertungsseite (SDK/Client-Analogon zu Unleash) mit
|
|
// lokalem TTL-Cache. Bewusst getrennt von Store (Verwaltung).
|
|
type Service struct {
|
|
store *Store
|
|
ttl time.Duration
|
|
|
|
mu sync.RWMutex
|
|
cache map[string]cacheEntry
|
|
}
|
|
|
|
func NewService(store *Store, ttl time.Duration) *Service {
|
|
if ttl <= 0 {
|
|
ttl = DefaultCacheTTL
|
|
}
|
|
return &Service{store: store, ttl: ttl, cache: make(map[string]cacheEntry)}
|
|
}
|
|
|
|
// IsEnabled wertet ein Flag fuer einen Tenant aus. Liefert IMMER einen
|
|
// bool ohne Fehlerwert — ein nicht erreichbarer Flag-Dienst darf abhaengige
|
|
// Aufrufer nicht zum Absturz bringen oder zu Fehlerbehandlungscode zwingen,
|
|
// der leicht vergessen wird (Akzeptanzkriterium 3 / Pruefung 3: dokumentiertes
|
|
// Fallback-Verhalten = false, ggf. aus dem zuletzt bekannten Zwischenspeicher).
|
|
func (s *Service) IsEnabled(ctx context.Context, tenantSlug, key string) bool {
|
|
f, ok := s.resolve(ctx, key)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return evaluate(f, tenantSlug)
|
|
}
|
|
|
|
func (s *Service) resolve(ctx context.Context, key string) (Flag, bool) {
|
|
s.mu.RLock()
|
|
entry, exists := s.cache[key]
|
|
fresh := exists && time.Now().Before(entry.expiresAt)
|
|
s.mu.RUnlock()
|
|
if fresh {
|
|
return entry.flag, true
|
|
}
|
|
|
|
f, err := s.store.Get(ctx, key)
|
|
if err != nil {
|
|
if exists {
|
|
slog.Warn("feature-flag-dienst nicht erreichbar, nutze zwischengespeicherten stand",
|
|
"flag_key", key, "error", err)
|
|
return entry.flag, true
|
|
}
|
|
slog.Warn("feature-flag-dienst nicht erreichbar, kein zwischengespeicherter stand vorhanden, fallback: deaktiviert",
|
|
"flag_key", key, "error", err)
|
|
return Flag{}, false
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.cache[key] = cacheEntry{flag: f, expiresAt: time.Now().Add(s.ttl)}
|
|
s.mu.Unlock()
|
|
return f, true
|
|
}
|
|
|
|
// Invalidate erzwingt beim naechsten IsEnabled-Aufruf ein sofortiges Neuladen
|
|
// aus der Datenbank statt auf den TTL-Ablauf zu warten — wird nach Store.Set
|
|
// auf derselben Instanz aufgerufen, damit der Schreiber die eigene Aenderung
|
|
// ohne Wartezeit sieht. Andere Core-Instanzen sehen sie spaetestens nach
|
|
// DefaultCacheTTL (siehe Akzeptanzkriterium 3).
|
|
func (s *Service) Invalidate(key string) {
|
|
s.mu.Lock()
|
|
delete(s.cache, key)
|
|
s.mu.Unlock()
|
|
}
|