Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
# 2.11.0 (December 2021)
#64: @DoubeDi added a method `GetItems` to retrieve all items in the cache. This method also triggers all callbacks associated with a normal `Get`
## API changes:
// GetItems returns a copy of all items in the cache. Returns nil when the cache has been closed.
func (cache *Cache) GetItems() map[string]interface{} {
# 2.10.0 (December 2021)
#62 : @nikhilk1701 found a memory leak where removed items are not directly eligible for garbage collection. There are no API changes.
# 2.9.0 (October 2021)
#55,#56,#57 : @chenyahui was on fire and greatly improved the peformance of the library. He also got rid of the blocking call to expirationNotification, making the code run twice as fast in the benchmarks!
# 2.8.1 (September 2021)
#53 : Avoids recalculation of TTL value returned in API when TTL is extended. by @iczc
# 2.8.0 (August 2021)
#51 : The call GetWithTTL(key string) (interface{}, time.Duration, error) is added so that you can retrieve an item, and also know the remaining TTL. Thanks to @asgarciap for contributing.
# 2.7.0 (June 2021)
#46 : got panic
A panic occured in a line that checks the maximum amount of items in the cache. While not definite root cause has been found, there is indeed the possibility of crashing an empty cache if the cache limit is set to 'zero' which codes for infinite. This would lead to removal of the first item in the cache which would panic on an empty cache.
Fixed this by applying the global cache lock to all configuration options as well.
# 2.6.0 (May 2021)
#44 : There are no API changes, but a contribution was made to use https://pkg.go.dev/golang.org/x/sync/singleflight as a way to provide everybody waiting for a key with that key when it's fetched.
This removes some complexity from the code and will make sure that all callers will get a return value even if there's high concurrency and low TTL (as proven by the test that was added).
# 2.5.0 (May 2021)
## API changes:
* #39 : Allow custom loader function for each key via `GetByLoader`
Introduce the `SimpleCache` interface for quick-start and basic usage.
# 2.4.0 (April 2021)
## API changes:
* #42 : Add option to get list of keys
* #40: Allow 'Touch' on items without other operation
// Touch resets the TTL of the key when it exists, returns ErrNotFound if the key is not present.
func (cache *Cache) Touch(key string) error
// GetKeys returns all keys of items in the cache. Returns nil when the cache has been closed.
func (cache *Cache) GetKeys() []string
# 2.3.0 (February 2021)
## API changes:
* #38: Added func (cache *Cache) SetExpirationReasonCallback(callback ExpireReasonCallback) This wil function will replace SetExpirationCallback(..) in the next major version.
# 2.2.0 (January 2021)
## API changes:
* #37 : a GetMetrics call is now available for some information on hits/misses etc.
* #34 : Errors are now const
# 2.1.0 (October 2020)
## API changes
* `SetCacheSizeLimit(limit int)` a call was contributed to set a cache limit. #35
# 2.0.0 (July 2020)
## Fixes #29, #30, #31
## Behavioural changes
* `Remove(key)` now also calls the expiration callback when it's set
* `Count()` returns zero when the cache is closed
## API changes
* `SetLoaderFunction` allows you to provide a function to retrieve data on missing cache keys.
* Operations that affect item behaviour such as `Close`, `Set`, `SetWithTTL`, `Get`, `Remove`, `Purge` now return an error with standard errors `ErrClosed` an `ErrNotFound` instead of a bool or nothing
* `SkipTTLExtensionOnHit` replaces `SkipTtlExtensionOnHit` to satisfy golint
* The callback types are now exported
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Jellydator
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+145
View File
@@ -0,0 +1,145 @@
# TTLCache - an in-memory cache with expiration
**Although v2 of ttlcache is not yet deprecated, v3 should be used as it
contains quite a few additions and improvements.**
TTLCache is a simple key/value cache in golang with the following functions:
1. Expiration of items based on time, or custom function
2. Loader function to retrieve missing keys can be provided. Additional `Get` calls on the same key block while fetching is in progress (groupcache style).
3. Individual expiring time or global expiring time, you can choose
4. Auto-Extending expiration on `Get` -or- DNS style TTL, see `SkipTTLExtensionOnHit(bool)`
5. Can trigger callback on key expiration
6. Cleanup resources by calling `Close()` at end of lifecycle.
7. Thread-safe with comprehensive testing suite. This code is in production at bol.com on critical systems.
Note (issue #25): by default, due to historic reasons, the TTL will be reset on each cache hit and you need to explicitly configure the cache to use a TTL that will not get extended.
## Usage
`go get github.com/jellydator/ttlcache/v2`
You can copy it as a full standalone demo program. The first snippet is basic usage, where the second exploits more options in the cache.
Basic:
```go
package main
import (
"fmt"
"time"
"github.com/jellydator/ttlcache/v2"
)
var notFound = ttlcache.ErrNotFound
func main() {
var cache ttlcache.SimpleCache = ttlcache.NewCache()
cache.SetTTL(time.Duration(10 * time.Second))
cache.Set("MyKey", "MyValue")
cache.Set("MyNumber", 1000)
if val, err := cache.Get("MyKey"); err != notFound {
fmt.Printf("Got it: %s\n", val)
}
cache.Remove("MyNumber")
cache.Purge()
cache.Close()
}
```
Advanced:
```go
package main
import (
"fmt"
"time"
"github.com/jellydator/ttlcache/v2"
)
var (
notFound = ttlcache.ErrNotFound
isClosed = ttlcache.ErrClosed
)
func main() {
newItemCallback := func(key string, value interface{}) {
fmt.Printf("New key(%s) added\n", key)
}
checkExpirationCallback := func(key string, value interface{}) bool {
if key == "key1" {
// if the key equals "key1", the value
// will not be allowed to expire
return false
}
// all other values are allowed to expire
return true
}
expirationCallback := func(key string, reason ttlcache.EvictionReason, value interface{}) {
fmt.Printf("This key(%s) has expired because of %s\n", key, reason)
}
loaderFunction := func(key string) (data interface{}, ttl time.Duration, err error) {
ttl = time.Second * 300
data, err = getFromNetwork(key)
return data, ttl, err
}
cache := ttlcache.NewCache()
cache.SetTTL(time.Duration(10 * time.Second))
cache.SetExpirationReasonCallback(expirationCallback)
cache.SetLoaderFunction(loaderFunction)
cache.SetNewItemCallback(newItemCallback)
cache.SetCheckExpirationCallback(checkExpirationCallback)
cache.SetCacheSizeLimit(2)
cache.Set("key", "value")
cache.SetWithTTL("keyWithTTL", "value", 10*time.Second)
if value, exists := cache.Get("key"); exists == nil {
fmt.Printf("Got value: %v\n", value)
}
count := cache.Count()
if result := cache.Remove("keyNNN"); result == notFound {
fmt.Printf("Not found, %d items left\n", count)
}
cache.Set("key6", "value")
cache.Set("key7", "value")
metrics := cache.GetMetrics()
fmt.Printf("Total inserted: %d\n", metrics.Inserted)
cache.Close()
}
func getFromNetwork(key string) (string, error) {
time.Sleep(time.Millisecond * 30)
return "value", nil
}
```
### TTLCache - Some design considerations
1. The complexity of the current cache is already quite high. Therefore not all requests can be implemented in a straight-forward manner.
2. The locking should be done only in the exported functions and `startExpirationProcessing` of the Cache struct. Else data races can occur or recursive locks are needed, which are both unwanted.
3. I prefer correct functionality over fast tests. It's ok for new tests to take seconds to proof something.
### Original Project
TTLCache was forked from [wunderlist/ttlcache](https://github.com/wunderlist/ttlcache) to add extra functions not avaiable in the original scope.
The main differences are:
1. A item can store any kind of object, previously, only strings could be saved
2. Optionally, you can add callbacks too: check if a value should expire, be notified if a value expires, and be notified when new values are added to the cache
3. The expiration can be either global or per item
4. Items can exist without expiration time (time.Zero)
5. Expirations and callbacks are realtime. Don't have a pooling time to check anymore, now it's done with a heap.
6. A cache count limiter
+605
View File
@@ -0,0 +1,605 @@
package ttlcache
import (
"sync"
"time"
"golang.org/x/sync/singleflight"
)
// CheckExpireCallback is used as a callback for an external check on item expiration
type CheckExpireCallback func(key string, value interface{}) bool
// ExpireCallback is used as a callback on item expiration or when notifying of an item new to the cache
// Note that ExpireReasonCallback will be the successor of this function in the next major release.
type ExpireCallback func(key string, value interface{})
// ExpireReasonCallback is used as a callback on item expiration with extra information why the item expired.
type ExpireReasonCallback func(key string, reason EvictionReason, value interface{})
// LoaderFunction can be supplied to retrieve an item where a cache miss occurs. Supply an item specific ttl or Duration.Zero
type LoaderFunction func(key string) (data interface{}, ttl time.Duration, err error)
// SimpleCache interface enables a quick-start. Interface for basic usage.
type SimpleCache interface {
Get(key string) (interface{}, error)
GetWithTTL(key string) (interface{}, time.Duration, error)
Set(key string, data interface{}) error
SetTTL(ttl time.Duration) error
SetWithTTL(key string, data interface{}, ttl time.Duration) error
Remove(key string) error
Close() error
Purge() error
}
// Cache is a synchronized map of items that can auto-expire once stale
type Cache struct {
// mutex is shared for all operations that need to be safe
mutex sync.Mutex
// ttl is the global ttl for the cache, can be zero (is infinite)
ttl time.Duration
// actual item storage
items map[string]*item
// lock used to avoid fetching a remote item multiple times
loaderLock *singleflight.Group
expireCallback ExpireCallback
expireReasonCallback ExpireReasonCallback
checkExpireCallback CheckExpireCallback
newItemCallback ExpireCallback
// the queue is used to have an ordered structure to use for expiration and cleanup.
priorityQueue *priorityQueue
expirationNotification chan bool
// hasNotified is used to not schedule new expiration processing when an request is already pending.
hasNotified bool
expirationTime time.Time
skipTTLExtension bool
shutdownSignal chan (chan struct{})
isShutDown bool
loaderFunction LoaderFunction
sizeLimit int
metrics Metrics
}
// EvictionReason is an enum that explains why an item was evicted
type EvictionReason int
const (
// Removed : explicitly removed from cache via API call
Removed EvictionReason = iota
// EvictedSize : evicted due to exceeding the cache size
EvictedSize
// Expired : the time to live is zero and therefore the item is removed
Expired
// Closed : the cache was closed
Closed
)
const (
// ErrClosed is raised when operating on a cache where Close() has already been called.
ErrClosed = constError("cache already closed")
// ErrNotFound indicates that the requested key is not present in the cache
ErrNotFound = constError("key not found")
)
type constError string
func (err constError) Error() string {
return string(err)
}
func (cache *Cache) getItem(key string) (*item, bool, bool) {
item, exists := cache.items[key]
if !exists || item.expired() {
return nil, false, false
}
// no need to change priority queue when skipTTLExtension is true or the item will not expire
if cache.skipTTLExtension || (item.ttl == 0 && cache.ttl == 0) {
return item, true, false
}
if item.ttl == 0 {
item.ttl = cache.ttl
}
item.touch()
oldExpireTime := cache.priorityQueue.root().expireAt
cache.priorityQueue.update(item)
nowExpireTime := cache.priorityQueue.root().expireAt
expirationNotification := false
// notify expiration only if the latest expire time is changed
if (oldExpireTime.IsZero() && !nowExpireTime.IsZero()) || oldExpireTime.After(nowExpireTime) {
expirationNotification = true
}
return item, exists, expirationNotification
}
func (cache *Cache) startExpirationProcessing() {
timer := time.NewTimer(time.Hour)
for {
var sleepTime time.Duration
cache.mutex.Lock()
cache.hasNotified = false
if cache.priorityQueue.Len() > 0 {
sleepTime = time.Until(cache.priorityQueue.root().expireAt)
if sleepTime < 0 && cache.priorityQueue.root().expireAt.IsZero() {
sleepTime = time.Hour
} else if sleepTime < 0 {
sleepTime = time.Microsecond
}
if cache.ttl > 0 {
sleepTime = min(sleepTime, cache.ttl)
}
} else if cache.ttl > 0 {
sleepTime = cache.ttl
} else {
sleepTime = time.Hour
}
cache.expirationTime = time.Now().Add(sleepTime)
cache.mutex.Unlock()
timer.Reset(sleepTime)
select {
case shutdownFeedback := <-cache.shutdownSignal:
timer.Stop()
cache.mutex.Lock()
if cache.priorityQueue.Len() > 0 {
cache.evictjob(Closed)
}
cache.mutex.Unlock()
shutdownFeedback <- struct{}{}
return
case <-timer.C:
timer.Stop()
cache.mutex.Lock()
if cache.priorityQueue.Len() == 0 {
cache.mutex.Unlock()
continue
}
cache.cleanjob()
cache.mutex.Unlock()
case <-cache.expirationNotification:
timer.Stop()
continue
}
}
}
func (cache *Cache) checkExpirationCallback(item *item, reason EvictionReason) {
if cache.expireCallback != nil {
go cache.expireCallback(item.key, item.data)
}
if cache.expireReasonCallback != nil {
go cache.expireReasonCallback(item.key, reason, item.data)
}
}
func (cache *Cache) removeItem(item *item, reason EvictionReason) {
cache.metrics.Evicted++
cache.checkExpirationCallback(item, reason)
cache.priorityQueue.remove(item)
delete(cache.items, item.key)
}
func (cache *Cache) evictjob(reason EvictionReason) {
// index will only be advanced if the current entry will not be evicted
i := 0
for item := cache.priorityQueue.items[i]; ; item = cache.priorityQueue.items[i] {
cache.removeItem(item, reason)
if cache.priorityQueue.Len() == 0 {
return
}
}
}
func (cache *Cache) cleanjob() {
// index will only be advanced if the current entry will not be evicted
i := 0
for item := cache.priorityQueue.items[i]; item.expired(); item = cache.priorityQueue.items[i] {
if cache.checkExpireCallback != nil {
if !cache.checkExpireCallback(item.key, item.data) {
item.touch()
cache.priorityQueue.update(item)
i++
if i == cache.priorityQueue.Len() {
break
}
continue
}
}
cache.removeItem(item, Expired)
if cache.priorityQueue.Len() == 0 {
return
}
}
}
// Close calls Purge after stopping the goroutine that does ttl checking, for a clean shutdown.
// The cache is no longer cleaning up after the first call to Close, repeated calls are safe and return ErrClosed.
func (cache *Cache) Close() error {
cache.mutex.Lock()
if !cache.isShutDown {
cache.isShutDown = true
cache.mutex.Unlock()
feedback := make(chan struct{})
cache.shutdownSignal <- feedback
<-feedback
close(cache.shutdownSignal)
cache.Purge()
} else {
cache.mutex.Unlock()
return ErrClosed
}
return nil
}
// Set is a thread-safe way to add new items to the map.
func (cache *Cache) Set(key string, data interface{}) error {
return cache.SetWithTTL(key, data, ItemExpireWithGlobalTTL)
}
// SetWithTTL is a thread-safe way to add new items to the map with individual ttl.
func (cache *Cache) SetWithTTL(key string, data interface{}, ttl time.Duration) error {
cache.mutex.Lock()
if cache.isShutDown {
cache.mutex.Unlock()
return ErrClosed
}
item, exists, _ := cache.getItem(key)
oldExpireTime := time.Time{}
if !cache.priorityQueue.isEmpty() {
oldExpireTime = cache.priorityQueue.root().expireAt
}
if exists {
item.data = data
item.ttl = ttl
} else {
if cache.sizeLimit != 0 && len(cache.items) >= cache.sizeLimit {
cache.removeItem(cache.priorityQueue.items[0], EvictedSize)
}
item = newItem(key, data, ttl)
cache.items[key] = item
}
cache.metrics.Inserted++
if item.ttl == 0 {
item.ttl = cache.ttl
}
item.touch()
if exists {
cache.priorityQueue.update(item)
} else {
cache.priorityQueue.push(item)
}
nowExpireTime := cache.priorityQueue.root().expireAt
cache.mutex.Unlock()
if !exists && cache.newItemCallback != nil {
cache.newItemCallback(key, data)
}
// notify expiration only if the latest expire time is changed
if (oldExpireTime.IsZero() && !nowExpireTime.IsZero()) || oldExpireTime.After(nowExpireTime) {
cache.notifyExpiration()
}
return nil
}
// Get is a thread-safe way to lookup items
// Every lookup, also touches the item, hence extending its life
func (cache *Cache) Get(key string) (interface{}, error) {
return cache.GetByLoader(key, nil)
}
// GetWithTTL has exactly the same behaviour as Get but also returns
// the remaining TTL for a specific item at the moment its retrieved
func (cache *Cache) GetWithTTL(key string) (interface{}, time.Duration, error) {
return cache.GetByLoaderWithTtl(key, nil)
}
// GetByLoader can take a per key loader function (i.e. to propagate context)
func (cache *Cache) GetByLoader(key string, customLoaderFunction LoaderFunction) (interface{}, error) {
dataToReturn, _, err := cache.GetByLoaderWithTtl(key, customLoaderFunction)
return dataToReturn, err
}
// GetByLoaderWithTtl can take a per key loader function (i.e. to propagate context)
func (cache *Cache) GetByLoaderWithTtl(key string, customLoaderFunction LoaderFunction) (interface{}, time.Duration, error) {
cache.mutex.Lock()
if cache.isShutDown {
cache.mutex.Unlock()
return nil, 0, ErrClosed
}
cache.metrics.Hits++
item, exists, triggerExpirationNotification := cache.getItem(key)
var dataToReturn interface{}
ttlToReturn := time.Duration(0)
if exists {
cache.metrics.Retrievals++
dataToReturn = item.data
if !cache.skipTTLExtension {
ttlToReturn = item.ttl
} else {
ttlToReturn = time.Until(item.expireAt)
}
if ttlToReturn < 0 {
ttlToReturn = 0
}
}
var err error
if !exists {
cache.metrics.Misses++
err = ErrNotFound
}
loaderFunction := cache.loaderFunction
if customLoaderFunction != nil {
loaderFunction = customLoaderFunction
}
if loaderFunction == nil || exists {
cache.mutex.Unlock()
}
if loaderFunction != nil && !exists {
type loaderResult struct {
data interface{}
ttl time.Duration
}
ch := cache.loaderLock.DoChan(key, func() (interface{}, error) {
// cache is not blocked during io
invokeData, ttl, err := cache.invokeLoader(key, loaderFunction)
lr := &loaderResult{
data: invokeData,
ttl: ttl,
}
return lr, err
})
cache.mutex.Unlock()
res := <-ch
dataToReturn = res.Val.(*loaderResult).data
ttlToReturn = res.Val.(*loaderResult).ttl
err = res.Err
}
if triggerExpirationNotification {
cache.notifyExpiration()
}
return dataToReturn, ttlToReturn, err
}
func (cache *Cache) notifyExpiration() {
cache.mutex.Lock()
if cache.hasNotified {
cache.mutex.Unlock()
return
}
cache.hasNotified = true
cache.mutex.Unlock()
cache.expirationNotification <- true
}
func (cache *Cache) invokeLoader(key string, loaderFunction LoaderFunction) (dataToReturn interface{}, ttl time.Duration, err error) {
dataToReturn, ttl, err = loaderFunction(key)
if err == nil {
err = cache.SetWithTTL(key, dataToReturn, ttl)
if err != nil {
dataToReturn = nil
}
}
return dataToReturn, ttl, err
}
// Remove removes an item from the cache if it exists, triggers expiration callback when set. Can return ErrNotFound if the entry was not present.
func (cache *Cache) Remove(key string) error {
cache.mutex.Lock()
defer cache.mutex.Unlock()
if cache.isShutDown {
return ErrClosed
}
object, exists := cache.items[key]
if !exists {
return ErrNotFound
}
cache.removeItem(object, Removed)
return nil
}
// Count returns the number of items in the cache. Returns zero when the cache has been closed.
func (cache *Cache) Count() int {
cache.mutex.Lock()
defer cache.mutex.Unlock()
if cache.isShutDown {
return 0
}
length := len(cache.items)
return length
}
// GetKeys returns all keys of items in the cache. Returns nil when the cache has been closed.
func (cache *Cache) GetKeys() []string {
cache.mutex.Lock()
defer cache.mutex.Unlock()
if cache.isShutDown {
return nil
}
keys := make([]string, len(cache.items))
i := 0
for k := range cache.items {
keys[i] = k
i++
}
return keys
}
// GetItems returns a copy of all items in the cache. Returns nil when the cache has been closed.
func (cache *Cache) GetItems() map[string]interface{} {
cache.mutex.Lock()
defer cache.mutex.Unlock()
if cache.isShutDown {
return nil
}
items := make(map[string]interface{}, len(cache.items))
for k := range cache.items {
item, exists, _ := cache.getItem(k)
if exists {
items[k] = item.data
}
}
return items
}
// SetTTL sets the global TTL value for items in the cache, which can be overridden at the item level.
func (cache *Cache) SetTTL(ttl time.Duration) error {
cache.mutex.Lock()
if cache.isShutDown {
cache.mutex.Unlock()
return ErrClosed
}
cache.ttl = ttl
cache.mutex.Unlock()
cache.notifyExpiration()
return nil
}
// SetExpirationCallback sets a callback that will be called when an item expires
func (cache *Cache) SetExpirationCallback(callback ExpireCallback) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.expireCallback = callback
}
// SetExpirationReasonCallback sets a callback that will be called when an item expires, includes reason of expiry
func (cache *Cache) SetExpirationReasonCallback(callback ExpireReasonCallback) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.expireReasonCallback = callback
}
// SetCheckExpirationCallback sets a callback that will be called when an item is about to expire
// in order to allow external code to decide whether the item expires or remains for another TTL cycle
func (cache *Cache) SetCheckExpirationCallback(callback CheckExpireCallback) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.checkExpireCallback = callback
}
// SetNewItemCallback sets a callback that will be called when a new item is added to the cache
func (cache *Cache) SetNewItemCallback(callback ExpireCallback) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.newItemCallback = callback
}
// SkipTTLExtensionOnHit allows the user to change the cache behaviour. When this flag is set to true it will
// no longer extend TTL of items when they are retrieved using Get, or when their expiration condition is evaluated
// using SetCheckExpirationCallback.
func (cache *Cache) SkipTTLExtensionOnHit(value bool) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.skipTTLExtension = value
}
// SetLoaderFunction allows you to set a function to retrieve cache misses. The signature matches that of the Get function.
// Additional Get calls on the same key block while fetching is in progress (groupcache style).
func (cache *Cache) SetLoaderFunction(loader LoaderFunction) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.loaderFunction = loader
}
// Purge will remove all entries
func (cache *Cache) Purge() error {
cache.mutex.Lock()
defer cache.mutex.Unlock()
if cache.isShutDown {
return ErrClosed
}
cache.metrics.Evicted += int64(len(cache.items))
cache.items = make(map[string]*item)
cache.priorityQueue = newPriorityQueue()
return nil
}
// SetCacheSizeLimit sets a limit to the amount of cached items.
// If a new item is getting cached, the closes item to being timed out will be replaced
// Set to 0 to turn off
func (cache *Cache) SetCacheSizeLimit(limit int) {
cache.mutex.Lock()
defer cache.mutex.Unlock()
cache.sizeLimit = limit
}
// NewCache is a helper to create instance of the Cache struct
func NewCache() *Cache {
shutdownChan := make(chan chan struct{})
cache := &Cache{
items: make(map[string]*item),
loaderLock: &singleflight.Group{},
priorityQueue: newPriorityQueue(),
expirationNotification: make(chan bool, 1),
expirationTime: time.Now(),
shutdownSignal: shutdownChan,
isShutDown: false,
loaderFunction: nil,
sizeLimit: 0,
metrics: Metrics{},
}
go cache.startExpirationProcessing()
return cache
}
// GetMetrics exposes the metrics of the cache. This is a snapshot copy of the metrics.
func (cache *Cache) GetMetrics() Metrics {
cache.mutex.Lock()
defer cache.mutex.Unlock()
return cache.metrics
}
// Touch resets the TTL of the key when it exists, returns ErrNotFound if the key is not present.
func (cache *Cache) Touch(key string) error {
cache.mutex.Lock()
defer cache.mutex.Unlock()
item, exists := cache.items[key]
if !exists {
return ErrNotFound
}
item.touch()
return nil
}
func min(duration time.Duration, second time.Duration) time.Duration {
if duration < second {
return duration
}
return second
}
@@ -0,0 +1,52 @@
// Code generated by "enumer -type EvictionReason"; DO NOT EDIT.
//
package ttlcache
import (
"fmt"
)
const _EvictionReasonName = "RemovedEvictedSizeExpiredClosed"
var _EvictionReasonIndex = [...]uint8{0, 7, 18, 25, 31}
func (i EvictionReason) String() string {
if i < 0 || i >= EvictionReason(len(_EvictionReasonIndex)-1) {
return fmt.Sprintf("EvictionReason(%d)", i)
}
return _EvictionReasonName[_EvictionReasonIndex[i]:_EvictionReasonIndex[i+1]]
}
var _EvictionReasonValues = []EvictionReason{0, 1, 2, 3}
var _EvictionReasonNameToValueMap = map[string]EvictionReason{
_EvictionReasonName[0:7]: 0,
_EvictionReasonName[7:18]: 1,
_EvictionReasonName[18:25]: 2,
_EvictionReasonName[25:31]: 3,
}
// EvictionReasonString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func EvictionReasonString(s string) (EvictionReason, error) {
if val, ok := _EvictionReasonNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to EvictionReason values", s)
}
// EvictionReasonValues returns all values of the enum
func EvictionReasonValues() []EvictionReason {
return _EvictionReasonValues
}
// IsAEvictionReason returns "true" if the value is listed in the enum definition. "false" otherwise
func (i EvictionReason) IsAEvictionReason() bool {
for _, v := range _EvictionReasonValues {
if i == v {
return true
}
}
return false
}
+46
View File
@@ -0,0 +1,46 @@
package ttlcache
import (
"time"
)
const (
// ItemNotExpire Will avoid the item being expired by TTL, but can still be exired by callback etc.
ItemNotExpire time.Duration = -1
// ItemExpireWithGlobalTTL will use the global TTL when set.
ItemExpireWithGlobalTTL time.Duration = 0
)
func newItem(key string, data interface{}, ttl time.Duration) *item {
item := &item{
data: data,
ttl: ttl,
key: key,
}
// since nobody is aware yet of this item, it's safe to touch without lock here
item.touch()
return item
}
type item struct {
key string
data interface{}
ttl time.Duration
expireAt time.Time
queueIndex int
}
// Reset the item expiration time
func (item *item) touch() {
if item.ttl > 0 {
item.expireAt = time.Now().Add(item.ttl)
}
}
// Verify if the item is expired
func (item *item) expired() bool {
if item.ttl <= 0 {
return false
}
return item.expireAt.Before(time.Now())
}
+15
View File
@@ -0,0 +1,15 @@
package ttlcache
// Metrics contains common cache metrics so you can calculate hit and miss rates
type Metrics struct {
// succesful inserts
Inserted int64
// retrieval attempts
Retrievals int64
// all get calls that were in the cache (excludes loader invocations)
Hits int64
// entries not in cache (includes loader invocations)
Misses int64
// items removed from the cache in any way
Evicted int64
}
+85
View File
@@ -0,0 +1,85 @@
package ttlcache
import (
"container/heap"
)
func newPriorityQueue() *priorityQueue {
queue := &priorityQueue{}
heap.Init(queue)
return queue
}
type priorityQueue struct {
items []*item
}
func (pq *priorityQueue) isEmpty() bool {
return len(pq.items) == 0
}
func (pq *priorityQueue) root() *item {
if len(pq.items) == 0 {
return nil
}
return pq.items[0]
}
func (pq *priorityQueue) update(item *item) {
heap.Fix(pq, item.queueIndex)
}
func (pq *priorityQueue) push(item *item) {
heap.Push(pq, item)
}
func (pq *priorityQueue) pop() *item {
if pq.Len() == 0 {
return nil
}
return heap.Pop(pq).(*item)
}
func (pq *priorityQueue) remove(item *item) {
heap.Remove(pq, item.queueIndex)
}
func (pq priorityQueue) Len() int {
length := len(pq.items)
return length
}
// Less will consider items with time.Time default value (epoch start) as more than set items.
func (pq priorityQueue) Less(i, j int) bool {
if pq.items[i].expireAt.IsZero() {
return false
}
if pq.items[j].expireAt.IsZero() {
return true
}
return pq.items[i].expireAt.Before(pq.items[j].expireAt)
}
func (pq priorityQueue) Swap(i, j int) {
pq.items[i], pq.items[j] = pq.items[j], pq.items[i]
pq.items[i].queueIndex = i
pq.items[j].queueIndex = j
}
func (pq *priorityQueue) Push(x interface{}) {
item := x.(*item)
item.queueIndex = len(pq.items)
pq.items = append(pq.items, item)
}
func (pq *priorityQueue) Pop() interface{} {
old := pq.items
n := len(old)
item := old[n-1]
item.queueIndex = -1
// de-reference the element to be popped for Garbage Collector to de-allocate the memory
old[n-1] = nil
pq.items = old[0 : n-1]
return item
}