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 cache
 
import (
	"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
}