Files
grafana/apps/dashboard/pkg/migration/schemaversion/cache.go
Rafael Bortolon Paulovic 8c6ccdd1ab feat(dashboard): Org-aware cache for schema migration (#115025)
* fix: use dsIndexProvider cache on migrations

* chore: use same comment as before

* feat: org-aware TTL cache for schemaversion migration and warmup for single tenant

* chore: use LRU cache

* chore: change DefaultCacheTTL to 1 minute

* chore: address copilot reviews

* chore: use expirable cache

* chore: remove unused import
2025-12-10 16:09:16 +01:00

105 lines
3.6 KiB
Go

package schemaversion
import (
"context"
"sync"
"time"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/hashicorp/golang-lru/v2/expirable"
k8srequest "k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
)
const defaultCacheSize = 1000
// CacheProvider is a generic cache interface for schema version providers.
type CacheProvider[T any] interface {
// Get returns the cached value if it's still valid, otherwise calls fetch and caches the result.
Get(ctx context.Context) T
}
// PreloadableCache is an interface for providers that support preloading the cache.
type PreloadableCache interface {
// Preload loads data into the cache for the given namespaces.
Preload(ctx context.Context, nsInfos []types.NamespaceInfo)
}
// cachedProvider is a thread-safe TTL cache that wraps any fetch function.
type cachedProvider[T any] struct {
fetch func(context.Context) T
cache *expirable.LRU[string, T] // LRU cache: namespace to cache entry
inFlight sync.Map // map[string]*sync.Mutex - per-namespace fetch locks
logger log.Logger
}
// newCachedProvider creates a new cachedProvider.
// The fetch function should be able to handle context with different namespaces.
// A non-positive size turns LRU mechanism off (cache of unlimited size).
// A non-positive cacheTTL disables TTL expiration.
func newCachedProvider[T any](fetch func(context.Context) T, size int, cacheTTL time.Duration, logger log.Logger) *cachedProvider[T] {
cacheProvider := &cachedProvider[T]{
fetch: fetch,
logger: logger,
}
cacheProvider.cache = expirable.NewLRU(size, func(key string, value T) {
cacheProvider.inFlight.Delete(key)
}, cacheTTL)
return cacheProvider
}
// Get returns the cached value if it's still valid, otherwise calls fetch and caches the result.
func (p *cachedProvider[T]) Get(ctx context.Context) T {
// Get namespace info from ctx
nsInfo, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
// No namespace, fall back to direct fetch call without caching
p.logger.Warn("Unable to get namespace info from context, skipping cache", "error", err)
return p.fetch(ctx)
}
namespace := nsInfo.Value
// Fast path: check if cache is still valid
if entry, ok := p.cache.Get(namespace); ok {
return entry
}
// Get or create a per-namespace lock for this fetch operation
// This ensures only one fetch happens per namespace at a time
lockInterface, _ := p.inFlight.LoadOrStore(namespace, &sync.Mutex{})
nsMutex := lockInterface.(*sync.Mutex)
// Lock this specific namespace - other namespaces can still proceed
nsMutex.Lock()
defer nsMutex.Unlock()
// Double-check: another goroutine might have already fetched while we waited
if entry, ok := p.cache.Get(namespace); ok {
return entry
}
// Fetch outside the main lock - only this namespace is blocked
p.logger.Debug("cache miss or expired, fetching new value", "namespace", namespace)
value := p.fetch(ctx)
// Update the cache for this namespace
p.cache.Add(namespace, value)
return value
}
// Preload loads data into the cache for the given namespaces.
func (p *cachedProvider[T]) Preload(ctx context.Context, nsInfos []types.NamespaceInfo) {
// Build the cache using a context with the namespace
p.logger.Info("preloading cache", "nsInfos", len(nsInfos))
startedAt := time.Now()
defer func() {
p.logger.Info("finished preloading cache", "nsInfos", len(nsInfos), "elapsed", time.Since(startedAt))
}()
for _, nsInfo := range nsInfos {
p.cache.Add(nsInfo.Value, p.fetch(k8srequest.WithNamespace(ctx, nsInfo.Value)))
}
}