-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
quick POC for generic implementation
- Loading branch information
Showing
6 changed files
with
606 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
name: build-v2 | ||
|
||
on: | ||
push: | ||
branches: | ||
tags: | ||
pull_request: | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: set up go 1.18 | ||
uses: actions/setup-go@v2 | ||
with: | ||
go-version: 1.18 | ||
id: go | ||
|
||
- name: checkout | ||
uses: actions/checkout@v2 | ||
|
||
- name: build and test | ||
run: | | ||
go test -timeout=60s -race -covermode=atomic -coverprofile=$GITHUB_WORKSPACE/profile.cov | ||
go build -race | ||
working-directory: v2 | ||
|
||
- name: install golangci-lint and goveralls | ||
run: | | ||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.46.2 | ||
GO111MODULE=off go get -u -v github.com/mattn/goveralls | ||
- name: run linters | ||
run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions | ||
working-directory: v2 | ||
|
||
- name: submit coverage | ||
run: $(go env GOPATH)/bin/goveralls -service="github" -coverprofile=$GITHUB_WORKSPACE/profile.cov | ||
env: | ||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
working-directory: v2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
// Package cache implements Cache similar to hashicorp/golang-lru | ||
// | ||
// Support LRC, LRU and TTL-based eviction. | ||
// Package is thread-safe and doesn't spawn any goroutines. | ||
// On every Set() call, cache deletes single oldest entry in case it's expired. | ||
// In case MaxSize is set, cache deletes the oldest entry disregarding its expiration date to maintain the size, | ||
// either using LRC or LRU eviction. | ||
// In case of default TTL (10 years) and default MaxSize (0, unlimited) the cache will be truly unlimited | ||
// and will never delete entries from itself automatically. | ||
// | ||
// Important: only reliable way of not having expired entries stuck in a cache is to | ||
// run cache.DeleteExpired periodically using time.Ticker, advisable period is 1/2 of TTL. | ||
package cache | ||
|
||
import ( | ||
"container/list" | ||
"fmt" | ||
"sync" | ||
"time" | ||
) | ||
|
||
// Cache defines cache interface | ||
type Cache[K comparable, V any] interface { | ||
fmt.Stringer | ||
Set(key K, value V, ttl time.Duration) | ||
Get(key K) (V, bool) | ||
Peek(key K) (V, bool) | ||
Keys() []K | ||
Len() int | ||
Invalidate(key K) | ||
InvalidateFn(fn func(key K) bool) | ||
RemoveOldest() | ||
DeleteExpired() | ||
Purge() | ||
Stat() Stats | ||
} | ||
|
||
// Stats provides statistics for cache | ||
type Stats struct { | ||
Hits, Misses int // cache effectiveness | ||
Added, Evicted int // number of added and evicted records | ||
} | ||
|
||
// cacheImpl provides Cache interface implementation. | ||
type cacheImpl[K comparable, V any] struct { | ||
ttl time.Duration | ||
maxKeys int | ||
isLRU bool | ||
onEvicted func(key K, value V) | ||
|
||
sync.Mutex | ||
stat Stats | ||
items map[K]*list.Element | ||
evictList *list.List | ||
} | ||
|
||
// noEvictionTTL - very long ttl to prevent eviction | ||
const noEvictionTTL = time.Hour * 24 * 365 * 10 | ||
|
||
// NewCache returns a new Cache. | ||
// Default MaxKeys is unlimited (0). | ||
// Default TTL is 10 years, sane value for expirable cache is 5 minutes. | ||
// Default eviction mode is LRC, appropriate option allow to change it to LRU. | ||
func NewCache[K comparable, V any](options ...*options[K, V]) (Cache[K, V], error) { | ||
res := &cacheImpl[K, V]{ | ||
items: map[K]*list.Element{}, | ||
evictList: list.New(), | ||
ttl: noEvictionTTL, | ||
maxKeys: 0, | ||
} | ||
|
||
if len(options) > 0 { | ||
if options[0].ttl != nil { | ||
res.ttl = *options[0].ttl | ||
} | ||
if options[0].maxKeys != nil { | ||
res.maxKeys = *options[0].maxKeys | ||
} | ||
if options[0].lru != nil { | ||
res.isLRU = *options[0].lru | ||
} | ||
if options[0].onEvicted != nil { | ||
res.onEvicted = options[0].onEvicted | ||
} | ||
} | ||
|
||
return res, nil | ||
} | ||
|
||
// Set key, ttl of 0 would use cache-wide TTL | ||
func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { | ||
c.Lock() | ||
defer c.Unlock() | ||
now := time.Now() | ||
if ttl == 0 { | ||
ttl = c.ttl | ||
} | ||
|
||
// Check for existing item | ||
if ent, ok := c.items[key]; ok { | ||
c.evictList.MoveToFront(ent) | ||
ent.Value.(*cacheItem[K, V]).value = value | ||
ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl) | ||
return | ||
} | ||
|
||
// Add new item | ||
ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)} | ||
entry := c.evictList.PushFront(ent) | ||
c.items[key] = entry | ||
c.stat.Added++ | ||
|
||
// Remove oldest entry if it is expired, only in case of non-default TTL. | ||
if c.ttl != noEvictionTTL || ttl != noEvictionTTL { | ||
c.removeOldestIfExpired() | ||
} | ||
|
||
// Verify size not exceeded | ||
if c.maxKeys > 0 && len(c.items) > c.maxKeys { | ||
c.removeOldest() | ||
} | ||
} | ||
|
||
// Get returns the key value if it's not expired | ||
func (c *cacheImpl[K, V]) Get(key K) (V, bool) { | ||
def := *new(V) | ||
c.Lock() | ||
defer c.Unlock() | ||
if ent, ok := c.items[key]; ok { | ||
// Expired item check | ||
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { | ||
c.stat.Misses++ | ||
return def, false | ||
} | ||
if c.isLRU { | ||
c.evictList.MoveToFront(ent) | ||
} | ||
c.stat.Hits++ | ||
return ent.Value.(*cacheItem[K, V]).value, true | ||
} | ||
c.stat.Misses++ | ||
return def, false | ||
} | ||
|
||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key. | ||
// Works exactly the same as Get in case of LRC mode (default one). | ||
func (c *cacheImpl[K, V]) Peek(key K) (V, bool) { | ||
def := *new(V) | ||
c.Lock() | ||
defer c.Unlock() | ||
if ent, ok := c.items[key]; ok { | ||
// Expired item check | ||
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { | ||
c.stat.Misses++ | ||
return def, false | ||
} | ||
c.stat.Hits++ | ||
return ent.Value.(*cacheItem[K, V]).value, true | ||
} | ||
c.stat.Misses++ | ||
return def, false | ||
} | ||
|
||
// Keys returns a slice of the keys in the cache, from oldest to newest. | ||
func (c *cacheImpl[K, V]) Keys() []K { | ||
c.Lock() | ||
defer c.Unlock() | ||
return c.keys() | ||
} | ||
|
||
// Len return count of items in cache, including expired | ||
func (c *cacheImpl[K, V]) Len() int { | ||
c.Lock() | ||
defer c.Unlock() | ||
return c.evictList.Len() | ||
} | ||
|
||
// Invalidate key (item) from the cache | ||
func (c *cacheImpl[K, V]) Invalidate(key K) { | ||
c.Lock() | ||
defer c.Unlock() | ||
if ent, ok := c.items[key]; ok { | ||
c.removeElement(ent) | ||
} | ||
} | ||
|
||
// InvalidateFn deletes multiple keys if predicate is true | ||
func (c *cacheImpl[K, V]) InvalidateFn(fn func(key K) bool) { | ||
c.Lock() | ||
defer c.Unlock() | ||
for key, ent := range c.items { | ||
if fn(key) { | ||
c.removeElement(ent) | ||
} | ||
} | ||
} | ||
|
||
// RemoveOldest remove oldest element in the cache | ||
func (c *cacheImpl[K, V]) RemoveOldest() { | ||
c.Lock() | ||
defer c.Unlock() | ||
c.removeOldest() | ||
} | ||
|
||
// DeleteExpired clears cache of expired items | ||
func (c *cacheImpl[K, V]) DeleteExpired() { | ||
c.Lock() | ||
defer c.Unlock() | ||
for _, key := range c.keys() { | ||
if time.Now().After(c.items[key].Value.(*cacheItem[K, V]).expiresAt) { | ||
c.removeElement(c.items[key]) | ||
} | ||
} | ||
} | ||
|
||
// Purge clears the cache completely. | ||
func (c *cacheImpl[K, V]) Purge() { | ||
c.Lock() | ||
defer c.Unlock() | ||
for k, v := range c.items { | ||
delete(c.items, k) | ||
c.stat.Evicted++ | ||
if c.onEvicted != nil { | ||
c.onEvicted(k, v.Value.(*cacheItem[K, V]).value) | ||
} | ||
} | ||
c.evictList.Init() | ||
} | ||
|
||
// Stat gets the current stats for cache | ||
func (c *cacheImpl[K, V]) Stat() Stats { | ||
c.Lock() | ||
defer c.Unlock() | ||
return c.stat | ||
} | ||
|
||
func (c *cacheImpl[K, V]) String() string { | ||
stats := c.Stat() | ||
size := c.Len() | ||
return fmt.Sprintf("Size: %d, Stats: %+v (%0.1f%%)", size, stats, 100*float64(stats.Hits)/float64(stats.Hits+stats.Misses)) | ||
} | ||
|
||
// Keys returns a slice of the keys in the cache, from oldest to newest. Has to be called with lock! | ||
func (c *cacheImpl[K, V]) keys() []K { | ||
keys := make([]K, 0, len(c.items)) | ||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() { | ||
keys = append(keys, ent.Value.(*cacheItem[K, V]).key) | ||
} | ||
return keys | ||
} | ||
|
||
// removeOldest removes the oldest item from the cache. Has to be called with lock! | ||
func (c *cacheImpl[K, V]) removeOldest() { | ||
ent := c.evictList.Back() | ||
if ent != nil { | ||
c.removeElement(ent) | ||
} | ||
} | ||
|
||
// removeOldest removes the oldest item from the cache in case it's already expired. Has to be called with lock! | ||
func (c *cacheImpl[K, V]) removeOldestIfExpired() { | ||
ent := c.evictList.Back() | ||
if ent != nil && time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) { | ||
c.removeElement(ent) | ||
} | ||
} | ||
|
||
// removeElement is used to remove a given list element from the cache. Has to be called with lock! | ||
func (c *cacheImpl[K, V]) removeElement(e *list.Element) { | ||
c.evictList.Remove(e) | ||
kv := e.Value.(*cacheItem[K, V]) | ||
delete(c.items, kv.key) | ||
c.stat.Evicted++ | ||
if c.onEvicted != nil { | ||
c.onEvicted(kv.key, kv.value) | ||
} | ||
} | ||
|
||
// cacheItem is used to hold a value in the evictList | ||
type cacheItem[K comparable, V any] struct { | ||
expiresAt time.Time | ||
key K | ||
value V | ||
} |
Oops, something went wrong.