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
}
+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.
+180
View File
@@ -0,0 +1,180 @@
## TTLCache - an in-memory cache with item expiration and generics
[![Go Reference](https://pkg.go.dev/badge/github.com/jellydator/ttlcache/v3.svg)](https://pkg.go.dev/github.com/jellydator/ttlcache/v3)
[![Build Status](https://github.com/jellydator/ttlcache/actions/workflows/go.yml/badge.svg)](https://github.com/jellydator/ttlcache/actions/workflows/go.yml)
[![Coverage Status](https://coveralls.io/repos/github/jellydator/ttlcache/badge.svg?branch=master)](https://coveralls.io/github/jellydator/ttlcache?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/jellydator/ttlcache/v3)](https://goreportcard.com/report/github.com/jellydator/ttlcache/v3)
## Features
- Simple API
- Type parameters
- Item expiration and automatic deletion
- Automatic expiration time extension on each `Get` call
- `Loader` interface that may be used to load/lazily initialize missing cache items
- Thread safety
- Event handlers (insertion, update, and eviction)
- Metrics
## Installation
```
go get github.com/jellydator/ttlcache/v3
```
## Status
The `ttlcache` package is stable and used by [Jellydator](https://jellydator.com/),
as well as thousands of other projects and organizations in production.
## Usage
The main type of `ttlcache` is `Cache`. It represents a single
in-memory data store.
To create a new instance of `ttlcache.Cache`, the `ttlcache.New()` function
should be called:
```go
func main() {
cache := ttlcache.New[string, string]()
}
```
Note that by default, a new cache instance does not let any of its
items to expire or be automatically deleted. However, this feature
can be activated by passing a few additional options into the
`ttlcache.New()` function and calling the `cache.Start()` method:
```go
func main() {
cache := ttlcache.New[string, string](
ttlcache.WithTTL[string, string](30 * time.Minute),
)
go cache.Start() // starts automatic expired item deletion
}
```
Even though the `cache.Start()` method handles expired item deletion well,
there may be times when the system that uses `ttlcache` needs to determine
when to delete the expired items itself. For example, it may need to
delete them only when the resource load is at its lowest (e.g., after
midnight, when the number of users/HTTP requests drops). So, in situations
like these, instead of calling `cache.Start()`, the system could
periodically call `cache.DeleteExpired()`:
```go
func main() {
cache := ttlcache.New[string, string](
ttlcache.WithTTL[string, string](30 * time.Minute),
)
for {
time.Sleep(4 * time.Hour)
cache.DeleteExpired()
}
}
```
The data stored in `ttlcache.Cache` can be retrieved, checked and updated with
`Set`, `Get`, `Delete`, `Has` etc. methods:
```go
func main() {
cache := ttlcache.New[string, string](
ttlcache.WithTTL[string, string](30 * time.Minute),
)
// insert data
cache.Set("first", "value1", ttlcache.DefaultTTL)
cache.Set("second", "value2", ttlcache.NoTTL)
cache.Set("third", "value3", ttlcache.DefaultTTL)
// retrieve data
item := cache.Get("first")
fmt.Println(item.Value(), item.ExpiresAt())
// check key
ok := cache.Has("third")
// delete data
cache.Delete("second")
cache.DeleteExpired()
cache.DeleteAll()
// retrieve data if in cache otherwise insert data
item, retrieved := cache.GetOrSet("fourth", "value4", WithTTL[string, string](ttlcache.DefaultTTL))
// retrieve and delete data
item, present := cache.GetAndDelete("fourth")
}
```
To subscribe to insertion, update and eviction events, `cache.OnInsertion()`, `cache.OnUpdate()` and
`cache.OnEviction()` methods should be used:
```go
func main() {
cache := ttlcache.New[string, string](
ttlcache.WithTTL[string, string](30 * time.Minute),
ttlcache.WithCapacity[string, string](300),
)
cache.OnInsertion(func(ctx context.Context, item *ttlcache.Item[string, string]) {
fmt.Println(item.Value(), item.ExpiresAt())
})
cache.OnUpdate(func(ctx context.Context, item *ttlcache.Item[string, string]) {
fmt.Println(item.Value(), item.ExpiresAt())
})
cache.OnEviction(func(ctx context.Context, reason ttlcache.EvictionReason, item *ttlcache.Item[string, string]) {
if reason == ttlcache.EvictionReasonCapacityReached {
fmt.Println(item.Key(), item.Value())
}
})
cache.Set("first", "value1", ttlcache.DefaultTTL)
cache.DeleteAll()
}
```
To load data when the cache does not have it, a custom or
existing implementation of `ttlcache.Loader` can be used:
```go
func main() {
loader := ttlcache.LoaderFunc[string, string](
func(c *ttlcache.Cache[string, string], key string) *ttlcache.Item[string, string] {
// load from file/make an HTTP request
item := c.Set("key from file", "value from file")
return item
},
)
cache := ttlcache.New[string, string](
ttlcache.WithLoader[string, string](loader),
)
item := cache.Get("key from file")
}
```
To restrict the cache's capacity based on criteria beyond the number
of items it can hold, the `ttlcache.WithMaxCost` option allows for
implementing custom strategies. The following example shows how to limit
memory usage for cached entries to ~5KiB.
```go
import (
"github.com/jellydator/ttlcache"
)
func main() {
cache := ttlcache.New[string, string](
ttlcache.WithMaxCost[string, string](5120, func(item ttlcache.CostItem[string, string]) uint64 {
// Note: The below line doesn't include memory used by internal
// structures or string metadata for the key and the value.
return len(item.Key) + len(item.Value)
}),
)
cache.Set("first", "value1", ttlcache.DefaultTTL)
}
```
## Examples & Tutorials
See the [example](https://github.com/jellydator/ttlcache/tree/v3/examples)
directory for applications demonstrating how to use `ttlcache`.
If you want to learn and follow along as these example applications are
built, check out the tutorials below:
- [Speeding Up HTTP Endpoints with Response Caching in Go](https://jellydator.com/blog/speeding-up-http-endpoints-with-response-caching-in-go/)
+902
View File
@@ -0,0 +1,902 @@
package ttlcache
import (
"container/list"
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
// Available eviction reasons.
const (
EvictionReasonDeleted EvictionReason = iota + 1
EvictionReasonCapacityReached
EvictionReasonExpired
EvictionReasonMaxCostExceeded
)
// EvictionReason is used to specify why a certain item was
// evicted/deleted.
type EvictionReason int
// Cache is a synchronised map of items that are automatically removed
// when they expire or the capacity is reached.
type Cache[K comparable, V any] struct {
items struct {
mu sync.RWMutex
values map[K]*list.Element
// a generic doubly linked list would be more convenient
// (and more performant?). It's possible that this
// will be introduced with/in go1.19+
lru *list.List
expQueue expirationQueue[K, V]
timerCh chan time.Duration
}
cost uint64
metricsMu sync.RWMutex
metrics Metrics
events struct {
insertion struct {
mu sync.RWMutex
nextID uint64
fns map[uint64]func(*Item[K, V])
}
update struct {
mu sync.RWMutex
nextID uint64
fns map[uint64]func(*Item[K, V])
}
eviction struct {
mu sync.RWMutex
nextID uint64
fns map[uint64]func(EvictionReason, *Item[K, V])
}
}
stopMu sync.Mutex
stopCh chan struct{}
stopped bool
options options[K, V]
}
// New creates a new instance of cache.
func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] {
c := &Cache[K, V]{
stopCh: make(chan struct{}),
stopped: true, // cache cleanup process is stopped by default
}
c.items.values = make(map[K]*list.Element)
c.items.lru = list.New()
c.items.expQueue = newExpirationQueue[K, V]()
c.items.timerCh = make(chan time.Duration, 1) // buffer is important
c.events.insertion.fns = make(map[uint64]func(*Item[K, V]))
c.events.update.fns = make(map[uint64]func(*Item[K, V]))
c.events.eviction.fns = make(map[uint64]func(EvictionReason, *Item[K, V]))
c.options = applyOptions(c.options, opts...)
return c
}
// updateExpirations updates the expiration queue and notifies
// the cache auto cleaner if needed.
// Not safe for concurrent use by multiple goroutines without additional
// locking.
func (c *Cache[K, V]) updateExpirations(fresh bool, elem *list.Element) {
var oldExpiresAt time.Time
if !c.items.expQueue.isEmpty() {
oldExpiresAt = c.items.expQueue[0].Value.(*Item[K, V]).expiresAt
}
if fresh {
c.items.expQueue.push(elem)
} else {
c.items.expQueue.update(elem)
}
newExpiresAt := c.items.expQueue[0].Value.(*Item[K, V]).expiresAt
// check if the closest/soonest expiration timestamp changed
if newExpiresAt.IsZero() || (!oldExpiresAt.IsZero() && !newExpiresAt.Before(oldExpiresAt)) {
return
}
d := time.Until(newExpiresAt)
// It's possible that the auto cleaner isn't active or
// is busy, so we need to drain the channel before
// sending a new value.
// Also, since this method is called after locking the items' mutex,
// we can be sure that there is no other concurrent call of this
// method
if len(c.items.timerCh) > 0 {
// we need to drain this channel in a select with a default
// case because it's possible that the auto cleaner
// read this channel just after we entered this if
select {
case d1 := <-c.items.timerCh:
if d1 < d {
d = d1
}
default:
}
}
// since the channel has a size 1 buffer, we can be sure
// that the line below won't block (we can't overfill the buffer
// because we just drained it)
c.items.timerCh <- d
}
// set creates a new item, adds it to the cache and then returns it.
// Not safe for concurrent use by multiple goroutines without additional
// locking.
func (c *Cache[K, V]) set(key K, value V, ttl time.Duration) *Item[K, V] {
if ttl == DefaultTTL {
ttl = c.options.ttl
}
elem := c.get(key, false, true)
if elem != nil {
// update/overwrite an existing item
item := elem.Value.(*Item[K, V])
oldItemCost := item.cost
item.update(value, ttl)
c.updateExpirations(false, elem)
if c.options.maxCost != 0 {
c.cost = c.cost - oldItemCost + item.cost
for c.cost > c.options.maxCost {
c.evict(EvictionReasonMaxCostExceeded, c.items.lru.Back())
}
}
c.metricsMu.Lock()
c.metrics.Updates++
c.metricsMu.Unlock()
c.events.update.mu.RLock()
for _, fn := range c.events.update.fns {
fn(item)
}
c.events.update.mu.RUnlock()
return item
}
if c.options.capacity != 0 && uint64(len(c.items.values)) >= c.options.capacity {
// delete the oldest item
c.evict(EvictionReasonCapacityReached, c.items.lru.Back())
}
if ttl == PreviousOrDefaultTTL {
ttl = c.options.ttl
}
// create a new item
item := NewItemWithOpts(key, value, ttl, c.options.itemOpts...)
elem = c.items.lru.PushFront(item)
c.items.values[key] = elem
c.updateExpirations(true, elem)
if c.options.maxCost != 0 {
c.cost += item.cost
for c.cost > c.options.maxCost {
c.evict(EvictionReasonMaxCostExceeded, c.items.lru.Back())
}
}
c.metricsMu.Lock()
c.metrics.Insertions++
c.metricsMu.Unlock()
c.events.insertion.mu.RLock()
for _, fn := range c.events.insertion.fns {
fn(item)
}
c.events.insertion.mu.RUnlock()
return item
}
// get retrieves an item from the cache and extends its expiration
// time if 'touch' is set to true.
// It returns nil if the item is not found or is expired.
// Not safe for concurrent use by multiple goroutines without additional
// locking.
func (c *Cache[K, V]) get(key K, touch bool, includeExpired bool) *list.Element {
elem := c.items.values[key]
if elem == nil {
return nil
}
item := elem.Value.(*Item[K, V])
if !includeExpired && item.isExpiredUnsafe() {
return nil
}
c.items.lru.MoveToFront(elem)
if touch && item.ttl > 0 {
item.touch()
c.updateExpirations(false, elem)
}
return elem
}
// getWithOpts wraps the get method, applies the given options, and updates
// the metrics.
// It returns nil if the item is not found or is expired.
// If 'lockAndLoad' is set to true, the mutex is locked before calling the
// get method and unlocked after it returns. It also indicates that the
// loader should be used to load external data when the get method returns
// a nil value and the mutex is unlocked.
// If 'lockAndLoad' is set to false, neither the mutex nor the loader is
// used.
func (c *Cache[K, V]) getWithOpts(key K, lockAndLoad bool, opts ...Option[K, V]) *Item[K, V] {
getOpts := options[K, V]{
loader: c.options.loader,
disableTouchOnHit: c.options.disableTouchOnHit,
}
getOpts = applyOptions(getOpts, opts...)
if lockAndLoad {
c.items.mu.Lock()
}
elem := c.get(key, !getOpts.disableTouchOnHit, false)
if lockAndLoad {
c.items.mu.Unlock()
}
if elem == nil {
c.metricsMu.Lock()
c.metrics.Misses++
c.metricsMu.Unlock()
if lockAndLoad && getOpts.loader != nil {
return getOpts.loader.Load(c, key)
}
return nil
}
c.metricsMu.Lock()
c.metrics.Hits++
c.metricsMu.Unlock()
return elem.Value.(*Item[K, V])
}
// evict deletes items from the cache.
// If no items are provided, all currently present cache items
// are evicted.
// Not safe for concurrent use by multiple goroutines without additional
// locking.
func (c *Cache[K, V]) evict(reason EvictionReason, elems ...*list.Element) {
if len(elems) > 0 {
c.metricsMu.Lock()
c.metrics.Evictions += uint64(len(elems))
c.metricsMu.Unlock()
c.events.eviction.mu.RLock()
for i := range elems {
item := elems[i].Value.(*Item[K, V])
delete(c.items.values, item.key)
if c.options.maxCost != 0 {
c.cost -= item.cost
}
c.items.lru.Remove(elems[i])
c.items.expQueue.remove(elems[i])
for _, fn := range c.events.eviction.fns {
fn(reason, item)
}
}
c.events.eviction.mu.RUnlock()
return
}
c.metricsMu.Lock()
c.metrics.Evictions += uint64(len(c.items.values))
c.metricsMu.Unlock()
c.events.eviction.mu.RLock()
for _, elem := range c.items.values {
item := elem.Value.(*Item[K, V])
for _, fn := range c.events.eviction.fns {
fn(reason, item)
}
}
c.events.eviction.mu.RUnlock()
c.items.values = make(map[K]*list.Element)
c.items.lru.Init()
c.items.expQueue = newExpirationQueue[K, V]()
}
// delete deletes an item by the provided key.
// The method is no-op if the item is not found.
// Not safe for concurrent use by multiple goroutines without additional
// locking.
func (c *Cache[K, V]) delete(key K) {
elem := c.items.values[key]
if elem == nil {
return
}
c.evict(EvictionReasonDeleted, elem)
}
// Set creates a new item from the provided key and value, adds
// it to the cache and then returns it. If an item associated with the
// provided key already exists, the new item overwrites the existing one.
// NoTTL constant or -1 can be used to indicate that the item should never
// expire.
// DefaultTTL constant or 0 can be used to indicate that the item should use
// the default/global TTL that was specified when the cache instance was
// created.
func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) *Item[K, V] {
c.items.mu.Lock()
defer c.items.mu.Unlock()
return c.set(key, value, ttl)
}
// Get retrieves an item from the cache by the provided key.
// Unless this is disabled, it also extends/touches an item's
// expiration timestamp on successful retrieval.
// If the item is not found, a nil value is returned.
func (c *Cache[K, V]) Get(key K, opts ...Option[K, V]) *Item[K, V] {
return c.getWithOpts(key, true, opts...)
}
// Delete deletes an item from the cache. If the item associated with
// the key is not found, the method is no-op.
func (c *Cache[K, V]) Delete(key K) {
c.items.mu.Lock()
defer c.items.mu.Unlock()
c.delete(key)
}
// Has checks whether the key exists in the cache.
func (c *Cache[K, V]) Has(key K) bool {
c.items.mu.RLock()
defer c.items.mu.RUnlock()
elem, ok := c.items.values[key]
return ok && !elem.Value.(*Item[K, V]).isExpiredUnsafe()
}
// GetOrSet retrieves an item from the cache by the provided key.
// If the item is not found, it is created with the provided options and
// then returned.
// The bool return value is true if the item was found, false if created
// during the execution of the method.
// If the loader is non-nil (i.e., used as an option or specified when
// creating the cache instance), its execution is skipped.
func (c *Cache[K, V]) GetOrSet(key K, value V, opts ...Option[K, V]) (*Item[K, V], bool) {
return c.GetOrSetFunc(
key,
func() V {
return value
},
opts...,
)
}
// GetOrSetFunc retrieves an item from the cache by the provided key.
// If the element is not found, it is created by executing the fn function
// with the provided options and then returned.
// The bool return value is true if the item was found, false if created
// during the execution of the method.
// If the loader is non-nil (i.e., used as an option or specified when
// creating the cache instance), its execution is skipped.
func (c *Cache[K, V]) GetOrSetFunc(key K, fn func() V, opts ...Option[K, V]) (*Item[K, V], bool) {
c.items.mu.Lock()
defer c.items.mu.Unlock()
elem := c.getWithOpts(key, false, opts...)
if elem != nil {
return elem, true
}
setOpts := options[K, V]{
ttl: c.options.ttl,
}
setOpts = applyOptions(setOpts, opts...) // used only to update the TTL
item := c.set(key, fn(), setOpts.ttl)
return item, false
}
// GetAndDelete retrieves an item from the cache by the provided key and
// then deletes it.
// The bool return value is true if the item was found before
// its deletion, false if not.
// If the loader is non-nil (i.e., used as an option or specified when
// creating the cache instance), it is executed normaly, i.e., only when
// the item is not found.
func (c *Cache[K, V]) GetAndDelete(key K, opts ...Option[K, V]) (*Item[K, V], bool) {
c.items.mu.Lock()
elem := c.getWithOpts(key, false, opts...)
if elem == nil {
c.items.mu.Unlock()
getOpts := options[K, V]{
loader: c.options.loader,
}
getOpts = applyOptions(getOpts, opts...) // used only to update the loader
if getOpts.loader != nil {
item := getOpts.loader.Load(c, key)
return item, item != nil
}
return nil, false
}
c.delete(key)
c.items.mu.Unlock()
return elem, true
}
// DeleteAll deletes all items from the cache.
func (c *Cache[K, V]) DeleteAll() {
c.items.mu.Lock()
c.evict(EvictionReasonDeleted)
c.items.mu.Unlock()
}
// DeleteExpired deletes all expired items from the cache.
func (c *Cache[K, V]) DeleteExpired() {
c.items.mu.Lock()
defer c.items.mu.Unlock()
if c.items.expQueue.isEmpty() {
return
}
e := c.items.expQueue[0]
for e.Value.(*Item[K, V]).isExpiredUnsafe() {
c.evict(EvictionReasonExpired, e)
if c.items.expQueue.isEmpty() {
break
}
// expiration queue has a new root
e = c.items.expQueue[0]
}
}
// Touch simulates an item's retrieval without actually returning it.
// Its main purpose is to extend an item's expiration timestamp.
// If the item is not found, the method is no-op.
func (c *Cache[K, V]) Touch(key K) {
c.items.mu.Lock()
c.get(key, true, false)
c.items.mu.Unlock()
}
// Len returns the number of unexpired items in the cache.
func (c *Cache[K, V]) Len() int {
c.items.mu.RLock()
defer c.items.mu.RUnlock()
total := c.items.expQueue.Len()
if total == 0 {
return 0
}
// search the heap-based expQueue by BFS
countExpired := func() int {
var (
q []int
res int
)
item := c.items.expQueue[0].Value.(*Item[K, V])
if !item.isExpiredUnsafe() {
return res
}
q = append(q, 0)
for len(q) > 0 {
pop := q[0]
q = q[1:]
res++
for i := 1; i <= 2; i++ {
idx := 2*pop + i
if idx >= total {
break
}
item = c.items.expQueue[idx].Value.(*Item[K, V])
if item.isExpiredUnsafe() {
q = append(q, idx)
}
}
}
return res
}
return total - countExpired()
}
// Keys returns all unexpired keys in the cache.
func (c *Cache[K, V]) Keys() []K {
c.items.mu.RLock()
defer c.items.mu.RUnlock()
res := make([]K, 0)
for k, elem := range c.items.values {
if !elem.Value.(*Item[K, V]).isExpiredUnsafe() {
res = append(res, k)
}
}
return res
}
// Items returns a copy of all items in the cache.
// It does not update any expiration timestamps.
func (c *Cache[K, V]) Items() map[K]*Item[K, V] {
c.items.mu.RLock()
defer c.items.mu.RUnlock()
items := make(map[K]*Item[K, V])
for k, elem := range c.items.values {
item := elem.Value.(*Item[K, V])
if item != nil && !item.isExpiredUnsafe() {
items[k] = item
}
}
return items
}
// Range calls fn for each unexpired item in the cache. If fn returns false,
// Range stops the iteration.
func (c *Cache[K, V]) Range(fn func(item *Item[K, V]) bool) {
c.items.mu.RLock()
// Check if cache is empty
if c.items.lru.Len() == 0 {
c.items.mu.RUnlock()
return
}
for item := c.items.lru.Front(); c.items.lru.Len() != 0 && item != c.items.lru.Back().Next(); item = item.Next() {
i := item.Value.(*Item[K, V])
expired := i.isExpiredUnsafe()
c.items.mu.RUnlock() // unlock mutex so fn func can access it (if it needs to)
if !expired && !fn(i) {
return
}
c.items.mu.RLock()
}
c.items.mu.RUnlock()
}
// RangeBackwards calls fn for each unexpired item in the cache in reverse order.
// If fn returns false, RangeBackwards stops the iteration.
func (c *Cache[K, V]) RangeBackwards(fn func(item *Item[K, V]) bool) {
c.items.mu.RLock()
// Check if cache is empty
if c.items.lru.Len() == 0 {
c.items.mu.RUnlock()
return
}
for item := c.items.lru.Back(); c.items.lru.Len() != 0 && item != c.items.lru.Front().Prev(); item = item.Prev() {
i := item.Value.(*Item[K, V])
expired := i.isExpiredUnsafe()
c.items.mu.RUnlock() // unlock mutex so fn func can access it (if it needs to)
if !expired && !fn(i) {
return
}
c.items.mu.RLock()
}
c.items.mu.RUnlock()
}
// Metrics returns the metrics of the cache.
func (c *Cache[K, V]) Metrics() Metrics {
c.metricsMu.RLock()
defer c.metricsMu.RUnlock()
return c.metrics
}
// Start starts an automatic cleanup process that periodically deletes
// expired items.
// It blocks until Stop is called.
func (c *Cache[K, V]) Start() {
c.stopMu.Lock()
if !c.stopped {
c.stopMu.Unlock()
return
}
c.stopped = false
c.stopMu.Unlock()
waitDur := func() time.Duration {
c.items.mu.RLock()
defer c.items.mu.RUnlock()
if !c.items.expQueue.isEmpty() &&
!c.items.expQueue[0].Value.(*Item[K, V]).expiresAt.IsZero() {
d := time.Until(c.items.expQueue[0].Value.(*Item[K, V]).expiresAt)
if d <= 0 {
// execute immediately
return time.Microsecond
}
return d
}
if c.options.ttl > 0 {
return c.options.ttl
}
return time.Hour
}
timer := time.NewTimer(waitDur())
stop := func() {
if !timer.Stop() {
// drain the timer chan
select {
case <-timer.C:
default:
}
}
}
defer stop()
for {
select {
case <-c.stopCh:
return
case d := <-c.items.timerCh:
stop()
timer.Reset(d)
case <-timer.C:
c.DeleteExpired()
stop()
timer.Reset(waitDur())
}
}
}
// Stop stops the automatic cleanup process.
// It blocks until the cleanup process exits.
func (c *Cache[K, V]) Stop() {
c.stopMu.Lock()
defer c.stopMu.Unlock()
if c.stopped {
return
}
c.stopCh <- struct{}{}
c.stopped = true
}
// OnInsertion adds the provided function to be executed when
// a new item is inserted into the cache. The function is executed
// on a separate goroutine and does not block the flow of the cache
// manager.
// The returned function may be called to delete the subscription function
// from the list of insertion subscribers.
// When the returned function is called, it blocks until all instances of
// the same subscription function return. A context is used to notify the
// subscription function when the returned/deletion function is called.
func (c *Cache[K, V]) OnInsertion(fn func(context.Context, *Item[K, V])) func() {
var (
wg sync.WaitGroup
ctx, cancel = context.WithCancel(context.Background())
)
c.events.insertion.mu.Lock()
id := c.events.insertion.nextID
c.events.insertion.fns[id] = func(item *Item[K, V]) {
wg.Add(1)
go func() {
fn(ctx, item)
wg.Done()
}()
}
c.events.insertion.nextID++
c.events.insertion.mu.Unlock()
return func() {
cancel()
c.events.insertion.mu.Lock()
delete(c.events.insertion.fns, id)
c.events.insertion.mu.Unlock()
wg.Wait()
}
}
// OnUpdate adds the provided function to be executed when
// an item is updated in the cache. The function is executed
// on a separate goroutine and does not block the flow of the cache
// manager.
// The returned function may be called to delete the subscription function
// from the list of update subscribers.
// When the returned function is called, it blocks until all instances of
// the same subscription function return. A context is used to notify the
// subscription function when the returned/deletion function is called.
func (c *Cache[K, V]) OnUpdate(fn func(context.Context, *Item[K, V])) func() {
var (
wg sync.WaitGroup
ctx, cancel = context.WithCancel(context.Background())
)
c.events.update.mu.Lock()
id := c.events.update.nextID
c.events.update.fns[id] = func(item *Item[K, V]) {
wg.Add(1)
go func() {
fn(ctx, item)
wg.Done()
}()
}
c.events.update.nextID++
c.events.update.mu.Unlock()
return func() {
cancel()
c.events.update.mu.Lock()
delete(c.events.update.fns, id)
c.events.update.mu.Unlock()
wg.Wait()
}
}
// OnEviction adds the provided function to be executed when
// an item is evicted/deleted from the cache. The function is executed
// on a separate goroutine and does not block the flow of the cache
// manager.
// The returned function may be called to delete the subscription function
// from the list of eviction subscribers.
// When the returned function is called, it blocks until all instances of
// the same subscription function return. A context is used to notify the
// subscription function when the returned/deletion function is called.
func (c *Cache[K, V]) OnEviction(fn func(context.Context, EvictionReason, *Item[K, V])) func() {
var (
wg sync.WaitGroup
ctx, cancel = context.WithCancel(context.Background())
)
c.events.eviction.mu.Lock()
id := c.events.eviction.nextID
c.events.eviction.fns[id] = func(r EvictionReason, item *Item[K, V]) {
wg.Add(1)
go func() {
fn(ctx, r, item)
wg.Done()
}()
}
c.events.eviction.nextID++
c.events.eviction.mu.Unlock()
return func() {
cancel()
c.events.eviction.mu.Lock()
delete(c.events.eviction.fns, id)
c.events.eviction.mu.Unlock()
wg.Wait()
}
}
// Loader is an interface that handles missing data loading.
type Loader[K comparable, V any] interface {
// Load should execute a custom item retrieval logic and
// return the item that is associated with the key.
// It should return nil if the item is not found/valid.
// The method is allowed to fetch data from the cache instance
// or update it for future use.
Load(c *Cache[K, V], key K) *Item[K, V]
}
// LoaderFunc type is an adapter that allows the use of ordinary
// functions as data loaders.
type LoaderFunc[K comparable, V any] func(*Cache[K, V], K) *Item[K, V]
// Load executes a custom item retrieval logic and returns the item that
// is associated with the key.
// It returns nil if the item is not found/valid.
func (l LoaderFunc[K, V]) Load(c *Cache[K, V], key K) *Item[K, V] {
return l(c, key)
}
// SuppressedLoader wraps another Loader and suppresses duplicate
// calls to its Load method.
type SuppressedLoader[K comparable, V any] struct {
loader Loader[K, V]
group *singleflight.Group
}
// NewSuppressedLoader creates a new instance of suppressed loader.
// If the group parameter is nil, a newly created instance of
// *singleflight.Group is used.
func NewSuppressedLoader[K comparable, V any](loader Loader[K, V], group *singleflight.Group) *SuppressedLoader[K, V] {
if group == nil {
group = &singleflight.Group{}
}
return &SuppressedLoader[K, V]{
loader: loader,
group: group,
}
}
// Load executes a custom item retrieval logic and returns the item that
// is associated with the key.
// It returns nil if the item is not found/valid.
// It also ensures that only one execution of the wrapped Loader's Load
// method is in-flight for a given key at a time.
func (l *SuppressedLoader[K, V]) Load(c *Cache[K, V], key K) *Item[K, V] {
// there should be a better/generic way to create a
// singleflight Group's key. It's possible that a generic
// singleflight.Group will be introduced with/in go1.19+
strKey := fmt.Sprint(key)
// the error can be discarded since the singleflight.Group
// itself does not return any of its errors, it returns
// the error that we return ourselves in the func below, which
// is also nil
res, _, _ := l.group.Do(strKey, func() (interface{}, error) {
item := l.loader.Load(c, key)
if item == nil {
return nil, nil
}
return item, nil
})
if res == nil {
return nil
}
return res.(*Item[K, V])
}
+85
View File
@@ -0,0 +1,85 @@
package ttlcache
import (
"container/heap"
"container/list"
)
// expirationQueue stores items that are ordered by their expiration
// timestamps. The 0th item is closest to its expiration.
type expirationQueue[K comparable, V any] []*list.Element
// newExpirationQueue creates and initializes a new expiration queue.
func newExpirationQueue[K comparable, V any]() expirationQueue[K, V] {
q := make(expirationQueue[K, V], 0)
heap.Init(&q)
return q
}
// isEmpty checks if the queue is empty.
func (q expirationQueue[K, V]) isEmpty() bool {
return q.Len() == 0
}
// update updates an existing item's value and position in the queue.
func (q *expirationQueue[K, V]) update(elem *list.Element) {
heap.Fix(q, elem.Value.(*Item[K, V]).queueIndex)
}
// push pushes a new item into the queue and updates the order of its
// elements.
func (q *expirationQueue[K, V]) push(elem *list.Element) {
heap.Push(q, elem)
}
// remove removes an item from the queue and updates the order of its
// elements.
func (q *expirationQueue[K, V]) remove(elem *list.Element) {
heap.Remove(q, elem.Value.(*Item[K, V]).queueIndex)
}
// Len returns the total number of items in the queue.
func (q expirationQueue[K, V]) Len() int {
return len(q)
}
// Less checks if the item at the i position expires sooner than
// the one at the j position.
func (q expirationQueue[K, V]) Less(i, j int) bool {
item1, item2 := q[i].Value.(*Item[K, V]), q[j].Value.(*Item[K, V])
if item1.expiresAt.IsZero() {
return false
}
if item2.expiresAt.IsZero() {
return true
}
return item1.expiresAt.Before(item2.expiresAt)
}
// Swap switches the places of two queue items.
func (q expirationQueue[K, V]) Swap(i, j int) {
q[i], q[j] = q[j], q[i]
q[i].Value.(*Item[K, V]).queueIndex = i
q[j].Value.(*Item[K, V]).queueIndex = j
}
// Push appends a new item to the item slice.
func (q *expirationQueue[K, V]) Push(x interface{}) {
elem := x.(*list.Element)
elem.Value.(*Item[K, V]).queueIndex = len(*q)
*q = append(*q, elem)
}
// Pop removes and returns the last item.
func (q *expirationQueue[K, V]) Pop() interface{} {
old := *q
i := len(old) - 1
elem := old[i]
elem.Value.(*Item[K, V]).queueIndex = -1
old[i] = nil // avoid memory leak
*q = old[:i]
return elem
}
+193
View File
@@ -0,0 +1,193 @@
package ttlcache
import (
"sync"
"time"
)
const (
// NoTTL indicates that an item should never expire.
NoTTL time.Duration = -1
// PreviousOrDefaultTTL indicates that existing TTL of item should be used
// default TTL will be used as fallback if item doesn't exist
PreviousOrDefaultTTL time.Duration = -2
// DefaultTTL indicates that the default TTL value of the cache
// instance should be used.
DefaultTTL time.Duration = 0
)
// CostItem holds the key and the value of the Item object for
// Item cost calculation purposes.
type CostItem[K comparable, V any] struct {
Key K
Value V
}
// Item holds all the information that is associated with a single
// cache value.
type Item[K comparable, V any] struct {
// the mutex needs to be locked only when:
// - data fields are being read inside accessor methods
// - data fields are being updated
// when data fields are being read in one of the cache's
// methods, we can be sure that these fields are not modified
// concurrently since the item list is locked by its own mutex as
// well, so locking this mutex would be redundant.
// In other words, this mutex is only useful when these fields
// are being read from the outside (e.g. in event functions).
mu sync.RWMutex
key K
value V
ttl time.Duration
expiresAt time.Time
queueIndex int
version int64
calculateCost CostFunc[K, V]
cost uint64
}
// NewItem creates a new cache item.
//
// Deprecated: Use NewItemWithOpts instead. This function will be removed
// in a future release.
func NewItem[K comparable, V any](key K, value V, ttl time.Duration, enableVersionTracking bool) *Item[K, V] {
return NewItemWithOpts(key, value, ttl, WithItemVersion[K, V](enableVersionTracking))
}
// NewItemWithOpts creates a new cache item and applies the provided item
// options.
func NewItemWithOpts[K comparable, V any](key K, value V, ttl time.Duration, opts ...ItemOption[K, V]) *Item[K, V] {
item := &Item[K, V]{
key: key,
value: value,
ttl: ttl,
version: -1,
calculateCost: func(item CostItem[K, V]) uint64 { return 0 },
}
applyItemOptions(item, opts...)
item.touch()
item.cost = item.calculateCost(CostItem[K, V]{
Key: key,
Value: value,
})
return item
}
// update modifies the item's value, TTL, and version.
func (item *Item[K, V]) update(value V, ttl time.Duration) {
item.mu.Lock()
defer item.mu.Unlock()
item.value = value
// update version if enabled
if item.version > -1 {
item.version++
}
// no need to update ttl or expiry in this case
if ttl != PreviousOrDefaultTTL {
item.ttl = ttl
// reset expiration timestamp because the new TTL may be
// 0 or below
item.expiresAt = time.Time{}
item.touchUnsafe()
}
// calculating the costs
item.cost = item.calculateCost(CostItem[K, V]{
Key: item.key,
Value: item.value,
})
}
// touch updates the item's expiration timestamp.
func (item *Item[K, V]) touch() {
item.mu.Lock()
defer item.mu.Unlock()
item.touchUnsafe()
}
// touchUnsafe updates the item's expiration timestamp without
// locking the mutex.
func (item *Item[K, V]) touchUnsafe() {
if item.ttl <= 0 {
return
}
item.expiresAt = time.Now().Add(item.ttl)
}
// IsExpired returns a bool value that indicates whether the item
// is expired.
func (item *Item[K, V]) IsExpired() bool {
item.mu.RLock()
defer item.mu.RUnlock()
return item.isExpiredUnsafe()
}
// isExpiredUnsafe returns a bool value that indicates whether the
// the item is expired without locking the mutex
func (item *Item[K, V]) isExpiredUnsafe() bool {
if item.ttl <= 0 {
return false
}
return item.expiresAt.Before(time.Now())
}
// Key returns the key of the item.
func (item *Item[K, V]) Key() K {
item.mu.RLock()
defer item.mu.RUnlock()
return item.key
}
// Value returns the value of the item.
func (item *Item[K, V]) Value() V {
item.mu.RLock()
defer item.mu.RUnlock()
return item.value
}
// TTL returns the TTL value of the item.
func (item *Item[K, V]) TTL() time.Duration {
item.mu.RLock()
defer item.mu.RUnlock()
return item.ttl
}
// Cost returns the cost of the item.
func (item *Item[K, V]) Cost() uint64 {
item.mu.RLock()
defer item.mu.RUnlock()
return item.cost
}
// ExpiresAt returns the expiration timestamp of the item.
func (item *Item[K, V]) ExpiresAt() time.Time {
item.mu.RLock()
defer item.mu.RUnlock()
return item.expiresAt
}
// Version returns the version of the item. It shows the total number of
// changes made to the item.
// If version tracking is disabled, the return value is always -1.
func (item *Item[K, V]) Version() int64 {
item.mu.RLock()
defer item.mu.RUnlock()
return item.version
}
+25
View File
@@ -0,0 +1,25 @@
package ttlcache
// Metrics contains common cache metrics calculated over the course
// of the cache's lifetime.
type Metrics struct {
// Insertions specifies how many items were inserted.
Insertions uint64
// Updates specifies how many items were updated.
Updates uint64
// Hits specifies how many items were successfully retrieved
// from the cache.
// Retrievals made with a loader function are not tracked.
Hits uint64
// Misses specifies how many items were not found in the cache.
// Retrievals made with a loader function are considered misses as
// well.
Misses uint64
// Evictions specifies how many items were removed from the
// cache.
Evictions uint64
}
+146
View File
@@ -0,0 +1,146 @@
package ttlcache
import "time"
// Option sets a specific cache option.
type Option[K comparable, V any] interface {
apply(opts options[K, V]) options[K, V]
}
// optionFunc wraps a function and implements the Option interface.
type optionFunc[K comparable, V any] func(options[K, V]) options[K, V]
// apply calls the wrapped function.
func (fn optionFunc[K, V]) apply(opts options[K, V]) options[K, V] {
return fn(opts)
}
// CostFunc is used to calculate the cost of the key and the item to be
// inserted into the cache.
type CostFunc[K comparable, V any] func(item CostItem[K, V]) uint64
// options holds all available cache configuration options.
type options[K comparable, V any] struct {
capacity uint64
maxCost uint64
ttl time.Duration
loader Loader[K, V]
disableTouchOnHit bool
itemOpts []ItemOption[K, V]
}
// applyOptions applies the provided option values to the option struct
// and returns the modified option struct.
func applyOptions[K comparable, V any](v options[K, V], opts ...Option[K, V]) options[K, V] {
for i := range opts {
v = opts[i].apply(v)
}
return v
}
// WithCapacity sets the maximum capacity of the cache.
// It has no effect when used with Get().
func WithCapacity[K comparable, V any](c uint64) Option[K, V] {
return optionFunc[K, V](func(opts options[K, V]) options[K, V] {
opts.capacity = c
return opts
})
}
// WithTTL sets the TTL of the cache.
// It has no effect when used with Get().
func WithTTL[K comparable, V any](ttl time.Duration) Option[K, V] {
return optionFunc[K, V](func(opts options[K, V]) options[K, V] {
opts.ttl = ttl
return opts
})
}
// WithVersion activates item version tracking.
// If version tracking is disabled, the version is always -1.
// It has no effect when used with Get().
func WithVersion[K comparable, V any](enable bool) Option[K, V] {
return optionFunc[K, V](func(opts options[K, V]) options[K, V] {
opts.itemOpts = append(opts.itemOpts, WithItemVersion[K, V](enable))
return opts
})
}
// WithLoader sets the loader of the cache.
// When passing into Get(), it sets an ephemeral loader that
// is used instead of the cache's default one.
func WithLoader[K comparable, V any](l Loader[K, V]) Option[K, V] {
return optionFunc[K, V](func(opts options[K, V]) options[K, V] {
opts.loader = l
return opts
})
}
// WithDisableTouchOnHit prevents the cache instance from
// extending/touching an item's expiration timestamp when it is being
// retrieved.
// When used with Get(), it overrides the default value of the
// cache.
func WithDisableTouchOnHit[K comparable, V any]() Option[K, V] {
return optionFunc[K, V](func(opts options[K, V]) options[K, V] {
opts.disableTouchOnHit = true
return opts
})
}
// WithMaxCost sets the maximum cost the cache is allowed to use (e.g. the used memory).
// The actual cost calculation for each inserted item happens by making use of the
// callback CostFunc.
// It has no effect when used with Get().
func WithMaxCost[K comparable, V any](s uint64, callback CostFunc[K, V]) Option[K, V] {
return optionFunc[K, V](func(opts options[K, V]) options[K, V] {
opts.maxCost = s
opts.itemOpts = append(opts.itemOpts, WithItemCostFunc(callback))
return opts
})
}
// ItemOption sets a specific item option on item creation.
type ItemOption[K comparable, V any] interface {
apply(item *Item[K, V])
}
// itemOptionFunc wraps a function and implements the itemOption interface.
type itemOptionFunc[K comparable, V any] func(*Item[K, V])
// apply calls the wrapped function.
func (fn itemOptionFunc[K, V]) apply(item *Item[K, V]) {
fn(item)
}
// applyItemOptions applies the provided option values to the Item.
// Note that this function needs to be called only when creating a new item,
// because we don't use the Item's mutex here.
func applyItemOptions[K comparable, V any](item *Item[K, V], opts ...ItemOption[K, V]) {
for i := range opts {
opts[i].apply(item)
}
}
// WithItemVersion activates item version tracking.
// If version tracking is disabled, the version is always -1.
func WithItemVersion[K comparable, V any](enable bool) ItemOption[K, V] {
return itemOptionFunc[K, V](func(item *Item[K, V]) {
if enable {
item.version = 0
} else {
item.version = -1
}
})
}
// WithItemCostFunc configures an item's cost calculation function.
// A nil value disables an item's cost calculation.
func WithItemCostFunc[K comparable, V any](costFunc CostFunc[K, V]) ItemOption[K, V] {
return itemOptionFunc[K, V](func(item *Item[K, V]) {
if costFunc != nil {
item.calculateCost = costFunc
}
})
}