package memory import ( "runtime" "sync" "time" ) // Forked from https://github.com/patrickmn/go-cache // CacheWrapper is used to ensure that the underlying cleaner goroutine used to clean expired keys will not prevent // Cache from being garbage collected. type CacheWrapper struct { *Cache } // A cleaner will periodically delete expired keys from cache. type cleaner struct { interval time.Duration stop chan bool } // Run will periodically delete expired keys from given cache until GC notify that it should stop. func (cleaner *cleaner) Run(cache *Cache) { ticker := time.NewTicker(cleaner.interval) for { select { case <-ticker.C: cache.Clean() case <-cleaner.stop: ticker.Stop() return } } } // stopCleaner is a callback from GC used to stop cleaner goroutine. func stopCleaner(wrapper *CacheWrapper) { wrapper.cleaner.stop <- true } // startCleaner will start a cleaner goroutine for given cache. func startCleaner(cache *Cache, interval time.Duration) { cleaner := &cleaner{ interval: interval, stop: make(chan bool), } cache.cleaner = cleaner go cleaner.Run(cache) } // Counter is a simple counter with an optional expiration. type Counter struct { Value int64 Expiration int64 } // Expired returns true if the counter has expired. func (counter Counter) Expired() bool { if counter.Expiration == 0 { return false } return time.Now().UnixNano() > counter.Expiration } // Cache contains a collection of counters. type Cache struct { mutex sync.RWMutex counters map[string]Counter cleaner *cleaner } // NewCache returns a new cache. func NewCache(cleanInterval time.Duration) *CacheWrapper { cache := &Cache{ counters: map[string]Counter{}, } wrapper := &CacheWrapper{Cache: cache} if cleanInterval > 0 { startCleaner(cache, cleanInterval) runtime.SetFinalizer(wrapper, stopCleaner) } return wrapper } // Increment increments given value on key. // If key is undefined or expired, it will create it. func (cache *Cache) Increment(key string, value int64, duration time.Duration) (int64, time.Time) { cache.mutex.Lock() counter, ok := cache.counters[key] if !ok || counter.Expired() { expiration := time.Now().Add(duration).UnixNano() counter = Counter{ Value: value, Expiration: expiration, } cache.counters[key] = counter cache.mutex.Unlock() return value, time.Unix(0, expiration) } value = counter.Value + value counter.Value = value expiration := counter.Expiration cache.counters[key] = counter cache.mutex.Unlock() return value, time.Unix(0, expiration) } // Get returns key's value and expiration. func (cache *Cache) Get(key string, duration time.Duration) (int64, time.Time) { cache.mutex.RLock() counter, ok := cache.counters[key] if !ok || counter.Expired() { expiration := time.Now().Add(duration).UnixNano() cache.mutex.RUnlock() return 0, time.Unix(0, expiration) } value := counter.Value expiration := counter.Expiration cache.mutex.RUnlock() return value, time.Unix(0, expiration) } // Clean will deleted any expired keys. func (cache *Cache) Clean() { now := time.Now().UnixNano() cache.mutex.Lock() for key, counter := range cache.counters { if now > counter.Expiration { delete(cache.counters, key) } } cache.mutex.Unlock() } // Reset changes the key's value and resets the expiration. func (cache *Cache) Reset(key string, duration time.Duration) (int64, time.Time) { cache.mutex.Lock() delete(cache.counters, key) cache.mutex.Unlock() expiration := time.Now().Add(duration).UnixNano() return 0, time.Unix(0, expiration) }