diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 56dd6676f..894eb99b6 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -463,10 +463,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * // Try to retrieve the repository index from the cache if r.Cache != nil { - if index, found := r.Cache.Get(r.Storage.LocalPath(*repo.GetArtifact())); err == nil { - if found { - chartRepo.Index = index.(*helmrepo.IndexFile) - } + if index, found := r.Cache.Get(r.Storage.LocalPath(*repo.GetArtifact())); found { + chartRepo.Index = index.(*helmrepo.IndexFile) } } @@ -502,7 +500,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * // Using r.Storage.LocalPath(*repo.GetArtifact() is safe as the path is in the format ///. err := r.Cache.Set(r.Storage.LocalPath(*repo.GetArtifact()), chartRepo.Index, r.TTL) if err != nil { - r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.CacheOperationFailedReason, "failed to cache index: %v", err) + r.eventLogf(ctx, obj, events.EventTypeTrace, sourcev1.CacheOperationFailedReason, "failed to cache index: %s", err) } } diff --git a/docs/spec/v1beta2/helmcharts.md b/docs/spec/v1beta2/helmcharts.md index 8f8f9800a..b423dde6d 100644 --- a/docs/spec/v1beta2/helmcharts.md +++ b/docs/spec/v1beta2/helmcharts.md @@ -390,6 +390,53 @@ Besides being reported in Events, the reconciliation errors are also logged by the controller. The Flux CLI offer commands for filtering the logs for a specific HelmChart, e.g. `flux logs --level=error --kind=HelmChart --name=`. +### Improving resource consumption by enabling the cache + +When using a `HelmRepository` as Source for a `HelmChart`, the controller loads +the repository index in memory to find the latest version of the chart. + +The controller can be configured to cache Helm repository indexes in memory. +The cache is used to avoid loading repository indexes for every `HelmChart` +reconciliation. + +The following flags are provided to enable and configure the cache: +- `helm-cache-max-size`: The maximum size of the cache in number of indexes. + If `0`, then the cache is disabled. +- `helm-cache-ttl`: The TTL of an index in the cache. +- `helm-cache-purge-interval`: The interval at which the cache is purged of + expired items. + +The caching strategy is to pull a repository index from the cache if it is +available, otherwise to load the index, retrieve and build the chart, +then cache the index. The cached index TTL is refreshed every time the +Helm repository index is loaded with the `helm-cache-ttl` value. + +The cache is purged of expired items every `helm-cache-purge-interval`. + +When the cache is full, no more items can be added to the cache, and the +source-controller will report a warning event instead. + +In order to use the cache, set the related flags in the source-controller +Deployment config: + +```yaml + spec: + containers: + - args: + - --watch-all-namespaces + - --log-level=info + - --log-encoding=json + - --enable-leader-election + - --storage-path=/data + - --storage-adv-addr=source-controller.$(RUNTIME_NAMESPACE).svc.cluster.local. + ## Helm cache with up to 10 items, i.e. 10 indexes. + - --helm-cache-max-size=10 + ## TTL of an index is 1 hour. + - --helm-cache-ttl=1h + ## Purge expired index every 10 minutes. + - --helm-cache-purge-interval=10m +``` + ## HelmChart Status ### Artifact diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 4673f4f2b..1c11f09d1 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -26,14 +26,16 @@ type Cache struct { // Item is an item stored in the cache. type Item struct { - Object interface{} + // Object is the item's value. + Object interface{} + // Expiration is the item's expiration time. Expiration int64 } type cache struct { // Items holds the elements in the cache. Items map[string]Item - // Maximum number of items the cache can hold. + // MaxItems is the maximum number of items the cache can hold. MaxItems int mu sync.RWMutex janitor *janitor @@ -82,6 +84,9 @@ func (c *cache) Set(key string, value interface{}, expiration time.Duration) err return fmt.Errorf("Cache is full") } +// Add an item to the cache, existing items will not be overwritten. +// To overwrite existing items, use Set. +// If the cache is full, Add will return an error. func (c *cache) Add(key string, value interface{}, expiration time.Duration) error { c.mu.Lock() _, found := c.Items[key] @@ -100,6 +105,8 @@ func (c *cache) Add(key string, value interface{}, expiration time.Duration) err return fmt.Errorf("Cache is full") } +// Get an item from the cache. Returns the item or nil, and a bool indicating +// whether the key was found. func (c *cache) Get(key string) (interface{}, bool) { c.mu.RLock() item, found := c.Items[key] @@ -117,18 +124,23 @@ func (c *cache) Get(key string) (interface{}, bool) { return item.Object, true } +// Delete an item from the cache. Does nothing if the key is not in the cache. func (c *cache) Delete(key string) { c.mu.Lock() delete(c.Items, key) c.mu.Unlock() } +// Clear all items from the cache. +// This reallocate the inderlying array holding the items, +// so that the memory used by the items is reclaimed. func (c *cache) Clear() { c.mu.Lock() c.Items = make(map[string]Item) c.mu.Unlock() } +// HasExpired returns true if the item has expired. func (c *cache) HasExpired(key string) bool { c.mu.RLock() item, ok := c.Items[key] @@ -146,6 +158,8 @@ func (c *cache) HasExpired(key string) bool { return false } +// SetExpiration sets the expiration for the given key. +// Does nothing if the key is not in the cache. func (c *cache) SetExpiration(key string, expiration time.Duration) { c.mu.Lock() item, ok := c.Items[key] @@ -157,6 +171,9 @@ func (c *cache) SetExpiration(key string, expiration time.Duration) { c.mu.Unlock() } +// GetExpiration returns the expiration for the given key. +// Returns zero if the key is not in the cache or the item +// has already expired. func (c *cache) GetExpiration(key string) time.Duration { c.mu.RLock() item, ok := c.Items[key] @@ -174,6 +191,7 @@ func (c *cache) GetExpiration(key string) time.Duration { return time.Duration(item.Expiration - time.Now().UnixNano()) } +// DeleteExpired deletes all expired items from the cache. func (c *cache) DeleteExpired() { c.mu.Lock() for k, v := range c.Items { @@ -185,12 +203,12 @@ func (c *cache) DeleteExpired() { } type janitor struct { - Interval time.Duration + interval time.Duration stop chan bool } -func (j *janitor) Run(c *cache) { - ticker := time.NewTicker(j.Interval) +func (j *janitor) run(c *cache) { + ticker := time.NewTicker(j.interval) for { select { case <-ticker.C: @@ -206,12 +224,13 @@ func stopJanitor(c *Cache) { c.janitor.stop <- true } +// New creates a new cache with the given configuration. func New(maxItems int, interval time.Duration) *Cache { c := &cache{ Items: make(map[string]Item), MaxItems: maxItems, janitor: &janitor{ - Interval: interval, + interval: interval, stop: make(chan bool), }, } @@ -219,7 +238,7 @@ func New(maxItems int, interval time.Duration) *Cache { C := &Cache{c} if interval > 0 { - go c.janitor.Run(c) + go c.janitor.run(c) runtime.SetFinalizer(C, stopJanitor) } diff --git a/main.go b/main.go index e24298360..1c398adc3 100644 --- a/main.go +++ b/main.go @@ -115,9 +115,9 @@ func main() { flag.DurationVar(&requeueDependency, "requeue-dependency", 30*time.Second, "The interval at which failing dependencies are reevaluated.") flag.IntVar(&helmCacheMaxSize, "helm-cache-max-size", 0, - "The maximum size of the cache in number of items.") + "The maximum size of the cache in number of indexes.") flag.StringVar(&helmCacheTTL, "helm-cache-ttl", "15m", - "The TTL of an item in the cache. Valid time units are ns, us (or µs), ms, s, m, h.") + "The TTL of an index in the cache. Valid time units are ns, us (or µs), ms, s, m, h.") flag.StringVar(&helmCachePurgeInterval, "helm-cache-purge-interval", "1m", "The interval at which the cache is purged. Valid time units are ns, us (or µs), ms, s, m, h.")