package cfgservice import ( "context" "errors" "sync" "time" ) // DefaultCacheTTL ist die dokumentierte Cache-Invalidierungszeit // (Akzeptanzkriterium 2 / Pruefung 2 in diesem Ticket bezieht sich auf die // Aenderungsnachvollziehbarkeit — die Cache-Frist selbst folgt demselben // Muster wie internal/flag.DefaultCacheTTL). const DefaultCacheTTL = 5 * time.Second type cacheEntry struct { value Value expiresAt time.Time } // Service ist die Leseseite mit Vorrangregel (Akzeptanzkriterium 1: // Tenant-Override vor Global-Default) und lokalem TTL-Cache. type Service struct { store *Store ttl time.Duration mu sync.RWMutex cache map[string]cacheEntry // Schluessel: key + "\x00" + tenantSlug } func NewService(store *Store, ttl time.Duration) *Service { if ttl <= 0 { ttl = DefaultCacheTTL } return &Service{store: store, ttl: ttl, cache: make(map[string]cacheEntry)} } func cacheKey(key, tenantSlug string) string { return key + "\x00" + tenantSlug } // Resolve liefert den Konfigurationswert fuer einen Tenant: ein // Tenant-spezifischer Override hat Vorrang vor dem globalen Default // (Akzeptanzkriterium 1 / Pruefung 1). tenantSlug == "" wertet nur den // globalen Wert aus. func (s *Service) Resolve(ctx context.Context, tenantSlug, key string) (Value, error) { ck := cacheKey(key, tenantSlug) s.mu.RLock() entry, exists := s.cache[ck] fresh := exists && time.Now().Before(entry.expiresAt) s.mu.RUnlock() if fresh { return entry.value, nil } v, err := s.resolveUncached(ctx, tenantSlug, key) if err != nil { return Value{}, err } s.mu.Lock() s.cache[ck] = cacheEntry{value: v, expiresAt: time.Now().Add(s.ttl)} s.mu.Unlock() return v, nil } func (s *Service) resolveUncached(ctx context.Context, tenantSlug, key string) (Value, error) { if tenantSlug != "" { v, err := s.store.Get(ctx, key, tenantSlug) if err == nil { return v, nil } if !errors.Is(err, ErrNotFound) { return Value{}, err } } return s.store.Get(ctx, key, GlobalScope) } // Invalidate erzwingt beim naechsten Resolve-Aufruf ein sofortiges Neuladen // fuer einen bestimmten (key, tenantSlug) statt auf den TTL-Ablauf zu warten // — analog internal/flag.Service.Invalidate. func (s *Service) Invalidate(key, tenantSlug string) { s.mu.Lock() delete(s.cache, cacheKey(key, tenantSlug)) s.mu.Unlock() }