harbor/src/chartserver/cache.go

230 lines
5.7 KiB
Go

package chartserver
import (
"context"
"encoding/json"
"errors"
"math"
"time"
beego_cache "github.com/beego/beego/v2/client/cache"
// Enable redis cache adaptor
_ "github.com/beego/beego/v2/client/cache/redis"
hlog "github.com/goharbor/harbor/src/lib/log"
)
const (
standardExpireTime = 3600 * time.Second
redisENVKey = "_REDIS_URL_CORE"
cacheDriverENVKey = "CHART_CACHE_DRIVER" // "memory" or "redis"
cacheDriverMem = "memory"
cacheDriverRedis = "redis"
cacheDriverRedisSentinel = "redis_sentinel"
cacheCollectionName = "helm_chart_cache"
maxTry = 10
)
// ChartCache is designed to cache some processed data for repeated accessing
// to improve the performance
type ChartCache struct {
// Cache driver
cache beego_cache.Cache
// Keep the driver type
driverType string
// To indicate if the chart cache is enabled
isEnabled bool
}
// ChartCacheConfig keeps the configurations of ChartCache
type ChartCacheConfig struct {
// Only support 'in-memory' and 'redis' now
DriverType string
// Align with config
Config string
}
// NewChartCache is constructor of ChartCache
// If return nil, that means no cache is enabled for chart repository server
func NewChartCache(config *ChartCacheConfig) *ChartCache {
// Never return nil object
chartCache := &ChartCache{
isEnabled: false,
}
// Double check the configurations are what we want
if config == nil {
return chartCache
}
if config.DriverType != cacheDriverMem && config.DriverType != cacheDriverRedis && config.DriverType != cacheDriverRedisSentinel {
return chartCache
}
if config.DriverType == cacheDriverRedis || config.DriverType == cacheDriverRedisSentinel {
if len(config.Config) == 0 {
return chartCache
}
}
// Try to create the upstream cache
cache := initCacheDriver(config)
if cache == nil {
return chartCache
}
// Cache enabled
chartCache.isEnabled = true
chartCache.driverType = config.DriverType
chartCache.cache = cache
return chartCache
}
// IsEnabled to indicate if the chart cache is successfully enabled
// The cache may be disabled if
//
// user does not set
// wrong configurations
func (chc *ChartCache) IsEnabled() bool {
return chc.isEnabled
}
// PutChart caches the detailed data of chart version
func (chc *ChartCache) PutChart(chart *ChartVersionDetails) {
ctx := context.Background()
// If cache is not enabled, do nothing
if !chc.IsEnabled() {
return
}
// As it's a valid json data anymore when retrieving back from redis cache,
// here we use separate methods to handle the data according to the driver type
if chart != nil {
var err error
switch chc.driverType {
case cacheDriverMem:
// Directly put object in
err = chc.cache.Put(ctx, chart.Metadata.Digest, chart, standardExpireTime)
case cacheDriverRedis, cacheDriverRedisSentinel:
// Marshal to json data before saving
var jsonData []byte
if jsonData, err = json.Marshal(chart); err == nil {
err = chc.cache.Put(ctx, chart.Metadata.Digest, jsonData, standardExpireTime)
}
default:
// Should not reach here, but still put guard code here
err = errors.New("meet invalid cache driver")
}
if err != nil {
// Just logged
hlog.Errorf("Failed to cache chart object with error: %s\n", err)
hlog.Warningf("If cache driver is using 'redis', please check the related configurations or the network connection")
}
}
}
// GetChart trys to retrieve it from the cache
// If hit, return the cached item;
// otherwise, nil object is returned
func (chc *ChartCache) GetChart(chartDigest string) *ChartVersionDetails {
// If cache is not enabled, do nothing
ctx := context.Background()
if !chc.IsEnabled() {
return nil
}
object, err := chc.cache.Get(ctx, chartDigest)
if err != nil {
hlog.Warningf("Failed to get cache value by key with error: %s", err)
}
if object != nil {
// Try to convert data
// First try the normal way
if chartDetails, ok := object.(*ChartVersionDetails); ok {
return chartDetails
}
// Maybe json bytes
if bytes, yes := object.([]byte); yes {
chartDetails := &ChartVersionDetails{}
err := json.Unmarshal(bytes, chartDetails)
if err == nil {
return chartDetails
}
// Just logged the error
hlog.Errorf("Failed to retrieve chart from cache with error: %s", err)
}
}
return nil
}
// Initialize the cache driver based on the config
func initCacheDriver(cacheConfig *ChartCacheConfig) beego_cache.Cache {
switch cacheConfig.DriverType {
case cacheDriverMem:
hlog.Info("Enable memory cache for chart caching")
return beego_cache.NewMemoryCache()
case cacheDriverRedis:
// New with retry
count := 0
for {
count++
redisCache, err := beego_cache.NewCache(cacheDriverRedis, cacheConfig.Config)
if err != nil {
// Just logged
hlog.Errorf("Failed to initialize redis cache: %s", err)
if count < maxTry {
<-time.After(time.Duration(backoff(count)) * time.Second)
continue
}
return nil
}
hlog.Info("Enable redis cache for chart caching")
return redisCache
}
case cacheDriverRedisSentinel:
// New with retry
count := 0
for {
count++
redisCache, err := beego_cache.NewCache(cacheDriverRedisSentinel, cacheConfig.Config)
if err != nil {
// Just logged
hlog.Errorf("Failed to initialize redis sentinel cache: %s", err)
if count < maxTry {
<-time.After(time.Duration(backoff(count)) * time.Second)
continue
}
return nil
}
hlog.Info("Enable redis sentinel cache for chart caching")
return redisCache
}
default:
break
}
// Any other cases
hlog.Info("No cache is enabled for chart caching")
return nil
}
// backoff: fast->slow->fast
func backoff(count int) int {
f := 5 - math.Abs((float64)(count)-5)
return (int)(math.Pow(2, f))
}