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
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user