source-controller/internal/cache/cache.go

246 lines
5.4 KiB
Go

// Copyright (c) 2012-2019 Patrick Mylund Nielsen and the go-cache contributors
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// Copyright 2022 The FluxCD contributors. All rights reserved.
// This package provides an in-memory cache
// derived from the https://github.com/patrickmn/go-cache
// package
// It has been modified in order to keep a small set of functions
// and to add a maxItems parameter in order to limit the number of,
// and thus the size of, items in the cache.
package cache
import (
"fmt"
"runtime"
"sync"
"time"
)
// Cache is a thread-safe in-memory key/value store.
type Cache struct {
*cache
}
// Item is an item stored in the cache.
type Item struct {
// 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
// MaxItems is the maximum number of items the cache can hold.
MaxItems int
mu sync.RWMutex
janitor *janitor
}
// ItemCount returns the number of items in the cache.
// This may include items that have expired, but have not yet been cleaned up.
func (c *cache) ItemCount() int {
c.mu.RLock()
n := len(c.Items)
c.mu.RUnlock()
return n
}
func (c *cache) set(key string, value interface{}, expiration time.Duration) {
var e int64
if expiration > 0 {
e = time.Now().Add(expiration).UnixNano()
}
c.Items[key] = Item{
Object: value,
Expiration: e,
}
}
// Set adds an item to the cache, replacing any existing item.
// If expiration is zero, the item never expires.
// If the cache is full, Set will return an error.
func (c *cache) Set(key string, value interface{}, expiration time.Duration) error {
c.mu.Lock()
_, found := c.Items[key]
if found {
c.set(key, value, expiration)
c.mu.Unlock()
return nil
}
if c.MaxItems > 0 && len(c.Items) < c.MaxItems {
c.set(key, value, expiration)
c.mu.Unlock()
return nil
}
c.mu.Unlock()
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]
if found {
c.mu.Unlock()
return fmt.Errorf("Item %s already exists", key)
}
if c.MaxItems > 0 && len(c.Items) < c.MaxItems {
c.set(key, value, expiration)
c.mu.Unlock()
return nil
}
c.mu.Unlock()
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]
if !found {
c.mu.RUnlock()
return nil, false
}
if item.Expiration > 0 {
if item.Expiration < time.Now().UnixNano() {
c.mu.RUnlock()
return nil, false
}
}
c.mu.RUnlock()
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 reallocates the underlying 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]
if !ok {
c.mu.RUnlock()
return true
}
if item.Expiration > 0 {
if item.Expiration < time.Now().UnixNano() {
c.mu.RUnlock()
return true
}
}
c.mu.RUnlock()
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]
if ok {
item.Expiration = time.Now().Add(expiration).UnixNano()
c.Items[key] = item
}
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]
if !ok {
c.mu.RUnlock()
return 0
}
if item.Expiration > 0 {
if item.Expiration < time.Now().UnixNano() {
c.mu.RUnlock()
return 0
}
}
c.mu.RUnlock()
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 {
if v.Expiration > 0 && v.Expiration < time.Now().UnixNano() {
delete(c.Items, k)
}
}
c.mu.Unlock()
}
type janitor struct {
interval time.Duration
stop chan bool
}
func (j *janitor) run(c *cache) {
ticker := time.NewTicker(j.interval)
for {
select {
case <-ticker.C:
c.DeleteExpired()
case <-j.stop:
ticker.Stop()
return
}
}
}
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,
stop: make(chan bool),
},
}
C := &Cache{c}
if interval > 0 {
go c.janitor.run(c)
runtime.SetFinalizer(C, stopJanitor)
}
return C
}