nettools/data_structures/cache.go

158 lines
3.5 KiB
Go

package dsUtils
import (
"context"
"sync"
"time"
)
type KeyValue struct {
Key string
Value []byte
}
type MaybeExpirableData struct {
Data []byte
ExpirationTime time.Time
CanExpire bool
}
type ToBeInvalidatedData struct {
Key string
ExpirationTime time.Time
}
type ICache interface {
Get(key string) (item []byte, isSet bool)
Set(data map[string][]byte, canExpire bool, updateExpirationTime bool)
Remove(key string)
SetIfNotExists(key string, value []byte, canExpire bool) (isSet bool)
GetAndRemove(key string) (item []byte, isSet bool)
Run(ctx context.Context) error
}
// Cache provides simple cache functionality
// All objects in the same cache instance are getting same TTL
type Cache struct {
ICache
newExpirableItem *sync.Cond
lock *sync.Mutex
data map[string]MaybeExpirableData
invalidateOrder []ToBeInvalidatedData
ttl time.Duration
}
// NewCache Creates new Cache instance
func NewCache(ttl time.Duration) *Cache {
return &Cache{
ttl: ttl,
newExpirableItem: &sync.Cond{L: &sync.Mutex{}},
lock: &sync.Mutex{},
data: make(map[string]MaybeExpirableData),
invalidateOrder: make([]ToBeInvalidatedData, 0),
}
}
func (c *Cache) get(key string) (item MaybeExpirableData, isSet bool) {
item, isSet = c.data[key]
if item.ExpirationTime.Before(time.Now()) {
delete(c.data, key)
return MaybeExpirableData{}, false
}
return item, isSet
}
func (c *Cache) Get(key string) (data []byte, isSet bool) {
c.lock.Lock()
defer c.lock.Unlock()
item, isSet := c.get(key)
return item.Data, isSet
}
func (c *Cache) GetAndRemove(key string) (data []byte, isSet bool) {
c.lock.Lock()
item, isSet := c.get(key)
delete(c.data, key)
c.lock.Unlock()
return item.Data, isSet
}
// Internal function
func (c *Cache) set(key string, value []byte, canExpire bool, expirationTime time.Time) {
c.data[key] = MaybeExpirableData{
Data: value,
ExpirationTime: expirationTime,
CanExpire: canExpire,
}
if canExpire {
c.invalidateOrder = append(c.invalidateOrder, ToBeInvalidatedData{
Key: key,
ExpirationTime: expirationTime,
})
c.newExpirableItem.Signal()
}
}
func (c *Cache) Set(data map[string][]byte, canExpire bool, updateExpirationTime bool) {
c.lock.Lock()
for key, val := range data {
if !updateExpirationTime {
if item, exists := c.data[key]; exists {
c.set(key, val, item.CanExpire, item.ExpirationTime)
return
}
}
c.set(key, val, canExpire, time.Now().Add(c.ttl))
}
c.lock.Unlock()
}
func (c *Cache) Remove(key string) {
c.lock.Lock()
delete(c.data, key)
c.lock.Unlock()
}
func (c *Cache) SetIfNotExists(key string, value []byte, canExpire bool) (isSet bool) {
c.lock.Lock()
defer c.lock.Unlock()
_, exists := c.data[key]
if exists {
return false
}
c.set(key, value, canExpire, time.Now().Add(c.ttl))
return true
}
func (c *Cache) timeInvalidate() {
c.lock.Lock()
if len(c.invalidateOrder) == 0 {
c.lock.Unlock()
c.newExpirableItem.Wait()
c.lock.Lock()
}
toBeInvalidated := c.invalidateOrder[0]
c.lock.Unlock()
time.After(time.Now().Sub(toBeInvalidated.ExpirationTime))
c.lock.Lock()
item, exists := c.data[toBeInvalidated.Key]
if exists && item.CanExpire {
if item.ExpirationTime.After(time.Now()) {
delete(c.data, toBeInvalidated.Key)
}
}
c.invalidateOrder = c.invalidateOrder[1:]
c.lock.Unlock()
}
func (c *Cache) Run(ctx context.Context) error {
// Invalidator loop
go func() {
for {
c.timeInvalidate()
}
}()
<-ctx.Done()
return ctx.Err()
}