update to lcw v2 with generic types
This commit is contained in:
committed by
Umputun
parent
3210de8f7b
commit
1313dee829
+14
-12
@@ -16,7 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/jrpc"
|
||||
"github.com/go-pkgz/lcw/eventbus"
|
||||
"github.com/go-pkgz/lcw/v2/eventbus"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
ntf "github.com/go-pkgz/notify"
|
||||
"github.com/golang-jwt/jwt"
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"github.com/go-pkgz/auth/provider"
|
||||
"github.com/go-pkgz/auth/provider/sender"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
|
||||
"github.com/umputun/remark42/backend/app/migrator"
|
||||
"github.com/umputun/remark42/backend/app/notify"
|
||||
@@ -869,27 +869,28 @@ func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
|
||||
|
||||
func (s *ServerCommand) makeCache() (LoadingCache, error) {
|
||||
log.Printf("[INFO] make cache, type=%s", s.Cache.Type)
|
||||
o := cache.NewOpts[[]byte]()
|
||||
switch s.Cache.Type {
|
||||
case "redis_pub_sub":
|
||||
redisPubSub, err := eventbus.NewRedisPubSub(s.Cache.RedisAddr, "remark42-cache")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache backend initialization, redis PubSub initialisation: %w", err)
|
||||
}
|
||||
backend, err := cache.NewLruCache(cache.MaxCacheSize(s.Cache.Max.Size), cache.MaxValSize(s.Cache.Max.Value),
|
||||
cache.MaxKeys(s.Cache.Max.Items), cache.EventBus(redisPubSub))
|
||||
backend, err := cache.NewLruCache(o.MaxCacheSize(s.Cache.Max.Size), o.MaxValSize(s.Cache.Max.Value),
|
||||
o.MaxKeys(s.Cache.Max.Items), o.EventBus(redisPubSub))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache backend initialization: %w", err)
|
||||
}
|
||||
return cache.NewScache(backend), nil
|
||||
return cache.NewScache[[]byte](backend), nil
|
||||
case "mem":
|
||||
backend, err := cache.NewLruCache(cache.MaxCacheSize(s.Cache.Max.Size), cache.MaxValSize(s.Cache.Max.Value),
|
||||
cache.MaxKeys(s.Cache.Max.Items))
|
||||
backend, err := cache.NewLruCache(o.MaxCacheSize(s.Cache.Max.Size), o.MaxValSize(s.Cache.Max.Value),
|
||||
o.MaxKeys(s.Cache.Max.Items))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache backend initialization: %w", err)
|
||||
}
|
||||
return cache.NewScache(backend), nil
|
||||
return cache.NewScache[[]byte](backend), nil
|
||||
case "none":
|
||||
return cache.NewScache(&cache.Nop{}), nil
|
||||
return cache.NewScache[[]byte](&cache.Nop[[]byte]{}), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported cache type %s", s.Cache.Type)
|
||||
}
|
||||
@@ -1326,11 +1327,12 @@ func splitAtCommas(s string) []string {
|
||||
|
||||
// authRefreshCache used by authenticator to minimize repeatable token refreshes
|
||||
type authRefreshCache struct {
|
||||
cache.LoadingCache
|
||||
cache.LoadingCache[string]
|
||||
}
|
||||
|
||||
func newAuthRefreshCache() *authRefreshCache {
|
||||
expirableCache, _ := cache.NewExpirableCache(cache.TTL(5 * time.Minute))
|
||||
o := cache.NewOpts[string]()
|
||||
expirableCache, _ := cache.NewExpirableCache(o.TTL(5 * time.Minute))
|
||||
return &authRefreshCache{LoadingCache: expirableCache}
|
||||
}
|
||||
|
||||
@@ -1341,5 +1343,5 @@ func (c *authRefreshCache) Get(key interface{}) (interface{}, bool) {
|
||||
|
||||
// Set implements cache setter with key converted to string
|
||||
func (c *authRefreshCache) Set(key, value interface{}) {
|
||||
_, _ = c.LoadingCache.Get(key.(string), func() (interface{}, error) { return value, nil })
|
||||
_, _ = c.LoadingCache.Get(key.(string), func() (string, error) { return value.(string), nil })
|
||||
}
|
||||
|
||||
@@ -848,5 +848,10 @@ func createAppFromCmd(t *testing.T, cmd ServerCommand) (*serverApp, context.Cont
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// ignore is added only for GitHub Actions, can't reproduce locally
|
||||
goleak.VerifyTestMain(m, goleak.IgnoreTopFunction("net/http.(*Server).Shutdown"))
|
||||
goleak.VerifyTestMain(
|
||||
m,
|
||||
goleak.IgnoreTopFunction("net/http.(*Server).Shutdown"),
|
||||
// this will be fixed in https://github.com/hashicorp/golang-lru/issues/159
|
||||
goleak.IgnoreTopFunction("github.com/hashicorp/golang-lru/v2/expirable.NewLRU[...].func1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,5 +157,7 @@ func TestMain(m *testing.M) {
|
||||
m,
|
||||
goleak.IgnoreTopFunction("github.com/umputun/remark42/backend/app.init.0.func1"),
|
||||
goleak.IgnoreTopFunction("net/http.(*Server).Shutdown"),
|
||||
// this will be fixed in https://github.com/hashicorp/golang-lru/issues/159
|
||||
goleak.IgnoreTopFunction("github.com/hashicorp/golang-lru/v2/expirable.NewLRU[...].func1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -348,7 +348,7 @@ func TestAdmin_Block(t *testing.T) {
|
||||
assert.Equal(t, "test test #1", comments.Comments[2].Text, "comment not removed and not cleared")
|
||||
assert.False(t, comments.Comments[2].Deleted, "not deleted")
|
||||
|
||||
srv.pubRest.cache = cache.NewScache(cache.NewNopCache()) // TODO: with lru cache it won't be refreshed and invalidated for long
|
||||
srv.pubRest.cache = cache.NewScache[[]byte](cache.NewNopCache[[]byte]()) // TODO: with lru cache it won't be refreshed and invalidated for long
|
||||
// time
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=remark42&url=https://radio-t.com/blah&sort=+time")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
"github.com/go-pkgz/lcw"
|
||||
"github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/go-pkgz/rest/logger"
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/golang-jwt/jwt"
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/skip2/go-qrcode"
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/go-pkgz/auth/avatar"
|
||||
"github.com/go-pkgz/auth/provider"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -438,7 +438,7 @@ func startupT(t *testing.T, srvHook ...func(srv *Rest)) (ts *httptest.Server, sr
|
||||
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDB, SiteID: "remark42"})
|
||||
require.NoError(t, err)
|
||||
|
||||
memCache := cache.NewScache(cache.NewNopCache())
|
||||
memCache := cache.NewScache[[]byte](cache.NewNopCache[[]byte]())
|
||||
|
||||
astore := adminstore.NewStaticStore("123456", []string{"remark42"}, []string{"a1", "a2"}, "admin@remark-42.com")
|
||||
restrictedWordsMatcher := service.NewRestrictedWordsMatcher(service.StaticRestrictedWordsLister{Words: []string{"duck"}})
|
||||
@@ -658,5 +658,9 @@ func waitForHTTPSServerStart(port int) {
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
goleak.VerifyTestMain(
|
||||
m,
|
||||
// this will be fixed in https://github.com/hashicorp/golang-lru/issues/159
|
||||
goleak.IgnoreTopFunction("github.com/hashicorp/golang-lru/v2/expirable.NewLRU[...].func1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
cache "github.com/go-pkgz/lcw"
|
||||
cache "github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/gorilla/feeds"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lcw"
|
||||
"github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@@ -49,7 +49,7 @@ type DataStore struct {
|
||||
}
|
||||
|
||||
repliesCache struct {
|
||||
lcw.LoadingCache
|
||||
lcw.LoadingCache[struct{}]
|
||||
once sync.Once
|
||||
}
|
||||
}
|
||||
@@ -534,7 +534,8 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
|
||||
func (s *DataStore) HasReplies(comment store.Comment) bool {
|
||||
s.repliesCache.once.Do(func() {
|
||||
// default expiration time of 5 minutes and cleanup time of 2.5 minutes
|
||||
s.repliesCache.LoadingCache, _ = lcw.NewExpirableCache(lcw.TTL(5 * time.Minute))
|
||||
o := lcw.NewOpts[struct{}]()
|
||||
s.repliesCache.LoadingCache, _ = lcw.NewExpirableCache[struct{}](o.TTL(5 * time.Minute))
|
||||
})
|
||||
|
||||
if _, found := s.repliesCache.Peek(comment.ID); found {
|
||||
@@ -554,7 +555,7 @@ func (s *DataStore) HasReplies(comment store.Comment) bool {
|
||||
// When this code is reached, key "comment.ID" is not in cache.
|
||||
// Calling cache.Get on it will put it in cache with 5 minutes TTL.
|
||||
// We call it with empty struct as value as we care about keys and not values.
|
||||
_, _ = s.repliesCache.Get(comment.ID, func() (interface{}, error) { return struct{}{}, nil })
|
||||
_, _ = s.repliesCache.Get(comment.ID, func() (struct{}, error) { return struct{}{}, nil })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lcw"
|
||||
"github.com/go-pkgz/lcw/v2"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
// TitleExtractor gets html title from remote page, cached
|
||||
type TitleExtractor struct {
|
||||
client http.Client
|
||||
cache lcw.LoadingCache
|
||||
cache lcw.LoadingCache[string]
|
||||
allowedDomains []string
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ func NewTitleExtractor(client http.Client, allowedDomains []string) *TitleExtrac
|
||||
allowedDomains: allowedDomains,
|
||||
}
|
||||
var err error
|
||||
res.cache, err = lcw.NewExpirableCache(lcw.TTL(teCacheTTL), lcw.MaxKeySize(teCacheMaxRecs))
|
||||
o := lcw.NewOpts[string]()
|
||||
res.cache, err = lcw.NewExpirableCache(o.TTL(teCacheTTL), o.MaxKeySize(teCacheMaxRecs))
|
||||
if err != nil {
|
||||
log.Printf("[WARN] failed to make cache, caching disabled for titles, %v", err)
|
||||
res.cache = &lcw.Nop{}
|
||||
res.cache = &lcw.Nop[string]{}
|
||||
}
|
||||
return &res
|
||||
}
|
||||
@@ -62,10 +63,10 @@ func (t *TitleExtractor) Get(pageURL string) (string, error) {
|
||||
}
|
||||
client := http.Client{Timeout: t.client.Timeout, Transport: t.client.Transport}
|
||||
defer client.CloseIdleConnections()
|
||||
b, err := t.cache.Get(pageURL, func() (interface{}, error) {
|
||||
b, err := t.cache.Get(pageURL, func() (string, error) {
|
||||
resp, e := client.Get(pageURL)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("failed to load page %s: %w", pageURL, e)
|
||||
return "", fmt.Errorf("failed to load page %s: %w", pageURL, e)
|
||||
}
|
||||
defer func() {
|
||||
if err = resp.Body.Close(); err != nil {
|
||||
@@ -73,23 +74,23 @@ func (t *TitleExtractor) Get(pageURL string) (string, error) {
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("can't load page %s, code %d", pageURL, resp.StatusCode)
|
||||
return "", fmt.Errorf("can't load page %s, code %d", pageURL, resp.StatusCode)
|
||||
}
|
||||
|
||||
title, ok := t.getTitle(resp.Body)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't get title for %s", pageURL)
|
||||
return "", fmt.Errorf("can't get title for %s", pageURL)
|
||||
}
|
||||
return title, nil
|
||||
})
|
||||
|
||||
// on error save result (empty string) to cache too and return "" title
|
||||
if err != nil {
|
||||
_, _ = t.cache.Get(pageURL, func() (interface{}, error) { return "", nil })
|
||||
_, _ = t.cache.Get(pageURL, func() (string, error) { return "", nil })
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b.(string), nil
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Close title extractor
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ require (
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240117071454-f721b8c33b05
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lcw v1.1.0
|
||||
github.com/go-pkgz/lcw/v2 v2.0.0
|
||||
github.com/go-pkgz/lgr v0.11.1
|
||||
github.com/go-pkgz/notify v1.1.0
|
||||
github.com/go-pkgz/repeater v1.1.3
|
||||
@@ -56,7 +56,7 @@ require (
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
||||
+8
-10
@@ -15,8 +15,8 @@ github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF
|
||||
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
@@ -72,8 +72,8 @@ github.com/go-pkgz/expirable-cache v1.0.0 h1:ns5+1hjY8hntGv8bPaQd9Gr7Jyo+Uw5SLyI
|
||||
github.com/go-pkgz/expirable-cache v1.0.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
|
||||
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
|
||||
github.com/go-pkgz/lcw v1.1.0 h1:hDJdQJZf4iw19a7cTgQvF6Poz/L7mL4E7FgG5jGs4lA=
|
||||
github.com/go-pkgz/lcw v1.1.0/go.mod h1:zwT7RSxFskQsHWHJezYq6n0iTn0n4yIYRDijXq3awM0=
|
||||
github.com/go-pkgz/lcw/v2 v2.0.0 h1:gTwXpiJBhQeA1rXuqkRuLcV79uATFna8CckH8ZBBrH0=
|
||||
github.com/go-pkgz/lcw/v2 v2.0.0/go.mod h1:yxJHOn+IbQBQHxUqkCtMrbGjIfdYcsBAZcVCBaL1Va8=
|
||||
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
|
||||
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
|
||||
github.com/go-pkgz/notify v1.1.0 h1:xdMKoY7W5t9lewGzn61yM6Z6oWafPiPS1WtAMO81p2k=
|
||||
@@ -132,8 +132,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@@ -189,8 +189,6 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
|
||||
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/slack-go/slack v0.12.4 h1:4iLT2opw+/QptmQxBNA7S8pNfSIvtn0NDGu7Jq0emi4=
|
||||
github.com/slack-go/slack v0.12.4/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -253,8 +251,8 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3Ifn
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
|
||||
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
fast: false
|
||||
disable-all: true
|
||||
|
||||
run:
|
||||
output:
|
||||
format: tab
|
||||
skip-dirs:
|
||||
- vendor
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
# Loading Cache Wrapper [](https://github.com/go-pkgz/lcw/actions) [](https://coveralls.io/github/go-pkgz/lcw?branch=master) [](https://godoc.org/github.com/go-pkgz/lcw)
|
||||
|
||||
The library adds a thin layer on top of [lru cache](https://github.com/hashicorp/golang-lru) and internal implementation
|
||||
of expirable cache.
|
||||
|
||||
| Cache name | Constructor | Defaults | Description |
|
||||
|----------------|-----------------------|-------------------|-------------------------|
|
||||
| LruCache | lcw.NewLruCache | keys=1000 | LRU cache with limits |
|
||||
| ExpirableCache | lcw.NewExpirableCache | keys=1000, ttl=5m | TTL cache with limits |
|
||||
| RedisCache | lcw.NewRedisCache | ttl=5m | Redis cache with limits |
|
||||
| Nop | lcw.NewNopCache | | Do-nothing cache |
|
||||
|
||||
Main features:
|
||||
|
||||
- LoadingCache (guava style)
|
||||
- Limit maximum cache size (in bytes)
|
||||
- Limit maximum key size
|
||||
- Limit maximum size of a value
|
||||
- Limit number of keys
|
||||
- TTL support (`ExpirableCache` and `RedisCache`)
|
||||
- Callback on eviction event (not supported in `RedisCache`)
|
||||
- Functional style invalidation
|
||||
- Functional options
|
||||
- Sane defaults
|
||||
|
||||
## Install and update
|
||||
|
||||
`go get -u github.com/go-pkgz/lcw`
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/go-pkgz/lcw"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cache, err := lcw.NewLruCache(lcw.MaxKeys(500), lcw.MaxCacheSize(65536), lcw.MaxValSize(200), lcw.MaxKeySize(32))
|
||||
if err != nil {
|
||||
panic("failed to create cache")
|
||||
}
|
||||
defer cache.Close()
|
||||
|
||||
val, err := cache.Get("key123", func() (interface{}, error) {
|
||||
res, err := getDataFromSomeSource(params) // returns string
|
||||
return res, err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic("failed to get data")
|
||||
}
|
||||
|
||||
s := val.(string) // cached value
|
||||
}
|
||||
```
|
||||
|
||||
### Cache with URI
|
||||
|
||||
Cache can be created with URIs:
|
||||
|
||||
- `mem://lru?max_key_size=10&max_val_size=1024&max_keys=50&max_cache_size=64000` - creates LRU cache with given limits
|
||||
- `mem://expirable?ttl=30s&max_key_size=10&max_val_size=1024&max_keys=50&max_cache_size=64000` - create expirable cache
|
||||
- `redis://10.0.0.1:1234?db=16&password=qwerty&network=tcp4&dial_timeout=1s&read_timeout=5s&write_timeout=3s` - create
|
||||
redis cache
|
||||
- `nop://` - create Nop cache
|
||||
|
||||
## Scoped cache
|
||||
|
||||
`Scache` provides a wrapper on top of all implementations of `LoadingCache` with a number of special features:
|
||||
|
||||
1. Key is not a string, but a composed type made from partition, key-id and list of scopes (tags).
|
||||
1. Value type limited to `[]byte`
|
||||
1. Added `Flush` method for scoped/tagged invalidation of multiple records in a given partition
|
||||
1. A simplified interface with Get, Stat, Flush and Close only.
|
||||
|
||||
## Details
|
||||
|
||||
- In all cache types other than Redis (e.g. LRU and Expirable at the moment) values are stored as-is which means
|
||||
that mutable values can be changed outside of cache. `ExampleLoadingCache_Mutability` illustrates that.
|
||||
- All byte-size limits (MaxCacheSize and MaxValSize) only work for values implementing `lcw.Sizer` interface.
|
||||
- Negative limits (max options) rejected
|
||||
- The implementation started as a part of [remark42](https://github.com/umputun/remark)
|
||||
and later on moved to [go-pkgz/rest](https://github.com/go-pkgz/rest/tree/master/cache)
|
||||
library and finally generalized to become `lcw`.
|
||||
-247
@@ -1,247 +0,0 @@
|
||||
// Package cache implements LoadingCache.
|
||||
//
|
||||
// Support LRC TTL-based eviction.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoadingCache provides expirable loading cache with LRC eviction.
|
||||
type LoadingCache struct {
|
||||
purgeEvery time.Duration
|
||||
ttl time.Duration
|
||||
maxKeys int64
|
||||
done chan struct{}
|
||||
onEvicted func(key string, value interface{})
|
||||
|
||||
mu sync.Mutex
|
||||
data map[string]*cacheItem
|
||||
}
|
||||
|
||||
// noEvictionTTL - very long ttl to prevent eviction
|
||||
const noEvictionTTL = time.Hour * 24 * 365 * 10
|
||||
|
||||
// NewLoadingCache returns a new expirable LRC cache, activates purge with purgeEvery (0 to never purge).
|
||||
// Default MaxKeys is unlimited (0).
|
||||
func NewLoadingCache(options ...Option) (*LoadingCache, error) {
|
||||
res := LoadingCache{
|
||||
data: map[string]*cacheItem{},
|
||||
ttl: noEvictionTTL,
|
||||
purgeEvery: 0,
|
||||
maxKeys: 0,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
if err := opt(&res); err != nil {
|
||||
return nil, fmt.Errorf("failed to set cache option: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if res.maxKeys > 0 || res.purgeEvery > 0 {
|
||||
if res.purgeEvery == 0 {
|
||||
res.purgeEvery = time.Minute * 5 // non-zero purge enforced because maxKeys defined
|
||||
}
|
||||
go func(done <-chan struct{}) {
|
||||
ticker := time.NewTicker(res.purgeEvery)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
res.mu.Lock()
|
||||
res.purge(res.maxKeys)
|
||||
res.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}(res.done)
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// Set key
|
||||
func (c *LoadingCache) Set(key string, value interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if _, ok := c.data[key]; !ok {
|
||||
c.data[key] = &cacheItem{}
|
||||
}
|
||||
c.data[key].data = value
|
||||
c.data[key].expiresAt = now.Add(c.ttl)
|
||||
|
||||
// Enforced purge call in addition the one from the ticker
|
||||
// to limit the worst-case scenario with a lot of sets in the
|
||||
// short period of time (between two timed purge calls)
|
||||
if c.maxKeys > 0 && int64(len(c.data)) >= c.maxKeys*2 {
|
||||
c.purge(c.maxKeys)
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the key value
|
||||
func (c *LoadingCache) Get(key string) (interface{}, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
value, ok := c.getValue(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *LoadingCache) Peek(key string) (interface{}, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
value, ok := c.getValue(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return value, ok
|
||||
}
|
||||
|
||||
// Invalidate key (item) from the cache
|
||||
func (c *LoadingCache) Invalidate(key string) {
|
||||
c.mu.Lock()
|
||||
if value, ok := c.data[key]; ok {
|
||||
delete(c.data, key)
|
||||
if c.onEvicted != nil {
|
||||
c.onEvicted(key, value.data)
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// InvalidateFn deletes multiple keys if predicate is true
|
||||
func (c *LoadingCache) InvalidateFn(fn func(key string) bool) {
|
||||
c.mu.Lock()
|
||||
for key, value := range c.data {
|
||||
if fn(key) {
|
||||
delete(c.data, key)
|
||||
if c.onEvicted != nil {
|
||||
c.onEvicted(key, value.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Keys return slice of current keys in the cache
|
||||
func (c *LoadingCache) Keys() []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
keys := make([]string, 0, len(c.data))
|
||||
for k := range c.data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// get value respecting the expiration, should be called with lock
|
||||
func (c *LoadingCache) getValue(key string) (interface{}, bool) {
|
||||
value, ok := c.data[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if time.Now().After(c.data[key].expiresAt) {
|
||||
return nil, false
|
||||
}
|
||||
return value.data, ok
|
||||
}
|
||||
|
||||
// Purge clears the cache completely.
|
||||
func (c *LoadingCache) Purge() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for k, v := range c.data {
|
||||
delete(c.data, k)
|
||||
if c.onEvicted != nil {
|
||||
c.onEvicted(k, v.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteExpired clears cache of expired items
|
||||
func (c *LoadingCache) DeleteExpired() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.purge(0)
|
||||
}
|
||||
|
||||
// ItemCount return count of items in cache
|
||||
func (c *LoadingCache) ItemCount() int {
|
||||
c.mu.Lock()
|
||||
n := len(c.data)
|
||||
c.mu.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
// Close cleans the cache and destroys running goroutines
|
||||
func (c *LoadingCache) Close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
// don't panic in case service is already closed
|
||||
select {
|
||||
case <-c.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
close(c.done)
|
||||
}
|
||||
|
||||
// keysWithTS includes list of keys with ts. This is for sorting keys
|
||||
// in order to provide least recently added sorting for size-based eviction
|
||||
type keysWithTS []struct {
|
||||
key string
|
||||
ts time.Time
|
||||
}
|
||||
|
||||
// purge records > maxKeys. Has to be called with lock!
|
||||
// call with maxKeys 0 will only clear expired entries.
|
||||
func (c *LoadingCache) purge(maxKeys int64) {
|
||||
kts := keysWithTS{}
|
||||
|
||||
for key, value := range c.data {
|
||||
// ttl eviction
|
||||
if time.Now().After(c.data[key].expiresAt) {
|
||||
delete(c.data, key)
|
||||
if c.onEvicted != nil {
|
||||
c.onEvicted(key, value.data)
|
||||
}
|
||||
}
|
||||
|
||||
// prepare list of keysWithTS for size eviction
|
||||
if maxKeys > 0 && int64(len(c.data)) > maxKeys {
|
||||
ts := c.data[key].expiresAt
|
||||
|
||||
kts = append(kts, struct {
|
||||
key string
|
||||
ts time.Time
|
||||
}{key, ts})
|
||||
}
|
||||
}
|
||||
|
||||
// size eviction
|
||||
size := int64(len(c.data))
|
||||
if len(kts) > 0 {
|
||||
sort.Slice(kts, func(i int, j int) bool { return kts[i].ts.Before(kts[j].ts) })
|
||||
for d := 0; int64(d) < size-maxKeys; d++ {
|
||||
key := kts[d].key
|
||||
value := c.data[key].data
|
||||
delete(c.data, key)
|
||||
if c.onEvicted != nil {
|
||||
c.onEvicted(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type cacheItem struct {
|
||||
expiresAt time.Time
|
||||
data interface{}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
// Option func type
|
||||
type Option func(lc *LoadingCache) error
|
||||
|
||||
// OnEvicted called automatically for expired and manually deleted entries
|
||||
func OnEvicted(fn func(key string, value interface{})) Option {
|
||||
return func(lc *LoadingCache) error {
|
||||
lc.onEvicted = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PurgeEvery functional option defines purge interval
|
||||
// by default it is 0, i.e. never. If MaxKeys set to any non-zero this default will be 5minutes
|
||||
func PurgeEvery(interval time.Duration) Option {
|
||||
return func(lc *LoadingCache) error {
|
||||
lc.purgeEvery = interval
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MaxKeys functional option defines how many keys to keep.
|
||||
// By default it is 0, which means unlimited.
|
||||
// If any non-zero MaxKeys set, default PurgeEvery will be set to 5 minutes
|
||||
func MaxKeys(max int) Option {
|
||||
return func(lc *LoadingCache) error {
|
||||
lc.maxKeys = int64(max)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TTL functional option defines TTL for all cache entries.
|
||||
// By default it is set to 10 years, sane option for expirable cache might be 5 minutes.
|
||||
func TTL(ttl time.Duration) Option {
|
||||
return func(lc *LoadingCache) error {
|
||||
lc.ttl = ttl
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
+20
-20
@@ -17,15 +17,15 @@ type Sizer interface {
|
||||
}
|
||||
|
||||
// LoadingCache defines guava-like cache with Get method returning cached value ao retrieving it if not in cache
|
||||
type LoadingCache interface {
|
||||
Get(key string, fn func() (interface{}, error)) (val interface{}, err error) // load or get from cache
|
||||
Peek(key string) (interface{}, bool) // get from cache by key
|
||||
Invalidate(fn func(key string) bool) // invalidate items for func(key) == true
|
||||
Delete(key string) // delete by key
|
||||
Purge() // clear cache
|
||||
Stat() CacheStat // cache stats
|
||||
Keys() []string // list of all keys
|
||||
Close() error // close open connections
|
||||
type LoadingCache[V any] interface {
|
||||
Get(key string, fn func() (V, error)) (val V, err error) // load or get from cache
|
||||
Peek(key string) (V, bool) // get from cache by key
|
||||
Invalidate(fn func(key string) bool) // invalidate items for func(key) == true
|
||||
Delete(key string) // delete by key
|
||||
Purge() // clear cache
|
||||
Stat() CacheStat // cache stats
|
||||
Keys() []string // list of all keys
|
||||
Close() error // close open connections
|
||||
}
|
||||
|
||||
// CacheStat represent stats values
|
||||
@@ -48,37 +48,37 @@ func (s CacheStat) String() string {
|
||||
}
|
||||
|
||||
// Nop is do-nothing implementation of LoadingCache
|
||||
type Nop struct{}
|
||||
type Nop[V any] struct{}
|
||||
|
||||
// NewNopCache makes new do-nothing cache
|
||||
func NewNopCache() *Nop {
|
||||
return &Nop{}
|
||||
func NewNopCache[V any]() *Nop[V] {
|
||||
return &Nop[V]{}
|
||||
}
|
||||
|
||||
// Get calls fn without any caching
|
||||
func (n *Nop) Get(_ string, fn func() (interface{}, error)) (interface{}, error) { return fn() }
|
||||
func (n *Nop[V]) Get(_ string, fn func() (V, error)) (V, error) { return fn() }
|
||||
|
||||
// Peek does nothing and always returns false
|
||||
func (n *Nop) Peek(string) (interface{}, bool) { return nil, false }
|
||||
func (n *Nop[V]) Peek(string) (V, bool) { var emptyValue V; return emptyValue, false }
|
||||
|
||||
// Invalidate does nothing for nop cache
|
||||
func (n *Nop) Invalidate(func(key string) bool) {}
|
||||
func (n *Nop[V]) Invalidate(func(key string) bool) {}
|
||||
|
||||
// Purge does nothing for nop cache
|
||||
func (n *Nop) Purge() {}
|
||||
func (n *Nop[V]) Purge() {}
|
||||
|
||||
// Delete does nothing for nop cache
|
||||
func (n *Nop) Delete(string) {}
|
||||
func (n *Nop[V]) Delete(string) {}
|
||||
|
||||
// Keys does nothing for nop cache
|
||||
func (n *Nop) Keys() []string { return nil }
|
||||
func (n *Nop[V]) Keys() []string { return nil }
|
||||
|
||||
// Stat always 0s for nop cache
|
||||
func (n *Nop) Stat() CacheStat {
|
||||
func (n *Nop[V]) Stat() CacheStat {
|
||||
return CacheStat{}
|
||||
}
|
||||
|
||||
// Close does nothing for nop cache
|
||||
func (n *Nop) Close() error {
|
||||
func (n *Nop[V]) Close() error {
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
+2
-2
@@ -6,9 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// NewRedisPubSub creates new RedisPubSub with given parameters.
|
||||
@@ -67,6 +66,7 @@ func (m *RedisPubSub) Publish(fromID, key string) error {
|
||||
// Close cleans up running goroutines and closes Redis clients
|
||||
func (m *RedisPubSub) Close() error {
|
||||
close(m.done)
|
||||
|
||||
errs := new(multierror.Error)
|
||||
if err := m.pubSub.Close(); err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("problem closing pubSub client: %w", err))
|
||||
Generated
Vendored
+52
-54
@@ -5,24 +5,25 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lcw/eventbus"
|
||||
"github.com/go-pkgz/lcw/internal/cache"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
|
||||
"github.com/go-pkgz/lcw/v2/eventbus"
|
||||
)
|
||||
|
||||
// ExpirableCache implements LoadingCache with TTL.
|
||||
type ExpirableCache struct {
|
||||
options
|
||||
type ExpirableCache[V any] struct {
|
||||
Workers[V]
|
||||
CacheStat
|
||||
currentSize int64
|
||||
id string
|
||||
backend *cache.LoadingCache
|
||||
backend *expirable.LRU[string, V]
|
||||
}
|
||||
|
||||
// NewExpirableCache makes expirable LoadingCache implementation, 1000 max keys by default and 5m TTL
|
||||
func NewExpirableCache(opts ...Option) (*ExpirableCache, error) {
|
||||
res := ExpirableCache{
|
||||
options: options{
|
||||
func NewExpirableCache[V any](opts ...Option[V]) (*ExpirableCache[V], error) {
|
||||
res := ExpirableCache[V]{
|
||||
Workers: Workers[V]{
|
||||
maxKeys: 1000,
|
||||
maxValueSize: 0,
|
||||
ttl: 5 * time.Minute,
|
||||
@@ -32,7 +33,7 @@ func NewExpirableCache(opts ...Option) (*ExpirableCache, error) {
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(&res.options); err != nil {
|
||||
if err := opt(&res.Workers); err != nil {
|
||||
return nil, fmt.Errorf("failed to set cache option: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -41,34 +42,25 @@ func NewExpirableCache(opts ...Option) (*ExpirableCache, error) {
|
||||
return nil, fmt.Errorf("can't subscribe to event bus: %w", err)
|
||||
}
|
||||
|
||||
backend, err := cache.NewLoadingCache(
|
||||
cache.MaxKeys(res.maxKeys),
|
||||
cache.TTL(res.ttl),
|
||||
cache.PurgeEvery(res.ttl/2),
|
||||
cache.OnEvicted(func(key string, value interface{}) {
|
||||
if res.onEvicted != nil {
|
||||
res.onEvicted(key, value)
|
||||
}
|
||||
if s, ok := value.(Sizer); ok {
|
||||
size := s.Size()
|
||||
atomic.AddInt64(&res.currentSize, -1*int64(size))
|
||||
}
|
||||
// ignore the error on Publish as we don't have log inside the module and
|
||||
// there is no other way to handle it: we publish the cache invalidation
|
||||
// and hope for the best
|
||||
_ = res.eventBus.Publish(res.id, key)
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating backend: %w", err)
|
||||
}
|
||||
res.backend = backend
|
||||
res.backend = expirable.NewLRU[string, V](res.maxKeys, func(key string, value V) {
|
||||
if res.onEvicted != nil {
|
||||
res.onEvicted(key, value)
|
||||
}
|
||||
if s, ok := any(value).(Sizer); ok {
|
||||
size := s.Size()
|
||||
atomic.AddInt64(&res.currentSize, -1*int64(size))
|
||||
}
|
||||
// ignore the error on Publish as we don't have log inside the module and
|
||||
// there is no other way to handle it: we publish the cache invalidation
|
||||
// and hope for the best
|
||||
_ = res.eventBus.Publish(res.id, key)
|
||||
}, res.ttl)
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// Get gets value by key or load with fn if not found in cache
|
||||
func (c *ExpirableCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
||||
func (c *ExpirableCache[V]) Get(key string, fn func() (V, error)) (data V, err error) {
|
||||
if v, ok := c.backend.Get(key); ok {
|
||||
atomic.AddInt64(&c.Hits, 1)
|
||||
return v, nil
|
||||
@@ -84,47 +76,50 @@ func (c *ExpirableCache) Get(key string, fn func() (interface{}, error)) (data i
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if s, ok := data.(Sizer); ok {
|
||||
if s, ok := any(data).(Sizer); ok {
|
||||
if c.maxCacheSize > 0 && atomic.LoadInt64(&c.currentSize)+int64(s.Size()) >= c.maxCacheSize {
|
||||
c.backend.DeleteExpired()
|
||||
return data, nil
|
||||
}
|
||||
atomic.AddInt64(&c.currentSize, int64(s.Size()))
|
||||
}
|
||||
|
||||
c.backend.Set(key, data)
|
||||
c.backend.Add(key, data)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Invalidate removes keys with passed predicate fn, i.e. fn(key) should be true to get evicted
|
||||
func (c *ExpirableCache) Invalidate(fn func(key string) bool) {
|
||||
c.backend.InvalidateFn(fn)
|
||||
func (c *ExpirableCache[V]) Invalidate(fn func(key string) bool) {
|
||||
for _, key := range c.backend.Keys() {
|
||||
if fn(key) {
|
||||
c.backend.Remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *ExpirableCache) Peek(key string) (interface{}, bool) {
|
||||
func (c *ExpirableCache[V]) Peek(key string) (V, bool) {
|
||||
return c.backend.Peek(key)
|
||||
}
|
||||
|
||||
// Purge clears the cache completely.
|
||||
func (c *ExpirableCache) Purge() {
|
||||
func (c *ExpirableCache[V]) Purge() {
|
||||
c.backend.Purge()
|
||||
atomic.StoreInt64(&c.currentSize, 0)
|
||||
}
|
||||
|
||||
// Delete cache item by key
|
||||
func (c *ExpirableCache) Delete(key string) {
|
||||
c.backend.Invalidate(key)
|
||||
func (c *ExpirableCache[V]) Delete(key string) {
|
||||
c.backend.Remove(key)
|
||||
}
|
||||
|
||||
// Keys returns cache keys
|
||||
func (c *ExpirableCache) Keys() (res []string) {
|
||||
func (c *ExpirableCache[V]) Keys() (res []string) {
|
||||
return c.backend.Keys()
|
||||
}
|
||||
|
||||
// Stat returns cache statistics
|
||||
func (c *ExpirableCache) Stat() CacheStat {
|
||||
func (c *ExpirableCache[V]) Stat() CacheStat {
|
||||
return CacheStat{
|
||||
Hits: c.Hits,
|
||||
Misses: c.Misses,
|
||||
@@ -134,35 +129,38 @@ func (c *ExpirableCache) Stat() CacheStat {
|
||||
}
|
||||
}
|
||||
|
||||
// Close kills cleanup goroutine
|
||||
func (c *ExpirableCache) Close() error {
|
||||
c.backend.Close()
|
||||
// Close supposed to kill cleanup goroutine,
|
||||
// but it's not possible before https://github.com/hashicorp/golang-lru/issues/159 is solved
|
||||
// so for now it just cleans it.
|
||||
func (c *ExpirableCache[V]) Close() error {
|
||||
c.backend.Purge()
|
||||
atomic.StoreInt64(&c.currentSize, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// onBusEvent reacts on invalidation message triggered by event bus from another cache instance
|
||||
func (c *ExpirableCache) onBusEvent(id, key string) {
|
||||
func (c *ExpirableCache[V]) onBusEvent(id, key string) {
|
||||
if id != c.id {
|
||||
c.backend.Invalidate(key)
|
||||
c.backend.Remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ExpirableCache) size() int64 {
|
||||
func (c *ExpirableCache[V]) size() int64 {
|
||||
return atomic.LoadInt64(&c.currentSize)
|
||||
}
|
||||
|
||||
func (c *ExpirableCache) keys() int {
|
||||
return c.backend.ItemCount()
|
||||
func (c *ExpirableCache[V]) keys() int {
|
||||
return c.backend.Len()
|
||||
}
|
||||
|
||||
func (c *ExpirableCache) allowed(key string, data interface{}) bool {
|
||||
if c.backend.ItemCount() >= c.maxKeys {
|
||||
func (c *ExpirableCache[V]) allowed(key string, data V) bool {
|
||||
if c.backend.Len() >= c.maxKeys {
|
||||
return false
|
||||
}
|
||||
if c.maxKeySize > 0 && len(key) > c.maxKeySize {
|
||||
return false
|
||||
}
|
||||
if s, ok := data.(Sizer); ok {
|
||||
if s, ok := any(data).(Sizer); ok {
|
||||
if c.maxValueSize > 0 && s.Size() >= c.maxValueSize {
|
||||
return false
|
||||
}
|
||||
Generated
Vendored
+33
-37
@@ -4,24 +4,25 @@ import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-pkgz/lcw/eventbus"
|
||||
"github.com/google/uuid"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
|
||||
"github.com/go-pkgz/lcw/v2/eventbus"
|
||||
)
|
||||
|
||||
// LruCache wraps lru.LruCache with loading cache Get and size limits
|
||||
type LruCache struct {
|
||||
options
|
||||
type LruCache[V any] struct {
|
||||
Workers[V]
|
||||
CacheStat
|
||||
backend *lru.Cache
|
||||
backend *lru.Cache[string, V]
|
||||
currentSize int64
|
||||
id string // uuid identifying cache instance
|
||||
}
|
||||
|
||||
// NewLruCache makes LRU LoadingCache implementation, 1000 max keys by default
|
||||
func NewLruCache(opts ...Option) (*LruCache, error) {
|
||||
res := LruCache{
|
||||
options: options{
|
||||
func NewLruCache[V any](opts ...Option[V]) (*LruCache[V], error) {
|
||||
res := LruCache[V]{
|
||||
Workers: Workers[V]{
|
||||
maxKeys: 1000,
|
||||
maxValueSize: 0,
|
||||
eventBus: &eventbus.NopPubSub{},
|
||||
@@ -29,7 +30,7 @@ func NewLruCache(opts ...Option) (*LruCache, error) {
|
||||
id: uuid.New().String(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
if err := opt(&res.options); err != nil {
|
||||
if err := opt(&res.Workers); err != nil {
|
||||
return nil, fmt.Errorf("failed to set cache option: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -38,25 +39,25 @@ func NewLruCache(opts ...Option) (*LruCache, error) {
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (c *LruCache) init() error {
|
||||
func (c *LruCache[V]) init() error {
|
||||
if err := c.eventBus.Subscribe(c.onBusEvent); err != nil {
|
||||
return fmt.Errorf("can't subscribe to event bus: %w", err)
|
||||
}
|
||||
|
||||
onEvicted := func(key interface{}, value interface{}) {
|
||||
onEvicted := func(key string, value V) {
|
||||
if c.onEvicted != nil {
|
||||
c.onEvicted(key.(string), value)
|
||||
c.onEvicted(key, value)
|
||||
}
|
||||
if s, ok := value.(Sizer); ok {
|
||||
if s, ok := any(value).(Sizer); ok {
|
||||
size := s.Size()
|
||||
atomic.AddInt64(&c.currentSize, -1*int64(size))
|
||||
}
|
||||
_ = c.eventBus.Publish(c.id, key.(string)) // signal invalidation to other nodes
|
||||
_ = c.eventBus.Publish(c.id, key) // signal invalidation to other nodes
|
||||
}
|
||||
|
||||
var err error
|
||||
// OnEvicted called automatically for expired and manually deleted
|
||||
if c.backend, err = lru.NewWithEvict(c.maxKeys, onEvicted); err != nil {
|
||||
if c.backend, err = lru.NewWithEvict[string, V](c.maxKeys, onEvicted); err != nil {
|
||||
return fmt.Errorf("failed to make lru cache backend: %w", err)
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ func (c *LruCache) init() error {
|
||||
}
|
||||
|
||||
// Get gets value by key or load with fn if not found in cache
|
||||
func (c *LruCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
||||
func (c *LruCache[V]) Get(key string, fn func() (V, error)) (data V, err error) {
|
||||
if v, ok := c.backend.Get(key); ok {
|
||||
atomic.AddInt64(&c.Hits, 1)
|
||||
return v, nil
|
||||
@@ -83,7 +84,7 @@ func (c *LruCache) Get(key string, fn func() (interface{}, error)) (data interfa
|
||||
|
||||
c.backend.Add(key, data)
|
||||
|
||||
if s, ok := data.(Sizer); ok {
|
||||
if s, ok := any(data).(Sizer); ok {
|
||||
atomic.AddInt64(&c.currentSize, int64(s.Size()))
|
||||
if c.maxCacheSize > 0 && atomic.LoadInt64(&c.currentSize) > c.maxCacheSize {
|
||||
for atomic.LoadInt64(&c.currentSize) > c.maxCacheSize {
|
||||
@@ -96,42 +97,37 @@ func (c *LruCache) Get(key string, fn func() (interface{}, error)) (data interfa
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *LruCache) Peek(key string) (interface{}, bool) {
|
||||
func (c *LruCache[V]) Peek(key string) (V, bool) {
|
||||
return c.backend.Peek(key)
|
||||
}
|
||||
|
||||
// Purge clears the cache completely.
|
||||
func (c *LruCache) Purge() {
|
||||
func (c *LruCache[V]) Purge() {
|
||||
c.backend.Purge()
|
||||
atomic.StoreInt64(&c.currentSize, 0)
|
||||
}
|
||||
|
||||
// Invalidate removes keys with passed predicate fn, i.e. fn(key) should be true to get evicted
|
||||
func (c *LruCache) Invalidate(fn func(key string) bool) {
|
||||
func (c *LruCache[V]) Invalidate(fn func(key string) bool) {
|
||||
for _, k := range c.backend.Keys() { // Keys() returns copy of cache's key, safe to remove directly
|
||||
if key, ok := k.(string); ok && fn(key) {
|
||||
c.backend.Remove(key)
|
||||
if fn(k) {
|
||||
c.backend.Remove(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete cache item by key
|
||||
func (c *LruCache) Delete(key string) {
|
||||
func (c *LruCache[V]) Delete(key string) {
|
||||
c.backend.Remove(key)
|
||||
}
|
||||
|
||||
// Keys returns cache keys
|
||||
func (c *LruCache) Keys() (res []string) {
|
||||
keys := c.backend.Keys()
|
||||
res = make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
res = append(res, key.(string))
|
||||
}
|
||||
return res
|
||||
func (c *LruCache[V]) Keys() (res []string) {
|
||||
return c.backend.Keys()
|
||||
}
|
||||
|
||||
// Stat returns cache statistics
|
||||
func (c *LruCache) Stat() CacheStat {
|
||||
func (c *LruCache[V]) Stat() CacheStat {
|
||||
return CacheStat{
|
||||
Hits: c.Hits,
|
||||
Misses: c.Misses,
|
||||
@@ -142,30 +138,30 @@ func (c *LruCache) Stat() CacheStat {
|
||||
}
|
||||
|
||||
// Close does nothing for this type of cache
|
||||
func (c *LruCache) Close() error {
|
||||
func (c *LruCache[V]) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// onBusEvent reacts on invalidation message triggered by event bus from another cache instance
|
||||
func (c *LruCache) onBusEvent(id, key string) {
|
||||
func (c *LruCache[V]) onBusEvent(id, key string) {
|
||||
if id != c.id && c.backend.Contains(key) { // prevent reaction on event from this cache
|
||||
c.backend.Remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) size() int64 {
|
||||
func (c *LruCache[V]) size() int64 {
|
||||
return atomic.LoadInt64(&c.currentSize)
|
||||
}
|
||||
|
||||
func (c *LruCache) keys() int {
|
||||
func (c *LruCache[V]) keys() int {
|
||||
return c.backend.Len()
|
||||
}
|
||||
|
||||
func (c *LruCache) allowed(key string, data interface{}) bool {
|
||||
func (c *LruCache[V]) allowed(key string, data V) bool {
|
||||
if c.maxKeySize > 0 && len(key) > c.maxKeySize {
|
||||
return false
|
||||
}
|
||||
if s, ok := data.(Sizer); ok {
|
||||
if s, ok := any(data).(Sizer); ok {
|
||||
if c.maxValueSize > 0 && s.Size() >= c.maxValueSize {
|
||||
return false
|
||||
}
|
||||
Generated
Vendored
+37
-20
@@ -4,26 +4,35 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lcw/eventbus"
|
||||
"github.com/go-pkgz/lcw/v2/eventbus"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
type Workers[V any] struct {
|
||||
maxKeys int
|
||||
maxValueSize int
|
||||
maxKeySize int
|
||||
maxCacheSize int64
|
||||
ttl time.Duration
|
||||
onEvicted func(key string, value interface{})
|
||||
onEvicted func(key string, value V)
|
||||
eventBus eventbus.PubSub
|
||||
strToV func(string) V
|
||||
}
|
||||
|
||||
// Option func type
|
||||
type Option func(o *options) error
|
||||
type Option[V any] func(o *Workers[V]) error
|
||||
|
||||
// WorkerOptions holds the option setting methods
|
||||
type WorkerOptions[T any] struct{}
|
||||
|
||||
// NewOpts creates a new WorkerOptions instance
|
||||
func NewOpts[T any]() *WorkerOptions[T] {
|
||||
return &WorkerOptions[T]{}
|
||||
}
|
||||
|
||||
// MaxValSize functional option defines the largest value's size allowed to be cached
|
||||
// By default it is 0, which means unlimited.
|
||||
func MaxValSize(max int) Option {
|
||||
return func(o *options) error {
|
||||
func (o *WorkerOptions[V]) MaxValSize(max int) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
if max < 0 {
|
||||
return fmt.Errorf("negative max value size")
|
||||
}
|
||||
@@ -34,8 +43,8 @@ func MaxValSize(max int) Option {
|
||||
|
||||
// MaxKeySize functional option defines the largest key's size allowed to be used in cache
|
||||
// By default it is 0, which means unlimited.
|
||||
func MaxKeySize(max int) Option {
|
||||
return func(o *options) error {
|
||||
func (o *WorkerOptions[V]) MaxKeySize(max int) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
if max < 0 {
|
||||
return fmt.Errorf("negative max key size")
|
||||
}
|
||||
@@ -45,9 +54,9 @@ func MaxKeySize(max int) Option {
|
||||
}
|
||||
|
||||
// MaxKeys functional option defines how many keys to keep.
|
||||
// By default it is 0, which means unlimited.
|
||||
func MaxKeys(max int) Option {
|
||||
return func(o *options) error {
|
||||
// By default, it is 0, which means unlimited.
|
||||
func (o *WorkerOptions[V]) MaxKeys(max int) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
if max < 0 {
|
||||
return fmt.Errorf("negative max keys")
|
||||
}
|
||||
@@ -57,9 +66,9 @@ func MaxKeys(max int) Option {
|
||||
}
|
||||
|
||||
// MaxCacheSize functional option defines the total size of cached data.
|
||||
// By default it is 0, which means unlimited.
|
||||
func MaxCacheSize(max int64) Option {
|
||||
return func(o *options) error {
|
||||
// By default, it is 0, which means unlimited.
|
||||
func (o *WorkerOptions[V]) MaxCacheSize(max int64) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
if max < 0 {
|
||||
return fmt.Errorf("negative max cache size")
|
||||
}
|
||||
@@ -70,8 +79,8 @@ func MaxCacheSize(max int64) Option {
|
||||
|
||||
// TTL functional option defines duration.
|
||||
// Works for ExpirableCache only
|
||||
func TTL(ttl time.Duration) Option {
|
||||
return func(o *options) error {
|
||||
func (o *WorkerOptions[V]) TTL(ttl time.Duration) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
if ttl < 0 {
|
||||
return fmt.Errorf("negative ttl")
|
||||
}
|
||||
@@ -81,17 +90,25 @@ func TTL(ttl time.Duration) Option {
|
||||
}
|
||||
|
||||
// OnEvicted sets callback on invalidation event
|
||||
func OnEvicted(fn func(key string, value interface{})) Option {
|
||||
return func(o *options) error {
|
||||
func (o *WorkerOptions[V]) OnEvicted(fn func(key string, value V)) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
o.onEvicted = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// EventBus sets PubSub for distributed cache invalidation
|
||||
func EventBus(pubSub eventbus.PubSub) Option {
|
||||
return func(o *options) error {
|
||||
func (o *WorkerOptions[V]) EventBus(pubSub eventbus.PubSub) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
o.eventBus = pubSub
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// StrToV sets strToV function for RedisCache
|
||||
func (o *WorkerOptions[V]) StrToV(fn func(string) V) Option[V] {
|
||||
return func(o *Workers[V]) error {
|
||||
o.strToV = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+61
-26
@@ -2,7 +2,9 @@ package lcw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -13,25 +15,42 @@ import (
|
||||
const RedisValueSizeLimit = 512 * 1024 * 1024
|
||||
|
||||
// RedisCache implements LoadingCache for Redis.
|
||||
type RedisCache struct {
|
||||
options
|
||||
type RedisCache[V any] struct {
|
||||
Workers[V]
|
||||
CacheStat
|
||||
backend *redis.Client
|
||||
}
|
||||
|
||||
// NewRedisCache makes Redis LoadingCache implementation.
|
||||
func NewRedisCache(backend *redis.Client, opts ...Option) (*RedisCache, error) {
|
||||
res := RedisCache{
|
||||
options: options{
|
||||
// Supports only string and string-based types and will return error otherwise.
|
||||
func NewRedisCache[V any](backend *redis.Client, opts ...Option[V]) (*RedisCache[V], error) {
|
||||
// check if V is string, not underlying type but directly, and otherwise return error if strToV is nil as it should be defined
|
||||
|
||||
res := RedisCache[V]{
|
||||
Workers: Workers[V]{
|
||||
ttl: 5 * time.Minute,
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
if err := opt(&res.options); err != nil {
|
||||
if err := opt(&res.Workers); err != nil {
|
||||
return nil, fmt.Errorf("failed to set cache option: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// check if underlying type is string, so we can safely store it in Redis
|
||||
var v V
|
||||
if reflect.TypeOf(v).Kind() != reflect.String {
|
||||
return nil, fmt.Errorf("can't store non-string types in Redis cache")
|
||||
}
|
||||
switch any(v).(type) {
|
||||
case string:
|
||||
// check strToV option only for string-like but non string types
|
||||
default:
|
||||
if res.strToV == nil {
|
||||
return nil, fmt.Errorf("StrToV option should be set for string-like type")
|
||||
}
|
||||
}
|
||||
|
||||
if res.maxValueSize <= 0 || res.maxValueSize > RedisValueSizeLimit {
|
||||
res.maxValueSize = RedisValueSizeLimit
|
||||
}
|
||||
@@ -42,23 +61,33 @@ func NewRedisCache(backend *redis.Client, opts ...Option) (*RedisCache, error) {
|
||||
}
|
||||
|
||||
// Get gets value by key or load with fn if not found in cache
|
||||
func (c *RedisCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
||||
func (c *RedisCache[V]) Get(key string, fn func() (V, error)) (data V, err error) {
|
||||
v, getErr := c.backend.Get(context.Background(), key).Result()
|
||||
switch getErr {
|
||||
switch {
|
||||
// RedisClient returns nil when find a key in DB
|
||||
case nil:
|
||||
case getErr == nil:
|
||||
atomic.AddInt64(&c.Hits, 1)
|
||||
return v, nil
|
||||
switch any(data).(type) {
|
||||
case string:
|
||||
return any(v).(V), nil
|
||||
default:
|
||||
return c.strToV(v), nil
|
||||
}
|
||||
// RedisClient returns redis.Nil when doesn't find a key in DB
|
||||
case redis.Nil:
|
||||
case errors.Is(getErr, redis.Nil):
|
||||
if data, err = fn(); err != nil {
|
||||
atomic.AddInt64(&c.Errors, 1)
|
||||
return data, err
|
||||
}
|
||||
// RedisClient returns !nil when something goes wrong while get data
|
||||
// RedisClient returns !nil when something goes wrong while get data
|
||||
default:
|
||||
atomic.AddInt64(&c.Errors, 1)
|
||||
return v, getErr
|
||||
switch any(data).(type) {
|
||||
case string:
|
||||
return any(v).(V), getErr
|
||||
default:
|
||||
return c.strToV(v), getErr
|
||||
}
|
||||
}
|
||||
atomic.AddInt64(&c.Misses, 1)
|
||||
|
||||
@@ -76,7 +105,7 @@ func (c *RedisCache) Get(key string, fn func() (interface{}, error)) (data inter
|
||||
}
|
||||
|
||||
// Invalidate removes keys with passed predicate fn, i.e. fn(key) should be true to get evicted
|
||||
func (c *RedisCache) Invalidate(fn func(key string) bool) {
|
||||
func (c *RedisCache[V]) Invalidate(fn func(key string) bool) {
|
||||
for _, key := range c.backend.Keys(context.Background(), "*").Val() { // Keys() returns copy of cache's key, safe to remove directly
|
||||
if fn(key) {
|
||||
c.backend.Del(context.Background(), key)
|
||||
@@ -85,32 +114,38 @@ func (c *RedisCache) Invalidate(fn func(key string) bool) {
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
func (c *RedisCache) Peek(key string) (interface{}, bool) {
|
||||
func (c *RedisCache[V]) Peek(key string) (data V, found bool) {
|
||||
ret, err := c.backend.Get(context.Background(), key).Result()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
var emptyValue V
|
||||
return emptyValue, false
|
||||
}
|
||||
switch any(data).(type) {
|
||||
case string:
|
||||
return any(ret).(V), true
|
||||
default:
|
||||
return any(ret).(V), true
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// Purge clears the cache completely.
|
||||
func (c *RedisCache) Purge() {
|
||||
func (c *RedisCache[V]) Purge() {
|
||||
c.backend.FlushDB(context.Background())
|
||||
|
||||
}
|
||||
|
||||
// Delete cache item by key
|
||||
func (c *RedisCache) Delete(key string) {
|
||||
func (c *RedisCache[V]) Delete(key string) {
|
||||
c.backend.Del(context.Background(), key)
|
||||
}
|
||||
|
||||
// Keys gets all keys for the cache
|
||||
func (c *RedisCache) Keys() (res []string) {
|
||||
func (c *RedisCache[V]) Keys() (res []string) {
|
||||
return c.backend.Keys(context.Background(), "*").Val()
|
||||
}
|
||||
|
||||
// Stat returns cache statistics
|
||||
func (c *RedisCache) Stat() CacheStat {
|
||||
func (c *RedisCache[V]) Stat() CacheStat {
|
||||
return CacheStat{
|
||||
Hits: c.Hits,
|
||||
Misses: c.Misses,
|
||||
@@ -121,26 +156,26 @@ func (c *RedisCache) Stat() CacheStat {
|
||||
}
|
||||
|
||||
// Close closes underlying connections
|
||||
func (c *RedisCache) Close() error {
|
||||
func (c *RedisCache[V]) Close() error {
|
||||
return c.backend.Close()
|
||||
}
|
||||
|
||||
func (c *RedisCache) size() int64 {
|
||||
func (c *RedisCache[V]) size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *RedisCache) keys() int {
|
||||
func (c *RedisCache[V]) keys() int {
|
||||
return int(c.backend.DBSize(context.Background()).Val())
|
||||
}
|
||||
|
||||
func (c *RedisCache) allowed(key string, data interface{}) bool {
|
||||
func (c *RedisCache[V]) allowed(key string, data V) bool {
|
||||
if c.maxKeys > 0 && c.backend.DBSize(context.Background()).Val() >= int64(c.maxKeys) {
|
||||
return false
|
||||
}
|
||||
if c.maxKeySize > 0 && len(key) > c.maxKeySize {
|
||||
return false
|
||||
}
|
||||
if s, ok := data.(Sizer); ok {
|
||||
if s, ok := any(data).(Sizer); ok {
|
||||
if c.maxValueSize > 0 && (s.Size() >= c.maxValueSize) {
|
||||
return false
|
||||
}
|
||||
backend/vendor/github.com/go-pkgz/lcw/scache.go → backend/vendor/github.com/go-pkgz/lcw/v2/scache.go
Generated
Vendored
+10
-10
@@ -7,36 +7,36 @@ import (
|
||||
|
||||
// Scache wraps LoadingCache with partitions (sub-system), and scopes.
|
||||
// Simplified interface with just 4 funcs - Get, Flush, Stats and Close
|
||||
type Scache struct {
|
||||
lc LoadingCache
|
||||
type Scache[V any] struct {
|
||||
lc LoadingCache[V]
|
||||
}
|
||||
|
||||
// NewScache creates Scache on top of LoadingCache
|
||||
func NewScache(lc LoadingCache) *Scache {
|
||||
return &Scache{lc: lc}
|
||||
func NewScache[V any](lc LoadingCache[V]) *Scache[V] {
|
||||
return &Scache[V]{lc: lc}
|
||||
}
|
||||
|
||||
// Get retrieves a key from underlying backend
|
||||
func (m *Scache) Get(key Key, fn func() ([]byte, error)) (data []byte, err error) {
|
||||
func (m *Scache[V]) Get(key Key, fn func() (V, error)) (data V, err error) {
|
||||
keyStr := key.String()
|
||||
val, err := m.lc.Get(keyStr, func() (value interface{}, e error) {
|
||||
val, err := m.lc.Get(keyStr, func() (value V, e error) {
|
||||
return fn()
|
||||
})
|
||||
return val.([]byte), err
|
||||
return val, err
|
||||
}
|
||||
|
||||
// Stat delegates the call to the underlying cache backend
|
||||
func (m *Scache) Stat() CacheStat {
|
||||
func (m *Scache[V]) Stat() CacheStat {
|
||||
return m.lc.Stat()
|
||||
}
|
||||
|
||||
// Close calls Close function of the underlying cache
|
||||
func (m *Scache) Close() error {
|
||||
func (m *Scache[V]) Close() error {
|
||||
return m.lc.Close()
|
||||
}
|
||||
|
||||
// Flush clears cache and calls postFlushFn async
|
||||
func (m *Scache) Flush(req FlusherRequest) {
|
||||
func (m *Scache[V]) Flush(req FlusherRequest) {
|
||||
if len(req.scopes) == 0 {
|
||||
m.lc.Purge()
|
||||
return
|
||||
Generated
Vendored
+12
-11
@@ -16,14 +16,14 @@ import (
|
||||
// - mem://lru?max_keys=10&max_cache_size=1024
|
||||
// - mem://expirable?ttl=30s&max_val_size=100
|
||||
// - nop://
|
||||
func New(uri string) (LoadingCache, error) {
|
||||
func New[V any](uri string) (LoadingCache[V], error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse cache uri %s: %w", uri, err)
|
||||
}
|
||||
|
||||
query := u.Query()
|
||||
opts, err := optionsFromQuery(query)
|
||||
opts, err := optionsFromQuery[V](query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse uri options %s: %w", uri, err)
|
||||
}
|
||||
@@ -42,27 +42,28 @@ func New(uri string) (LoadingCache, error) {
|
||||
case "mem":
|
||||
switch u.Hostname() {
|
||||
case "lru":
|
||||
return NewLruCache(opts...)
|
||||
return NewLruCache[V](opts...)
|
||||
case "expirable":
|
||||
return NewExpirableCache(opts...)
|
||||
return NewExpirableCache[V](opts...)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported mem cache type %s", u.Hostname())
|
||||
}
|
||||
case "nop":
|
||||
return NewNopCache(), nil
|
||||
return NewNopCache[V](), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported cache type %s", u.Scheme)
|
||||
}
|
||||
|
||||
func optionsFromQuery(q url.Values) (opts []Option, err error) {
|
||||
func optionsFromQuery[V any](q url.Values) (opts []Option[V], err error) {
|
||||
errs := new(multierror.Error)
|
||||
o := NewOpts[V]()
|
||||
|
||||
if v := q.Get("max_val_size"); v != "" {
|
||||
vv, e := strconv.Atoi(v)
|
||||
if e != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("max_val_size query param %s: %w", v, e))
|
||||
} else {
|
||||
opts = append(opts, MaxValSize(vv))
|
||||
opts = append(opts, o.MaxValSize(vv))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ func optionsFromQuery(q url.Values) (opts []Option, err error) {
|
||||
if e != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("max_key_size query param %s: %w", v, e))
|
||||
} else {
|
||||
opts = append(opts, MaxKeySize(vv))
|
||||
opts = append(opts, o.MaxKeySize(vv))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ func optionsFromQuery(q url.Values) (opts []Option, err error) {
|
||||
if e != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("max_keys query param %s: %w", v, e))
|
||||
} else {
|
||||
opts = append(opts, MaxKeys(vv))
|
||||
opts = append(opts, o.MaxKeys(vv))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +90,7 @@ func optionsFromQuery(q url.Values) (opts []Option, err error) {
|
||||
if e != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("max_cache_size query param %s: %w", v, e))
|
||||
} else {
|
||||
opts = append(opts, MaxCacheSize(vv))
|
||||
opts = append(opts, o.MaxCacheSize(vv))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ func optionsFromQuery(q url.Values) (opts []Option, err error) {
|
||||
if e != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("ttl query param %s: %w", v, e))
|
||||
} else {
|
||||
opts = append(opts, TTL(vv))
|
||||
opts = append(opts, o.TTL(vv))
|
||||
}
|
||||
}
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- unused
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- stylecheck
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
fast: false
|
||||
disable-all: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
exclude-use-default: false
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
golang-lru
|
||||
==========
|
||||
|
||||
Please upgrade to github.com/hashicorp/golang-lru/v2 for all new code as v1 will
|
||||
not be updated anymore. The v2 version supports generics and is faster; old code
|
||||
can specify a specific tag, e.g. github.com/hashicorp/golang-lru/v1.0.2 for
|
||||
backwards compatibility.
|
||||
-256
@@ -1,256 +0,0 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/golang-lru/simplelru"
|
||||
)
|
||||
|
||||
// ARCCache is a thread-safe fixed size Adaptive Replacement Cache (ARC).
|
||||
// ARC is an enhancement over the standard LRU cache in that tracks both
|
||||
// frequency and recency of use. This avoids a burst in access to new
|
||||
// entries from evicting the frequently used older entries. It adds some
|
||||
// additional tracking overhead to a standard LRU cache, computationally
|
||||
// it is roughly 2x the cost, and the extra memory overhead is linear
|
||||
// with the size of the cache. ARC has been patented by IBM, but is
|
||||
// similar to the TwoQueueCache (2Q) which requires setting parameters.
|
||||
type ARCCache struct {
|
||||
size int // Size is the total capacity of the cache
|
||||
p int // P is the dynamic preference towards T1 or T2
|
||||
|
||||
t1 simplelru.LRUCache // T1 is the LRU for recently accessed items
|
||||
b1 simplelru.LRUCache // B1 is the LRU for evictions from t1
|
||||
|
||||
t2 simplelru.LRUCache // T2 is the LRU for frequently accessed items
|
||||
b2 simplelru.LRUCache // B2 is the LRU for evictions from t2
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewARC creates an ARC of the given size
|
||||
func NewARC(size int) (*ARCCache, error) {
|
||||
// Create the sub LRUs
|
||||
b1, err := simplelru.NewLRU(size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b2, err := simplelru.NewLRU(size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t1, err := simplelru.NewLRU(size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t2, err := simplelru.NewLRU(size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the ARC
|
||||
c := &ARCCache{
|
||||
size: size,
|
||||
p: 0,
|
||||
t1: t1,
|
||||
b1: b1,
|
||||
t2: t2,
|
||||
b2: b2,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *ARCCache) Get(key interface{}) (value interface{}, ok bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// If the value is contained in T1 (recent), then
|
||||
// promote it to T2 (frequent)
|
||||
if val, ok := c.t1.Peek(key); ok {
|
||||
c.t1.Remove(key)
|
||||
c.t2.Add(key, val)
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Check if the value is contained in T2 (frequent)
|
||||
if val, ok := c.t2.Get(key); ok {
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// No hit
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Add adds a value to the cache.
|
||||
func (c *ARCCache) Add(key, value interface{}) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Check if the value is contained in T1 (recent), and potentially
|
||||
// promote it to frequent T2
|
||||
if c.t1.Contains(key) {
|
||||
c.t1.Remove(key)
|
||||
c.t2.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the value is already in T2 (frequent) and update it
|
||||
if c.t2.Contains(key) {
|
||||
c.t2.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this value was recently evicted as part of the
|
||||
// recently used list
|
||||
if c.b1.Contains(key) {
|
||||
// T1 set is too small, increase P appropriately
|
||||
delta := 1
|
||||
b1Len := c.b1.Len()
|
||||
b2Len := c.b2.Len()
|
||||
if b2Len > b1Len {
|
||||
delta = b2Len / b1Len
|
||||
}
|
||||
if c.p+delta >= c.size {
|
||||
c.p = c.size
|
||||
} else {
|
||||
c.p += delta
|
||||
}
|
||||
|
||||
// Potentially need to make room in the cache
|
||||
if c.t1.Len()+c.t2.Len() >= c.size {
|
||||
c.replace(false)
|
||||
}
|
||||
|
||||
// Remove from B1
|
||||
c.b1.Remove(key)
|
||||
|
||||
// Add the key to the frequently used list
|
||||
c.t2.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this value was recently evicted as part of the
|
||||
// frequently used list
|
||||
if c.b2.Contains(key) {
|
||||
// T2 set is too small, decrease P appropriately
|
||||
delta := 1
|
||||
b1Len := c.b1.Len()
|
||||
b2Len := c.b2.Len()
|
||||
if b1Len > b2Len {
|
||||
delta = b1Len / b2Len
|
||||
}
|
||||
if delta >= c.p {
|
||||
c.p = 0
|
||||
} else {
|
||||
c.p -= delta
|
||||
}
|
||||
|
||||
// Potentially need to make room in the cache
|
||||
if c.t1.Len()+c.t2.Len() >= c.size {
|
||||
c.replace(true)
|
||||
}
|
||||
|
||||
// Remove from B2
|
||||
c.b2.Remove(key)
|
||||
|
||||
// Add the key to the frequently used list
|
||||
c.t2.Add(key, value)
|
||||
return
|
||||
}
|
||||
|
||||
// Potentially need to make room in the cache
|
||||
if c.t1.Len()+c.t2.Len() >= c.size {
|
||||
c.replace(false)
|
||||
}
|
||||
|
||||
// Keep the size of the ghost buffers trim
|
||||
if c.b1.Len() > c.size-c.p {
|
||||
c.b1.RemoveOldest()
|
||||
}
|
||||
if c.b2.Len() > c.p {
|
||||
c.b2.RemoveOldest()
|
||||
}
|
||||
|
||||
// Add to the recently seen list
|
||||
c.t1.Add(key, value)
|
||||
}
|
||||
|
||||
// replace is used to adaptively evict from either T1 or T2
|
||||
// based on the current learned value of P
|
||||
func (c *ARCCache) replace(b2ContainsKey bool) {
|
||||
t1Len := c.t1.Len()
|
||||
if t1Len > 0 && (t1Len > c.p || (t1Len == c.p && b2ContainsKey)) {
|
||||
k, _, ok := c.t1.RemoveOldest()
|
||||
if ok {
|
||||
c.b1.Add(k, nil)
|
||||
}
|
||||
} else {
|
||||
k, _, ok := c.t2.RemoveOldest()
|
||||
if ok {
|
||||
c.b2.Add(k, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of cached entries
|
||||
func (c *ARCCache) Len() int {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.t1.Len() + c.t2.Len()
|
||||
}
|
||||
|
||||
// Keys returns all the cached keys
|
||||
func (c *ARCCache) Keys() []interface{} {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
k1 := c.t1.Keys()
|
||||
k2 := c.t2.Keys()
|
||||
return append(k1, k2...)
|
||||
}
|
||||
|
||||
// Remove is used to purge a key from the cache
|
||||
func (c *ARCCache) Remove(key interface{}) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.t1.Remove(key) {
|
||||
return
|
||||
}
|
||||
if c.t2.Remove(key) {
|
||||
return
|
||||
}
|
||||
if c.b1.Remove(key) {
|
||||
return
|
||||
}
|
||||
if c.b2.Remove(key) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Purge is used to clear the cache
|
||||
func (c *ARCCache) Purge() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.t1.Purge()
|
||||
c.t2.Purge()
|
||||
c.b1.Purge()
|
||||
c.b2.Purge()
|
||||
}
|
||||
|
||||
// Contains is used to check if the cache contains a key
|
||||
// without updating recency or frequency.
|
||||
func (c *ARCCache) Contains(key interface{}) bool {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.t1.Contains(key) || c.t2.Contains(key)
|
||||
}
|
||||
|
||||
// Peek is used to inspect the cache value of a key
|
||||
// without updating recency or frequency.
|
||||
func (c *ARCCache) Peek(key interface{}) (value interface{}, ok bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
if val, ok := c.t1.Peek(key); ok {
|
||||
return val, ok
|
||||
}
|
||||
return c.t2.Peek(key)
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
// Package lru provides three different LRU caches of varying sophistication.
|
||||
//
|
||||
// Cache is a simple LRU cache. It is based on the
|
||||
// LRU implementation in groupcache:
|
||||
// https://github.com/golang/groupcache/tree/master/lru
|
||||
//
|
||||
// TwoQueueCache tracks frequently used and recently used entries separately.
|
||||
// This avoids a burst of accesses from taking out frequently used entries,
|
||||
// at the cost of about 2x computational overhead and some extra bookkeeping.
|
||||
//
|
||||
// ARCCache is an adaptive replacement cache. It tracks recent evictions as
|
||||
// well as recent usage in both the frequent and recent caches. Its
|
||||
// computational overhead is comparable to TwoQueueCache, but the memory
|
||||
// overhead is linear with the size of the cache.
|
||||
//
|
||||
// ARC has been patented by IBM, so do not use it if that is problematic for
|
||||
// your program.
|
||||
//
|
||||
// All caches in this package take locks while operating, and are therefore
|
||||
// thread-safe for consumers.
|
||||
package lru
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getRand(tb testing.TB) int64 {
|
||||
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return out.Int64()
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
linters:
|
||||
fast: false
|
||||
disable-all: true
|
||||
enable:
|
||||
- revive
|
||||
- megacheck
|
||||
- govet
|
||||
- unconvert
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- unused
|
||||
- typecheck
|
||||
- ineffassign
|
||||
# - stylecheck
|
||||
- exportloopref
|
||||
- gocritic
|
||||
- nakedret
|
||||
- gosimple
|
||||
- prealloc
|
||||
|
||||
# golangci-lint configuration file
|
||||
linters-settings:
|
||||
revive:
|
||||
ignore-generated-header: true
|
||||
severity: warning
|
||||
rules:
|
||||
- name: package-comments
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: exported
|
||||
severity: warning
|
||||
disabled: false
|
||||
arguments: ["checkPrivateReceivers", "disableStutteringCheck"]
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
Generated
Vendored
+74
-29
@@ -1,10 +1,13 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package lru
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/golang-lru/simplelru"
|
||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,33 +29,35 @@ const (
|
||||
// computationally about 2x the cost, and adds some metadata over
|
||||
// head. The ARCCache is similar, but does not require setting any
|
||||
// parameters.
|
||||
type TwoQueueCache struct {
|
||||
size int
|
||||
recentSize int
|
||||
type TwoQueueCache[K comparable, V any] struct {
|
||||
size int
|
||||
recentSize int
|
||||
recentRatio float64
|
||||
ghostRatio float64
|
||||
|
||||
recent simplelru.LRUCache
|
||||
frequent simplelru.LRUCache
|
||||
recentEvict simplelru.LRUCache
|
||||
recent simplelru.LRUCache[K, V]
|
||||
frequent simplelru.LRUCache[K, V]
|
||||
recentEvict simplelru.LRUCache[K, struct{}]
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New2Q creates a new TwoQueueCache using the default
|
||||
// values for the parameters.
|
||||
func New2Q(size int) (*TwoQueueCache, error) {
|
||||
return New2QParams(size, Default2QRecentRatio, Default2QGhostEntries)
|
||||
func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {
|
||||
return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
|
||||
}
|
||||
|
||||
// New2QParams creates a new TwoQueueCache using the provided
|
||||
// parameter values.
|
||||
func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, error) {
|
||||
func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {
|
||||
if size <= 0 {
|
||||
return nil, fmt.Errorf("invalid size")
|
||||
return nil, errors.New("invalid size")
|
||||
}
|
||||
if recentRatio < 0.0 || recentRatio > 1.0 {
|
||||
return nil, fmt.Errorf("invalid recent ratio")
|
||||
return nil, errors.New("invalid recent ratio")
|
||||
}
|
||||
if ghostRatio < 0.0 || ghostRatio > 1.0 {
|
||||
return nil, fmt.Errorf("invalid ghost ratio")
|
||||
return nil, errors.New("invalid ghost ratio")
|
||||
}
|
||||
|
||||
// Determine the sub-sizes
|
||||
@@ -60,23 +65,25 @@ func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, err
|
||||
evictSize := int(float64(size) * ghostRatio)
|
||||
|
||||
// Allocate the LRUs
|
||||
recent, err := simplelru.NewLRU(size, nil)
|
||||
recent, err := simplelru.NewLRU[K, V](size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frequent, err := simplelru.NewLRU(size, nil)
|
||||
frequent, err := simplelru.NewLRU[K, V](size, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recentEvict, err := simplelru.NewLRU(evictSize, nil)
|
||||
recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize the cache
|
||||
c := &TwoQueueCache{
|
||||
c := &TwoQueueCache[K, V]{
|
||||
size: size,
|
||||
recentSize: recentSize,
|
||||
recentRatio: recentRatio,
|
||||
ghostRatio: ghostRatio,
|
||||
recent: recent,
|
||||
frequent: frequent,
|
||||
recentEvict: recentEvict,
|
||||
@@ -85,7 +92,7 @@ func New2QParams(size int, recentRatio, ghostRatio float64) (*TwoQueueCache, err
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) {
|
||||
func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
@@ -103,11 +110,11 @@ func (c *TwoQueueCache) Get(key interface{}) (value interface{}, ok bool) {
|
||||
}
|
||||
|
||||
// No hit
|
||||
return nil, false
|
||||
return
|
||||
}
|
||||
|
||||
// Add adds a value to the cache.
|
||||
func (c *TwoQueueCache) Add(key, value interface{}) {
|
||||
func (c *TwoQueueCache[K, V]) Add(key K, value V) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
@@ -141,7 +148,7 @@ func (c *TwoQueueCache) Add(key, value interface{}) {
|
||||
}
|
||||
|
||||
// ensureSpace is used to ensure we have space in the cache
|
||||
func (c *TwoQueueCache) ensureSpace(recentEvict bool) {
|
||||
func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {
|
||||
// If we have space, nothing to do
|
||||
recentLen := c.recent.Len()
|
||||
freqLen := c.frequent.Len()
|
||||
@@ -153,7 +160,7 @@ func (c *TwoQueueCache) ensureSpace(recentEvict bool) {
|
||||
// the target, evict from there
|
||||
if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
|
||||
k, _, _ := c.recent.RemoveOldest()
|
||||
c.recentEvict.Add(k, nil)
|
||||
c.recentEvict.Add(k, struct{}{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,15 +169,43 @@ func (c *TwoQueueCache) ensureSpace(recentEvict bool) {
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *TwoQueueCache) Len() int {
|
||||
func (c *TwoQueueCache[K, V]) Len() int {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.recent.Len() + c.frequent.Len()
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *TwoQueueCache[K, V]) Resize(size int) (evicted int) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// Recalculate the sub-sizes
|
||||
recentSize := int(float64(size) * c.recentRatio)
|
||||
evictSize := int(float64(size) * c.ghostRatio)
|
||||
c.size = size
|
||||
c.recentSize = recentSize
|
||||
|
||||
// ensureSpace
|
||||
diff := c.recent.Len() + c.frequent.Len() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
}
|
||||
for i := 0; i < diff; i++ {
|
||||
c.ensureSpace(true)
|
||||
}
|
||||
|
||||
// Reallocate the LRUs
|
||||
c.recent.Resize(size)
|
||||
c.frequent.Resize(size)
|
||||
c.recentEvict.Resize(evictSize)
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache.
|
||||
// The frequently used keys are first in the returned slice.
|
||||
func (c *TwoQueueCache) Keys() []interface{} {
|
||||
func (c *TwoQueueCache[K, V]) Keys() []K {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
k1 := c.frequent.Keys()
|
||||
@@ -178,8 +213,18 @@ func (c *TwoQueueCache) Keys() []interface{} {
|
||||
return append(k1, k2...)
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache.
|
||||
// The frequently used values are first in the returned slice.
|
||||
func (c *TwoQueueCache[K, V]) Values() []V {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
v1 := c.frequent.Values()
|
||||
v2 := c.recent.Values()
|
||||
return append(v1, v2...)
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *TwoQueueCache) Remove(key interface{}) {
|
||||
func (c *TwoQueueCache[K, V]) Remove(key K) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.frequent.Remove(key) {
|
||||
@@ -194,7 +239,7 @@ func (c *TwoQueueCache) Remove(key interface{}) {
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *TwoQueueCache) Purge() {
|
||||
func (c *TwoQueueCache[K, V]) Purge() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.recent.Purge()
|
||||
@@ -204,7 +249,7 @@ func (c *TwoQueueCache) Purge() {
|
||||
|
||||
// Contains is used to check if the cache contains a key
|
||||
// without updating recency or frequency.
|
||||
func (c *TwoQueueCache) Contains(key interface{}) bool {
|
||||
func (c *TwoQueueCache[K, V]) Contains(key K) bool {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return c.frequent.Contains(key) || c.recent.Contains(key)
|
||||
@@ -212,7 +257,7 @@ func (c *TwoQueueCache) Contains(key interface{}) bool {
|
||||
|
||||
// Peek is used to inspect the cache value of a key
|
||||
// without updating recency or frequency.
|
||||
func (c *TwoQueueCache) Peek(key interface{}) (value interface{}, ok bool) {
|
||||
func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
if val, ok := c.frequent.Peek(key); ok {
|
||||
Generated
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
golang-lru
|
||||
==========
|
||||
|
||||
This provides the `lru` package which implements a fixed-size
|
||||
thread safe LRU cache. It is based on the cache in Groupcache.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2)
|
||||
|
||||
LRU cache example
|
||||
=================
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
l, _ := lru.New[int, any](128)
|
||||
for i := 0; i < 256; i++ {
|
||||
l.Add(i, nil)
|
||||
}
|
||||
if l.Len() != 128 {
|
||||
panic(fmt.Sprintf("bad len: %v", l.Len()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expirable LRU cache example
|
||||
===========================
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// make cache with 10ms TTL and 5 max keys
|
||||
cache := expirable.NewLRU[string, string](5, nil, time.Millisecond*10)
|
||||
|
||||
|
||||
// set value under key1.
|
||||
cache.Add("key1", "val1")
|
||||
|
||||
// get value under key1
|
||||
r, ok := cache.Get("key1")
|
||||
|
||||
// check for OK value
|
||||
if ok {
|
||||
fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r)
|
||||
}
|
||||
|
||||
// wait for cache to expire
|
||||
time.Sleep(time.Millisecond * 12)
|
||||
|
||||
// get value under key1 after key expiration
|
||||
r, ok = cache.Get("key1")
|
||||
fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r)
|
||||
|
||||
// set value under key2, would evict old entry because it is already expired.
|
||||
cache.Add("key2", "val2")
|
||||
|
||||
fmt.Printf("Cache len: %d\n", cache.Len())
|
||||
// Output:
|
||||
// value before expiration is found: true, value: "val1"
|
||||
// value after expiration is found: false, value: ""
|
||||
// Cache len: 1
|
||||
}
|
||||
```
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package lru provides three different LRU caches of varying sophistication.
|
||||
//
|
||||
// Cache is a simple LRU cache. It is based on the LRU implementation in
|
||||
// groupcache: https://github.com/golang/groupcache/tree/master/lru
|
||||
//
|
||||
// TwoQueueCache tracks frequently used and recently used entries separately.
|
||||
// This avoids a burst of accesses from taking out frequently used entries, at
|
||||
// the cost of about 2x computational overhead and some extra bookkeeping.
|
||||
//
|
||||
// ARCCache is an adaptive replacement cache. It tracks recent evictions as well
|
||||
// as recent usage in both the frequent and recent caches. Its computational
|
||||
// overhead is comparable to TwoQueueCache, but the memory overhead is linear
|
||||
// with the size of the cache.
|
||||
//
|
||||
// ARC has been patented by IBM, so do not use it if that is problematic for
|
||||
// your program. For this reason, it is in a separate go module contained within
|
||||
// this repository.
|
||||
//
|
||||
// All caches in this package take locks while operating, and are therefore
|
||||
// thread-safe for consumers.
|
||||
package lru
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package expirable
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/internal"
|
||||
)
|
||||
|
||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||
type EvictCallback[K comparable, V any] func(key K, value V)
|
||||
|
||||
// LRU implements a thread-safe LRU with expirable entries.
|
||||
type LRU[K comparable, V any] struct {
|
||||
size int
|
||||
evictList *internal.LruList[K, V]
|
||||
items map[K]*internal.Entry[K, V]
|
||||
onEvict EvictCallback[K, V]
|
||||
|
||||
// expirable options
|
||||
mu sync.Mutex
|
||||
ttl time.Duration
|
||||
done chan struct{}
|
||||
|
||||
// buckets for expiration
|
||||
buckets []bucket[K, V]
|
||||
// uint8 because it's number between 0 and numBuckets
|
||||
nextCleanupBucket uint8
|
||||
}
|
||||
|
||||
// bucket is a container for holding entries to be expired
|
||||
type bucket[K comparable, V any] struct {
|
||||
entries map[K]*internal.Entry[K, V]
|
||||
newestEntry time.Time
|
||||
}
|
||||
|
||||
// noEvictionTTL - very long ttl to prevent eviction
|
||||
const noEvictionTTL = time.Hour * 24 * 365 * 10
|
||||
|
||||
// because of uint8 usage for nextCleanupBucket, should not exceed 256.
|
||||
// casting it as uint8 explicitly requires type conversions in multiple places
|
||||
const numBuckets = 100
|
||||
|
||||
// NewLRU returns a new thread-safe cache with expirable entries.
|
||||
//
|
||||
// Size parameter set to 0 makes cache of unlimited size, e.g. turns LRU mechanism off.
|
||||
//
|
||||
// Providing 0 TTL turns expiring off.
|
||||
//
|
||||
// Delete expired entries every 1/100th of ttl value. Goroutine which deletes expired entries runs indefinitely.
|
||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time.Duration) *LRU[K, V] {
|
||||
if size < 0 {
|
||||
size = 0
|
||||
}
|
||||
if ttl <= 0 {
|
||||
ttl = noEvictionTTL
|
||||
}
|
||||
|
||||
res := LRU[K, V]{
|
||||
ttl: ttl,
|
||||
size: size,
|
||||
evictList: internal.NewList[K, V](),
|
||||
items: make(map[K]*internal.Entry[K, V]),
|
||||
onEvict: onEvict,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
// initialize the buckets
|
||||
res.buckets = make([]bucket[K, V], numBuckets)
|
||||
for i := 0; i < numBuckets; i++ {
|
||||
res.buckets[i] = bucket[K, V]{entries: make(map[K]*internal.Entry[K, V])}
|
||||
}
|
||||
|
||||
// enable deleteExpired() running in separate goroutine for cache with non-zero TTL
|
||||
//
|
||||
// Important: done channel is never closed, so deleteExpired() goroutine will never exit,
|
||||
// it's decided to add functionality to close it in the version later than v2.
|
||||
if res.ttl != noEvictionTTL {
|
||||
go func(done <-chan struct{}) {
|
||||
ticker := time.NewTicker(res.ttl / numBuckets)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
res.deleteExpired()
|
||||
}
|
||||
}
|
||||
}(res.done)
|
||||
}
|
||||
return &res
|
||||
}
|
||||
|
||||
// Purge clears the cache completely.
|
||||
// onEvict is called for each evicted key.
|
||||
func (c *LRU[K, V]) Purge() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for k, v := range c.items {
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(k, v.Value)
|
||||
}
|
||||
delete(c.items, k)
|
||||
}
|
||||
for _, b := range c.buckets {
|
||||
for _, ent := range b.entries {
|
||||
delete(b.entries, ent.Key)
|
||||
}
|
||||
}
|
||||
c.evictList.Init()
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
// Returns false if there was no eviction: the item was already in the cache,
|
||||
// or the size was not exceeded.
|
||||
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
now := time.Now()
|
||||
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
c.removeFromBucket(ent) // remove the entry from its current bucket as expiresAt is renewed
|
||||
ent.Value = value
|
||||
ent.ExpiresAt = now.Add(c.ttl)
|
||||
c.addToBucket(ent)
|
||||
return false
|
||||
}
|
||||
|
||||
// Add new item
|
||||
ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))
|
||||
c.items[key] = ent
|
||||
c.addToBucket(ent) // adds the entry to the appropriate bucket and sets entry.expireBucket
|
||||
|
||||
evict := c.size > 0 && c.evictList.Length() > c.size
|
||||
// Verify size not exceeded
|
||||
if evict {
|
||||
c.removeOldest()
|
||||
}
|
||||
return evict
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var ent *internal.Entry[K, V]
|
||||
if ent, ok = c.items[key]; ok {
|
||||
// Expired item check
|
||||
if time.Now().After(ent.ExpiresAt) {
|
||||
return value, false
|
||||
}
|
||||
c.evictList.MoveToFront(ent)
|
||||
return ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
||||
// or deleting it for being stale.
|
||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
_, ok = c.items[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var ent *internal.Entry[K, V]
|
||||
if ent, ok = c.items[key]; ok {
|
||||
// Expired item check
|
||||
if time.Now().After(ent.ExpiresAt) {
|
||||
return value, false
|
||||
}
|
||||
return ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache, returning if the
|
||||
// key was contained.
|
||||
func (c *LRU[K, V]) Remove(key K) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.removeElement(ent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
c.removeElement(ent)
|
||||
return ent.Key, ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
return ent.Key, ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *LRU[K, V]) Keys() []K {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
keys := make([]K, 0, len(c.items))
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
||||
keys = append(keys, ent.Key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
// Expired entries are filtered out.
|
||||
func (c *LRU[K, V]) Values() []V {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
values := make([]V, len(c.items))
|
||||
i := 0
|
||||
now := time.Now()
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
||||
if now.After(ent.ExpiresAt) {
|
||||
continue
|
||||
}
|
||||
values[i] = ent.Value
|
||||
i++
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *LRU[K, V]) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.evictList.Length()
|
||||
}
|
||||
|
||||
// Resize changes the cache size. Size of 0 means unlimited.
|
||||
func (c *LRU[K, V]) Resize(size int) (evicted int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if size <= 0 {
|
||||
c.size = 0
|
||||
return 0
|
||||
}
|
||||
diff := c.evictList.Length() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
}
|
||||
for i := 0; i < diff; i++ {
|
||||
c.removeOldest()
|
||||
}
|
||||
c.size = size
|
||||
return diff
|
||||
}
|
||||
|
||||
// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().
|
||||
// func (c *LRU[K, V]) Close() {
|
||||
// c.mu.Lock()
|
||||
// defer c.mu.Unlock()
|
||||
// select {
|
||||
// case <-c.done:
|
||||
// return
|
||||
// default:
|
||||
// }
|
||||
// close(c.done)
|
||||
// }
|
||||
|
||||
// removeOldest removes the oldest item from the cache. Has to be called with lock!
|
||||
func (c *LRU[K, V]) removeOldest() {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
|
||||
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
|
||||
c.evictList.Remove(e)
|
||||
delete(c.items, e.Key)
|
||||
c.removeFromBucket(e)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(e.Key, e.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteExpired deletes expired records from the oldest bucket, waiting for the newest entry
|
||||
// in it to expire first.
|
||||
func (c *LRU[K, V]) deleteExpired() {
|
||||
c.mu.Lock()
|
||||
bucketIdx := c.nextCleanupBucket
|
||||
timeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)
|
||||
// wait for newest entry to expire before cleanup without holding lock
|
||||
if timeToExpire > 0 {
|
||||
c.mu.Unlock()
|
||||
time.Sleep(timeToExpire)
|
||||
c.mu.Lock()
|
||||
}
|
||||
for _, ent := range c.buckets[bucketIdx].entries {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
c.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBuckets
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// addToBucket adds entry to expire bucket so that it will be cleaned up when the time comes. Has to be called with lock!
|
||||
func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {
|
||||
bucketID := (numBuckets + c.nextCleanupBucket - 1) % numBuckets
|
||||
e.ExpireBucket = bucketID
|
||||
c.buckets[bucketID].entries[e.Key] = e
|
||||
if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {
|
||||
c.buckets[bucketID].newestEntry = e.ExpiresAt
|
||||
}
|
||||
}
|
||||
|
||||
// removeFromBucket removes the entry from its corresponding bucket. Has to be called with lock!
|
||||
func (c *LRU[K, V]) removeFromBucket(e *internal.Entry[K, V]) {
|
||||
delete(c.buckets[e.ExpireBucket].entries, e.Key)
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE_list file.
|
||||
|
||||
package internal
|
||||
|
||||
import "time"
|
||||
|
||||
// Entry is an LRU Entry
|
||||
type Entry[K comparable, V any] struct {
|
||||
// Next and previous pointers in the doubly-linked list of elements.
|
||||
// To simplify the implementation, internally a list l is implemented
|
||||
// as a ring, such that &l.root is both the next element of the last
|
||||
// list element (l.Back()) and the previous element of the first list
|
||||
// element (l.Front()).
|
||||
next, prev *Entry[K, V]
|
||||
|
||||
// The list to which this element belongs.
|
||||
list *LruList[K, V]
|
||||
|
||||
// The LRU Key of this element.
|
||||
Key K
|
||||
|
||||
// The Value stored with this element.
|
||||
Value V
|
||||
|
||||
// The time this element would be cleaned up, optional
|
||||
ExpiresAt time.Time
|
||||
|
||||
// The expiry bucket item was put in, optional
|
||||
ExpireBucket uint8
|
||||
}
|
||||
|
||||
// PrevEntry returns the previous list element or nil.
|
||||
func (e *Entry[K, V]) PrevEntry() *Entry[K, V] {
|
||||
if p := e.prev; e.list != nil && p != &e.list.root {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LruList represents a doubly linked list.
|
||||
// The zero Value for LruList is an empty list ready to use.
|
||||
type LruList[K comparable, V any] struct {
|
||||
root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used
|
||||
len int // current list Length excluding (this) sentinel element
|
||||
}
|
||||
|
||||
// Init initializes or clears list l.
|
||||
func (l *LruList[K, V]) Init() *LruList[K, V] {
|
||||
l.root.next = &l.root
|
||||
l.root.prev = &l.root
|
||||
l.len = 0
|
||||
return l
|
||||
}
|
||||
|
||||
// NewList returns an initialized list.
|
||||
func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() }
|
||||
|
||||
// Length returns the number of elements of list l.
|
||||
// The complexity is O(1).
|
||||
func (l *LruList[K, V]) Length() int { return l.len }
|
||||
|
||||
// Back returns the last element of list l or nil if the list is empty.
|
||||
func (l *LruList[K, V]) Back() *Entry[K, V] {
|
||||
if l.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.root.prev
|
||||
}
|
||||
|
||||
// lazyInit lazily initializes a zero List Value.
|
||||
func (l *LruList[K, V]) lazyInit() {
|
||||
if l.root.next == nil {
|
||||
l.Init()
|
||||
}
|
||||
}
|
||||
|
||||
// insert inserts e after at, increments l.len, and returns e.
|
||||
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
e.list = l
|
||||
l.len++
|
||||
return e
|
||||
}
|
||||
|
||||
// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at).
|
||||
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
|
||||
return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
|
||||
}
|
||||
|
||||
// Remove removes e from its list, decrements l.len
|
||||
func (l *LruList[K, V]) Remove(e *Entry[K, V]) V {
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
e.next = nil // avoid memory leaks
|
||||
e.prev = nil // avoid memory leaks
|
||||
e.list = nil
|
||||
l.len--
|
||||
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// move moves e to next to at.
|
||||
func (l *LruList[K, V]) move(e, at *Entry[K, V]) {
|
||||
if e == at {
|
||||
return
|
||||
}
|
||||
e.prev.next = e.next
|
||||
e.next.prev = e.prev
|
||||
|
||||
e.prev = at
|
||||
e.next = at.next
|
||||
e.prev.next = e
|
||||
e.next.prev = e
|
||||
}
|
||||
|
||||
// PushFront inserts a new element e with value v at the front of list l and returns e.
|
||||
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(k, v, time.Time{}, &l.root)
|
||||
}
|
||||
|
||||
// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e.
|
||||
func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] {
|
||||
l.lazyInit()
|
||||
return l.insertValue(k, v, expiresAt, &l.root)
|
||||
}
|
||||
|
||||
// MoveToFront moves element e to the front of list l.
|
||||
// If e is not an element of l, the list is not modified.
|
||||
// The element must not be nil.
|
||||
func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {
|
||||
if e.list != l || l.root.next == e {
|
||||
return
|
||||
}
|
||||
// see comment in List.Remove about initialization of l
|
||||
l.move(e, &l.root)
|
||||
}
|
||||
Generated
Vendored
+54
-35
@@ -1,9 +1,12 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/golang-lru/simplelru"
|
||||
"github.com/hashicorp/golang-lru/v2/simplelru"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,23 +15,24 @@ const (
|
||||
)
|
||||
|
||||
// Cache is a thread-safe fixed size LRU cache.
|
||||
type Cache struct {
|
||||
lru *simplelru.LRU
|
||||
evictedKeys, evictedVals []interface{}
|
||||
onEvictedCB func(k, v interface{})
|
||||
lock sync.RWMutex
|
||||
type Cache[K comparable, V any] struct {
|
||||
lru *simplelru.LRU[K, V]
|
||||
evictedKeys []K
|
||||
evictedVals []V
|
||||
onEvictedCB func(k K, v V)
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates an LRU of the given size.
|
||||
func New(size int) (*Cache, error) {
|
||||
return NewWithEvict(size, nil)
|
||||
func New[K comparable, V any](size int) (*Cache[K, V], error) {
|
||||
return NewWithEvict[K, V](size, nil)
|
||||
}
|
||||
|
||||
// NewWithEvict constructs a fixed size cache with the given eviction
|
||||
// callback.
|
||||
func NewWithEvict(size int, onEvicted func(key, value interface{})) (c *Cache, err error) {
|
||||
func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) {
|
||||
// create a cache with default settings
|
||||
c = &Cache{
|
||||
c = &Cache[K, V]{
|
||||
onEvictedCB: onEvicted,
|
||||
}
|
||||
if onEvicted != nil {
|
||||
@@ -39,21 +43,22 @@ func NewWithEvict(size int, onEvicted func(key, value interface{})) (c *Cache, e
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache) initEvictBuffers() {
|
||||
c.evictedKeys = make([]interface{}, 0, DefaultEvictedBufferSize)
|
||||
c.evictedVals = make([]interface{}, 0, DefaultEvictedBufferSize)
|
||||
func (c *Cache[K, V]) initEvictBuffers() {
|
||||
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
|
||||
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
|
||||
}
|
||||
|
||||
// onEvicted save evicted key/val and sent in externally registered callback
|
||||
// outside of critical section
|
||||
func (c *Cache) onEvicted(k, v interface{}) {
|
||||
func (c *Cache[K, V]) onEvicted(k K, v V) {
|
||||
c.evictedKeys = append(c.evictedKeys, k)
|
||||
c.evictedVals = append(c.evictedVals, v)
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *Cache) Purge() {
|
||||
var ks, vs []interface{}
|
||||
func (c *Cache[K, V]) Purge() {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
c.lru.Purge()
|
||||
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
|
||||
@@ -70,8 +75,9 @@ func (c *Cache) Purge() {
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *Cache) Add(key, value interface{}) (evicted bool) {
|
||||
var k, v interface{}
|
||||
func (c *Cache[K, V]) Add(key K, value V) (evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Add(key, value)
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
@@ -86,7 +92,7 @@ func (c *Cache) Add(key, value interface{}) (evicted bool) {
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *Cache) Get(key interface{}) (value interface{}, ok bool) {
|
||||
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
|
||||
c.lock.Lock()
|
||||
value, ok = c.lru.Get(key)
|
||||
c.lock.Unlock()
|
||||
@@ -95,7 +101,7 @@ func (c *Cache) Get(key interface{}) (value interface{}, ok bool) {
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the
|
||||
// recent-ness or deleting it for being stale.
|
||||
func (c *Cache) Contains(key interface{}) bool {
|
||||
func (c *Cache[K, V]) Contains(key K) bool {
|
||||
c.lock.RLock()
|
||||
containKey := c.lru.Contains(key)
|
||||
c.lock.RUnlock()
|
||||
@@ -104,7 +110,7 @@ func (c *Cache) Contains(key interface{}) bool {
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) {
|
||||
func (c *Cache[K, V]) Peek(key K) (value V, ok bool) {
|
||||
c.lock.RLock()
|
||||
value, ok = c.lru.Peek(key)
|
||||
c.lock.RUnlock()
|
||||
@@ -114,8 +120,9 @@ func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) {
|
||||
// ContainsOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) {
|
||||
var k, v interface{}
|
||||
func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
if c.lru.Contains(key) {
|
||||
c.lock.Unlock()
|
||||
@@ -136,8 +143,9 @@ func (c *Cache) ContainsOrAdd(key, value interface{}) (ok, evicted bool) {
|
||||
// PeekOrAdd checks if a key is in the cache without updating the
|
||||
// recent-ness or deleting it for being stale, and if not, adds the value.
|
||||
// Returns whether found and whether an eviction occurred.
|
||||
func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evicted bool) {
|
||||
var k, v interface{}
|
||||
func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
previous, ok = c.lru.Peek(key)
|
||||
if ok {
|
||||
@@ -153,12 +161,13 @@ func (c *Cache) PeekOrAdd(key, value interface{}) (previous interface{}, ok, evi
|
||||
if c.onEvictedCB != nil && evicted {
|
||||
c.onEvictedCB(k, v)
|
||||
}
|
||||
return nil, false, evicted
|
||||
return
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *Cache) Remove(key interface{}) (present bool) {
|
||||
var k, v interface{}
|
||||
func (c *Cache[K, V]) Remove(key K) (present bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
present = c.lru.Remove(key)
|
||||
if c.onEvictedCB != nil && present {
|
||||
@@ -173,8 +182,9 @@ func (c *Cache) Remove(key interface{}) (present bool) {
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *Cache) Resize(size int) (evicted int) {
|
||||
var ks, vs []interface{}
|
||||
func (c *Cache[K, V]) Resize(size int) (evicted int) {
|
||||
var ks []K
|
||||
var vs []V
|
||||
c.lock.Lock()
|
||||
evicted = c.lru.Resize(size)
|
||||
if c.onEvictedCB != nil && evicted > 0 {
|
||||
@@ -191,8 +201,9 @@ func (c *Cache) Resize(size int) (evicted int) {
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) {
|
||||
var k, v interface{}
|
||||
func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
||||
var k K
|
||||
var v V
|
||||
c.lock.Lock()
|
||||
key, value, ok = c.lru.RemoveOldest()
|
||||
if c.onEvictedCB != nil && ok {
|
||||
@@ -207,7 +218,7 @@ func (c *Cache) RemoveOldest() (key, value interface{}, ok bool) {
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *Cache) GetOldest() (key, value interface{}, ok bool) {
|
||||
func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
c.lock.RLock()
|
||||
key, value, ok = c.lru.GetOldest()
|
||||
c.lock.RUnlock()
|
||||
@@ -215,15 +226,23 @@ func (c *Cache) GetOldest() (key, value interface{}, ok bool) {
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *Cache) Keys() []interface{} {
|
||||
func (c *Cache[K, V]) Keys() []K {
|
||||
c.lock.RLock()
|
||||
keys := c.lru.Keys()
|
||||
c.lock.RUnlock()
|
||||
return keys
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
func (c *Cache[K, V]) Values() []V {
|
||||
c.lock.RLock()
|
||||
values := c.lru.Values()
|
||||
c.lock.RUnlock()
|
||||
return values
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache) Len() int {
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
c.lock.RLock()
|
||||
length := c.lru.Len()
|
||||
c.lock.RUnlock()
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
This license applies to simplelru/list.go
|
||||
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+61
-61
@@ -1,46 +1,45 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package simplelru
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/internal"
|
||||
)
|
||||
|
||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||
type EvictCallback func(key interface{}, value interface{})
|
||||
type EvictCallback[K comparable, V any] func(key K, value V)
|
||||
|
||||
// LRU implements a non-thread safe fixed size LRU cache
|
||||
type LRU struct {
|
||||
type LRU[K comparable, V any] struct {
|
||||
size int
|
||||
evictList *list.List
|
||||
items map[interface{}]*list.Element
|
||||
onEvict EvictCallback
|
||||
}
|
||||
|
||||
// entry is used to hold a value in the evictList
|
||||
type entry struct {
|
||||
key interface{}
|
||||
value interface{}
|
||||
evictList *internal.LruList[K, V]
|
||||
items map[K]*internal.Entry[K, V]
|
||||
onEvict EvictCallback[K, V]
|
||||
}
|
||||
|
||||
// NewLRU constructs an LRU of the given size
|
||||
func NewLRU(size int, onEvict EvictCallback) (*LRU, error) {
|
||||
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
|
||||
if size <= 0 {
|
||||
return nil, errors.New("must provide a positive size")
|
||||
}
|
||||
c := &LRU{
|
||||
|
||||
c := &LRU[K, V]{
|
||||
size: size,
|
||||
evictList: list.New(),
|
||||
items: make(map[interface{}]*list.Element),
|
||||
evictList: internal.NewList[K, V](),
|
||||
items: make(map[K]*internal.Entry[K, V]),
|
||||
onEvict: onEvict,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Purge is used to completely clear the cache.
|
||||
func (c *LRU) Purge() {
|
||||
func (c *LRU[K, V]) Purge() {
|
||||
for k, v := range c.items {
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(k, v.Value.(*entry).value)
|
||||
c.onEvict(k, v.Value)
|
||||
}
|
||||
delete(c.items, k)
|
||||
}
|
||||
@@ -48,20 +47,19 @@ func (c *LRU) Purge() {
|
||||
}
|
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occurred.
|
||||
func (c *LRU) Add(key, value interface{}) (evicted bool) {
|
||||
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
ent.Value.(*entry).value = value
|
||||
ent.Value = value
|
||||
return false
|
||||
}
|
||||
|
||||
// Add new item
|
||||
ent := &entry{key, value}
|
||||
entry := c.evictList.PushFront(ent)
|
||||
c.items[key] = entry
|
||||
ent := c.evictList.PushFront(key, value)
|
||||
c.items[key] = ent
|
||||
|
||||
evict := c.evictList.Len() > c.size
|
||||
evict := c.evictList.Length() > c.size
|
||||
// Verify size not exceeded
|
||||
if evict {
|
||||
c.removeOldest()
|
||||
@@ -70,37 +68,34 @@ func (c *LRU) Add(key, value interface{}) (evicted bool) {
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *LRU) Get(key interface{}) (value interface{}, ok bool) {
|
||||
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
if ent.Value.(*entry) == nil {
|
||||
return nil, false
|
||||
}
|
||||
return ent.Value.(*entry).value, true
|
||||
return ent.Value, true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Contains checks if a key is in the cache, without updating the recent-ness
|
||||
// or deleting it for being stale.
|
||||
func (c *LRU) Contains(key interface{}) (ok bool) {
|
||||
func (c *LRU[K, V]) Contains(key K) (ok bool) {
|
||||
_, ok = c.items[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Peek returns the key value (or undefined if not found) without updating
|
||||
// the "recently used"-ness of the key.
|
||||
func (c *LRU) Peek(key interface{}) (value interface{}, ok bool) {
|
||||
var ent *list.Element
|
||||
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
|
||||
var ent *internal.Entry[K, V]
|
||||
if ent, ok = c.items[key]; ok {
|
||||
return ent.Value.(*entry).value, true
|
||||
return ent.Value, true
|
||||
}
|
||||
return nil, ok
|
||||
return
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache, returning if the
|
||||
// key was contained.
|
||||
func (c *LRU) Remove(key interface{}) (present bool) {
|
||||
func (c *LRU[K, V]) Remove(key K) (present bool) {
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.removeElement(ent)
|
||||
return true
|
||||
@@ -109,44 +104,51 @@ func (c *LRU) Remove(key interface{}) (present bool) {
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *LRU) RemoveOldest() (key, value interface{}, ok bool) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
c.removeElement(ent)
|
||||
kv := ent.Value.(*entry)
|
||||
return kv.key, kv.value, true
|
||||
return ent.Key, ent.Value, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return
|
||||
}
|
||||
|
||||
// GetOldest returns the oldest entry
|
||||
func (c *LRU) GetOldest() (key, value interface{}, ok bool) {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
kv := ent.Value.(*entry)
|
||||
return kv.key, kv.value, true
|
||||
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
return ent.Key, ent.Value, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return
|
||||
}
|
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *LRU) Keys() []interface{} {
|
||||
keys := make([]interface{}, len(c.items))
|
||||
func (c *LRU[K, V]) Keys() []K {
|
||||
keys := make([]K, c.evictList.Length())
|
||||
i := 0
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
keys[i] = ent.Value.(*entry).key
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
||||
keys[i] = ent.Key
|
||||
i++
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
func (c *LRU[K, V]) Values() []V {
|
||||
values := make([]V, len(c.items))
|
||||
i := 0
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
|
||||
values[i] = ent.Value
|
||||
i++
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *LRU) Len() int {
|
||||
return c.evictList.Len()
|
||||
func (c *LRU[K, V]) Len() int {
|
||||
return c.evictList.Length()
|
||||
}
|
||||
|
||||
// Resize changes the cache size.
|
||||
func (c *LRU) Resize(size int) (evicted int) {
|
||||
func (c *LRU[K, V]) Resize(size int) (evicted int) {
|
||||
diff := c.Len() - size
|
||||
if diff < 0 {
|
||||
diff = 0
|
||||
@@ -159,19 +161,17 @@ func (c *LRU) Resize(size int) (evicted int) {
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache.
|
||||
func (c *LRU) removeOldest() {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil {
|
||||
func (c *LRU[K, V]) removeOldest() {
|
||||
if ent := c.evictList.Back(); ent != nil {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache
|
||||
func (c *LRU) removeElement(e *list.Element) {
|
||||
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
|
||||
c.evictList.Remove(e)
|
||||
kv := e.Value.(*entry)
|
||||
delete(c.items, kv.key)
|
||||
delete(c.items, e.Key)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(kv.key, kv.value)
|
||||
c.onEvict(e.Key, e.Value)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+15
-9
@@ -1,33 +1,39 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package simplelru provides simple LRU implementation based on build-in container/list.
|
||||
package simplelru
|
||||
|
||||
// LRUCache is the interface for simple LRU cache.
|
||||
type LRUCache interface {
|
||||
type LRUCache[K comparable, V any] interface {
|
||||
// Adds a value to the cache, returns true if an eviction occurred and
|
||||
// updates the "recently used"-ness of the key.
|
||||
Add(key, value interface{}) bool
|
||||
Add(key K, value V) bool
|
||||
|
||||
// Returns key's value from the cache and
|
||||
// updates the "recently used"-ness of the key. #value, isFound
|
||||
Get(key interface{}) (value interface{}, ok bool)
|
||||
Get(key K) (value V, ok bool)
|
||||
|
||||
// Checks if a key exists in cache without updating the recent-ness.
|
||||
Contains(key interface{}) (ok bool)
|
||||
Contains(key K) (ok bool)
|
||||
|
||||
// Returns key's value without updating the "recently used"-ness of the key.
|
||||
Peek(key interface{}) (value interface{}, ok bool)
|
||||
Peek(key K) (value V, ok bool)
|
||||
|
||||
// Removes a key from the cache.
|
||||
Remove(key interface{}) bool
|
||||
Remove(key K) bool
|
||||
|
||||
// Removes the oldest entry from cache.
|
||||
RemoveOldest() (interface{}, interface{}, bool)
|
||||
RemoveOldest() (K, V, bool)
|
||||
|
||||
// Returns the oldest entry from the cache. #key, value, isFound
|
||||
GetOldest() (interface{}, interface{}, bool)
|
||||
GetOldest() (K, V, bool)
|
||||
|
||||
// Returns a slice of the keys in the cache, from oldest to newest.
|
||||
Keys() []interface{}
|
||||
Keys() []K
|
||||
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
Values() []V
|
||||
|
||||
// Returns the number of items in the cache.
|
||||
Len() int
|
||||
Vendored
+9
-8
@@ -86,11 +86,10 @@ github.com/go-pkgz/expirable-cache
|
||||
# github.com/go-pkgz/jrpc v0.3.0
|
||||
## explicit; go 1.16
|
||||
github.com/go-pkgz/jrpc
|
||||
# github.com/go-pkgz/lcw v1.1.0
|
||||
# github.com/go-pkgz/lcw/v2 v2.0.0
|
||||
## explicit; go 1.21
|
||||
github.com/go-pkgz/lcw
|
||||
github.com/go-pkgz/lcw/eventbus
|
||||
github.com/go-pkgz/lcw/internal/cache
|
||||
github.com/go-pkgz/lcw/v2
|
||||
github.com/go-pkgz/lcw/v2/eventbus
|
||||
# github.com/go-pkgz/lgr v0.11.1
|
||||
## explicit; go 1.20
|
||||
github.com/go-pkgz/lgr
|
||||
@@ -136,10 +135,12 @@ github.com/hashicorp/errwrap
|
||||
# github.com/hashicorp/go-multierror v1.1.1
|
||||
## explicit; go 1.13
|
||||
github.com/hashicorp/go-multierror
|
||||
# github.com/hashicorp/golang-lru v1.0.2
|
||||
## explicit; go 1.12
|
||||
github.com/hashicorp/golang-lru
|
||||
github.com/hashicorp/golang-lru/simplelru
|
||||
# github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
## explicit; go 1.18
|
||||
github.com/hashicorp/golang-lru/v2
|
||||
github.com/hashicorp/golang-lru/v2/expirable
|
||||
github.com/hashicorp/golang-lru/v2/internal
|
||||
github.com/hashicorp/golang-lru/v2/simplelru
|
||||
# github.com/jessevdk/go-flags v1.5.0
|
||||
## explicit; go 1.15
|
||||
github.com/jessevdk/go-flags
|
||||
|
||||
Reference in New Issue
Block a user