wormhole-explorer/api/cacheable/cacheable.go

80 lines
2.2 KiB
Go

package cacheable
import (
"context"
"encoding/json"
"time"
"github.com/wormhole-foundation/wormhole-explorer/api/internal/metrics"
"github.com/wormhole-foundation/wormhole-explorer/common/client/cache"
"go.uber.org/zap"
)
// GetOrLoad is a function that tries to get the result from the cache, if it is not found or it is expired, then it loads the result.
func GetOrLoad[T any](
ctx context.Context,
logger *zap.Logger,
cacheClient cache.Cache,
expirations time.Duration,
key string,
metrics metrics.Metrics,
load func() (T, error),
) (T, error) {
log := logger.With(zap.String("key", key))
// Try to get the result from the cache.
value, err := cacheClient.Get(ctx, key)
foundCache := true
//If the result is not found in the cache or fails, then load the result.
if err != nil {
foundCache = false
if err != cache.ErrNotFound {
log.Warn("getting result from cache", zap.Error(err))
}
}
var cached CachedResult[T]
//If the result is found in the cache and it is not expired, then return the result.
if foundCache {
err = json.Unmarshal([]byte(value), &cached)
if err != nil {
log.Warn("unmarshal cache", zap.Error(err))
} else if cached.Timestamp.Add(expirations).After(time.Now()) {
return cached.Result, nil
}
}
//If the result is not found in the cache or it is expired, then load the result.
result, err := load()
if err != nil {
//If the load function fails and the cache was found and is expired, the cache value is returned anyway.
if foundCache {
metrics.IncExpiredCacheResponse(key)
log.Warn("load function fails but returns cached result",
zap.Error(err), zap.String("cacheTime", cached.Timestamp.String()))
return cached.Result, nil
}
return result, err
}
//Saves the result of the execution of the load function in cache.
newValue := CachedResult[T]{Timestamp: time.Now(), Result: result}
err = cacheClient.Set(ctx, key, newValue, 0)
if err != nil {
log.Warn("saving the result in the cache", zap.Error(err))
}
//Returns the result of the execution of the function load
return result, nil
}
type CachedResult[T any] struct {
Timestamp time.Time `json:"timestamp"`
Result T `json:"result"`
}
func (c CachedResult[T]) MarshalBinary() ([]byte, error) {
return json.Marshal(c)
}