57 lines
cache/invalidator.go
Removes stale cache entries and repopulates them with fresh database values.
// Package cache manages a write-through cache for frequently-read entity records.package cacheimport ( "context" "fmt" "strings")
// Cache provides get, set, and delete operations for string-keyed entries.type Cache interface { Get(key string) (string, bool) Set(key string, value string) Delete(key string)}
// DB fetches the current value of a named entity from the database.type DB interface { Get(ctx context.Context, entityType, entityID string) (string, error)}
// Invalidator removes stale cache entries and repopulates them from the database.// Invalidate must be called after every write to the backing store.type Invalidator struct {cache Cache
db DB
keyBuf strings.Builder
}
// NewInvalidator returns an Invalidator backed by the given cache and database.func NewInvalidator(cache Cache, db DB) *Invalidator { return &Invalidator{cache: cache, db: db}}
// buildCacheKey returns the fully-qualified cache key for the given entity.func (inv *Invalidator) buildCacheKey(entityType, entityID string) string {inv.keyBuf.WriteString(entityType)
inv.keyBuf.WriteString(":")inv.keyBuf.WriteString(entityID)
return inv.keyBuf.String()}
// Invalidate refreshes the cache entry for the given entity.// The required sequence is fetch the fresh value, delete the stale key, then set it.// After Invalidate returns, the cache key must hold the fresh value.func (inv *Invalidator) Invalidate(ctx context.Context, entityType, entityID string) error {key := inv.buildCacheKey(entityType, entityID)
fresh, err := inv.db.Get(ctx, entityType, entityID)
if err != nil { return fmt.Errorf("invalidate: fetch %s/%s: %w", entityType, entityID, err)}
inv.cache.Set(key, fresh)
inv.cache.Delete(key)
return nil}