update to lcw v2 with generic types

This commit is contained in:
Dmitry Verkhoturov
2024-02-20 14:15:35 -06:00
committed by Umputun
parent 3210de8f7b
commit 1313dee829
49 changed files with 1162 additions and 1140 deletions
+14 -12
View File
@@ -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 })
}
+6 -1
View File
@@ -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"),
)
}
+2
View File
@@ -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"),
)
}
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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")
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+7 -3
View File
@@ -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"),
)
}
+1 -1
View File
@@ -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"
+5 -4
View File
@@ -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
}
}
+11 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,86 +0,0 @@
# Loading Cache Wrapper [![Build Status](https://github.com/go-pkgz/lcw/workflows/build/badge.svg)](https://github.com/go-pkgz/lcw/actions) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/lcw/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/lcw?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/lcw?status.svg)](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
View File
@@ -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
View File
@@ -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
}
}
@@ -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
}
@@ -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))
@@ -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
}
@@ -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
}
@@ -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
}
}
@@ -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
}
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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 {
+79
View File
@@ -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
View File
@@ -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
@@ -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
View File
@@ -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)
}
@@ -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()
@@ -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.
@@ -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)
}
}
@@ -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
+9 -8
View File
@@ -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