From 588af7fc9afc72569142f3a7d7485a8cacb89d32 Mon Sep 17 00:00:00 2001 From: Umputun Date: Sat, 2 Feb 2019 18:25:57 -0600 Subject: [PATCH] switch title to lcw cache, optimize error hits --- backend/Gopkg.lock | 9 ++ backend/app/store/service/title.go | 19 +-- backend/app/store/service/title_test.go | 19 +++ .../vendor/github.com/go-pkgz/lcw/.gitignore | 12 ++ .../vendor/github.com/go-pkgz/lcw/.travis.yml | 21 +++ backend/vendor/github.com/go-pkgz/lcw/LICENSE | 21 +++ .../vendor/github.com/go-pkgz/lcw/README.md | 52 +++++++ .../github.com/go-pkgz/lcw/expirable_cache.go | 133 ++++++++++++++++++ backend/vendor/github.com/go-pkgz/lcw/go.mod | 9 ++ backend/vendor/github.com/go-pkgz/lcw/go.sum | 15 ++ .../github.com/go-pkgz/lcw/interface.go | 61 ++++++++ .../github.com/go-pkgz/lcw/lru_cache.go | 128 +++++++++++++++++ .../vendor/github.com/go-pkgz/lcw/options.go | 77 ++++++++++ 13 files changed, 568 insertions(+), 8 deletions(-) create mode 100644 backend/vendor/github.com/go-pkgz/lcw/.gitignore create mode 100644 backend/vendor/github.com/go-pkgz/lcw/.travis.yml create mode 100644 backend/vendor/github.com/go-pkgz/lcw/LICENSE create mode 100644 backend/vendor/github.com/go-pkgz/lcw/README.md create mode 100644 backend/vendor/github.com/go-pkgz/lcw/expirable_cache.go create mode 100644 backend/vendor/github.com/go-pkgz/lcw/go.mod create mode 100644 backend/vendor/github.com/go-pkgz/lcw/go.sum create mode 100644 backend/vendor/github.com/go-pkgz/lcw/interface.go create mode 100644 backend/vendor/github.com/go-pkgz/lcw/lru_cache.go create mode 100644 backend/vendor/github.com/go-pkgz/lcw/options.go diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 954d6b43..acc5ddc4 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -126,6 +126,14 @@ revision = "7597c083287c33ba5362687a7815a032fd92c418" version = "v0.4.1" +[[projects]] + digest = "1:c4cb8b521f8e55dd02e1ed0952953adb35dbcc8eca8aeac9d707abc7ed625843" + name = "github.com/go-pkgz/lcw" + packages = ["."] + pruneopts = "UT" + revision = "4f4492679c5c96714bc4b25d195036534bb01f72" + version = "v0.2.0" + [[projects]] digest = "1:7b1f422f560b103f435f8501f854f238f1ce00d0f8146e76eacdfa71f2c8d8c0" name = "github.com/go-pkgz/lgr" @@ -413,6 +421,7 @@ "github.com/go-pkgz/auth/avatar", "github.com/go-pkgz/auth/provider", "github.com/go-pkgz/auth/token", + "github.com/go-pkgz/lcw", "github.com/go-pkgz/lgr", "github.com/go-pkgz/mongo", "github.com/go-pkgz/repeater", diff --git a/backend/app/store/service/title.go b/backend/app/store/service/title.go index a8a72c80..00bc4e94 100644 --- a/backend/app/store/service/title.go +++ b/backend/app/store/service/title.go @@ -3,9 +3,10 @@ package service import ( "io" "net/http" + "time" + "github.com/go-pkgz/lcw" log "github.com/go-pkgz/lgr" - "github.com/go-pkgz/rest/cache" "github.com/pkg/errors" "golang.org/x/net/html" ) @@ -15,7 +16,7 @@ const teMaxCachedRecs = 1000 // TitleExtractor gets html title from remote page, cached type TitleExtractor struct { client http.Client - cache cache.LoadingCache + cache lcw.LoadingCache } // NewTitleExtractor makes extractor with cache. If memory cache failed, switching to no-cache @@ -24,10 +25,10 @@ func NewTitleExtractor(client http.Client) *TitleExtractor { client: client, } var err error - res.cache, err = cache.NewMemoryCache(cache.MaxKeys(teMaxCachedRecs)) + res.cache, err = lcw.NewExpirableCache(lcw.TTL(15*time.Minute), lcw.MaxKeySize(teMaxCachedRecs)) if err != nil { - log.Printf("[WARN] failed to make cache, %v", err) - res.cache = &cache.Nop{} + log.Printf("[WARN] failed to make cache, caching disabled for titles, %v", err) + res.cache = &lcw.Nop{} } return &res } @@ -35,7 +36,7 @@ func NewTitleExtractor(client http.Client) *TitleExtractor { // Get page for url and return title func (t *TitleExtractor) Get(url string) (string, error) { client := http.Client{Timeout: t.client.Timeout, Transport: t.client.Transport} - b, err := t.cache.Get(cache.NewKey("site").ID(url), func() ([]byte, error) { + b, err := t.cache.Get(url, func() (lcw.Value, error) { resp, err := client.Get(url) if err != nil { return nil, errors.Wrapf(err, "failed to load page %s", url) @@ -49,14 +50,16 @@ func (t *TitleExtractor) Get(url string) (string, error) { if !ok { return nil, errors.Errorf("can't get title for %s", url) } - return []byte(title), nil + return title, nil }) + // on error save result (empty strung) to cache too but if err != nil { + _, _ = t.cache.Get(url, func() (lcw.Value, error) { return "", nil }) return "", err } - return string(b), nil + return b.(string), nil } // get title from body reader, traverse recursively diff --git a/backend/app/store/service/title_test.go b/backend/app/store/service/title_test.go index c0a2d94f..0efa49e2 100644 --- a/backend/app/store/service/title_test.go +++ b/backend/app/store/service/title_test.go @@ -93,3 +93,22 @@ func TestTitle_GetConcurrent(t *testing.T) { g.Wait() assert.Equal(t, int32(100), atomic.LoadInt32(&hits)) } + +func TestTitle_GetFailed(t *testing.T) { + ex := NewTitleExtractor(http.Client{Timeout: 5 * time.Second}) + var hits int32 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&hits, 1) + w.WriteHeader(404) + })) + + _, err := ex.Get(ts.URL + "/bad") + require.NotNil(t, err) + + for i := 0; i < 100; i++ { + title, err := ex.Get(ts.URL + "/bad") + require.Nil(t, err) + assert.Equal(t, "", title) + } + assert.Equal(t, int32(1), atomic.LoadInt32(&hits), "hit once, errors cached") +} diff --git a/backend/vendor/github.com/go-pkgz/lcw/.gitignore b/backend/vendor/github.com/go-pkgz/lcw/.gitignore new file mode 100644 index 00000000..f1c181ec --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/.gitignore @@ -0,0 +1,12 @@ +# 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 diff --git a/backend/vendor/github.com/go-pkgz/lcw/.travis.yml b/backend/vendor/github.com/go-pkgz/lcw/.travis.yml new file mode 100644 index 00000000..0e3d2500 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/.travis.yml @@ -0,0 +1,21 @@ +language: go + +go: + - "1.11.x" + +install: true + +go_import_path: github.com/go-pkgz/lcw + +before_install: + - export TZ=America/Chicago + - curl -L https://git.io/vp6lP | sh + - go get github.com/mattn/goveralls + - export PATH=$(pwd)/bin:$PATH + +script: + - GO111MODULE=on go get ./... + - GO111MODULE=on go mod vendor + - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; + - ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --exclude=_example --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1; + - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/backend/vendor/github.com/go-pkgz/lcw/LICENSE b/backend/vendor/github.com/go-pkgz/lcw/LICENSE new file mode 100644 index 00000000..ac540250 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Umputun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/vendor/github.com/go-pkgz/lcw/README.md b/backend/vendor/github.com/go-pkgz/lcw/README.md new file mode 100644 index 00000000..73eb21ce --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/README.md @@ -0,0 +1,52 @@ +# Loading Cache Wrapper [![Build Status](https://travis-ci.org/go-pkgz/lcw.svg?branch=master)](https://travis-ci.org/go-pkgz/lcw) [![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 [patrickmn/go-cache](https://github.com/patrickmn/go-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 | +| 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` only) +- Functional style invalidation +- Functional options +- Sane defaults + +## Install and update + +`go get -u github.com/go-pkgz/lcw` + +## Usage + +``` +cache := lcw.NewLruCache(lcw.MaxKeys(500), lcw.MaxCacheSize(65536), lcw.MaxValSize(200), lcw.MaxKeySize(32)) + +val, err := cache.Get("key123", func() (lcw.Value, error) { + res, err := getDataFromSomeSource(params) // returns string + return res, err +}) + +if err != nil { + panic("failed to get data") +} + +s := val.(string) // cached value + +``` + +## Details + +- All byte-size limits (MaxCacheSize and MaxValSize) only work for values implementing `lcw.Sizer` interface. +- Negative limits (max options) rejected +- `lgr.Value` wraps `interface{}` and should be converted back to the concrete type. +- 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 finaly generalized to become `lcw`. diff --git a/backend/vendor/github.com/go-pkgz/lcw/expirable_cache.go b/backend/vendor/github.com/go-pkgz/lcw/expirable_cache.go new file mode 100644 index 00000000..469769c1 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/expirable_cache.go @@ -0,0 +1,133 @@ +package lcw + +import ( + "sync/atomic" + "time" + + cache "github.com/patrickmn/go-cache" + "github.com/pkg/errors" +) + +// ExpirableCache implements LoadingCache with TTL. +type ExpirableCache struct { + options + CacheStat + currentSize int64 + currKeys int64 + backend *cache.Cache +} + +// NewExpirableCache makes expirable LoadingCache implementation, 1000 max keys by default and 5s TTL +func NewExpirableCache(opts ...Option) (*ExpirableCache, error) { + + res := ExpirableCache{ + options: options{ + maxKeys: 1000, + maxValueSize: 0, + ttl: 5 * time.Minute, + }, + } + + for _, opt := range opts { + if err := opt(&res.options); err != nil { + return nil, errors.Wrap(err, "failed to set cache option") + } + } + + res.backend = cache.New(res.ttl, res.ttl/2) + + // OnEvicted called automatically for expired and manually deleted + res.backend.OnEvicted(func(key string, value interface{}) { + atomic.AddInt64(&res.currKeys, -1) + if s, ok := value.(Sizer); ok { + size := s.Size() + atomic.AddInt64(&res.currentSize, -1*int64(size)) + } + }) + + 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() (Value, error)) (data Value, err error) { + + if v, ok := c.backend.Get(key); ok { + atomic.AddInt64(&c.Hits, 1) + return v, nil + } + + if data, err = fn(); err != nil { + atomic.AddInt64(&c.Errors, 1) + return data, err + } + atomic.AddInt64(&c.Misses, 1) + + if c.allowed(key, data) { + if s, ok := 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())) + } + atomic.AddInt64(&c.currKeys, 1) + _ = c.backend.Add(key, data, time.Second) + } + + 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) { + for key := range c.backend.Items() { // Keys() returns copy of cache's key, safe to remove directly + if fn(key) { + c.backend.Delete(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) (Value, bool) { + return c.backend.Get(key) +} + +// Purge clears the cache completely. +func (c *ExpirableCache) Purge() { + c.backend.Flush() + atomic.StoreInt64(&c.currentSize, 0) + atomic.StoreInt64(&c.currKeys, 0) +} + +// Stat returns cache statistics +func (c *ExpirableCache) Stat() CacheStat { + return CacheStat{ + Hits: c.Hits, + Misses: c.Misses, + Size: c.size(), + Keys: c.keys(), + Errors: c.Errors, + } +} + +func (c *ExpirableCache) size() int64 { + return atomic.LoadInt64(&c.currentSize) +} + +func (c *ExpirableCache) keys() int { + return int(atomic.LoadInt64(&c.currKeys)) +} + +func (c *ExpirableCache) allowed(key string, data Value) bool { + if atomic.LoadInt64(&c.currKeys) >= int64(c.maxKeys) { + return false + } + if c.maxKeySize > 0 && len(key) > c.maxKeySize { + return false + } + if s, ok := data.(Sizer); ok { + if c.maxValueSize > 0 && s.Size() >= c.maxValueSize { + return false + } + } + return true +} diff --git a/backend/vendor/github.com/go-pkgz/lcw/go.mod b/backend/vendor/github.com/go-pkgz/lcw/go.mod new file mode 100644 index 00000000..42c8a662 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/go.mod @@ -0,0 +1,9 @@ +module github.com/go-pkgz/lcw + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/golang-lru v0.5.0 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.3.0 +) diff --git a/backend/vendor/github.com/go-pkgz/lcw/go.sum b/backend/vendor/github.com/go-pkgz/lcw/go.sum new file mode 100644 index 00000000..57d795fe --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/backend/vendor/github.com/go-pkgz/lcw/interface.go b/backend/vendor/github.com/go-pkgz/lcw/interface.go new file mode 100644 index 00000000..95ba97ba --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/interface.go @@ -0,0 +1,61 @@ +package lcw + +import "fmt" + +// Value type wraps interface{} +type Value interface{} + +// Sizer allows to perform size-based restrictions, optional. +// If not defined both maxValueSize and maxCacheSize checks will be ignored +type Sizer interface { + Size() int +} + +// LoadingCache defines guava-like cache with Get method returning cached value ao retriving it if not in cache +type LoadingCache interface { + Get(key string, fn func() (Value, error)) (val Value, err error) // load or get from cache + Peek(key string) (Value, bool) // get from cache by key + Invalidate(fn func(key string) bool) // invalidate items for func(key) == true + Purge() // clear cache + Stat() CacheStat // cache stats +} + +// CacheStat represent stats values +type CacheStat struct { + Hits int64 + Misses int64 + Keys int + Size int64 + Errors int64 +} + +// String fromats cache stats +func (s *CacheStat) String() string { + return fmt.Sprintf("{hits:%d, misses:%d, ratio:%.1f%%, keys:%d, size:%d, errors:%d}", + s.Hits, s.Misses, 100*(float64(s.Hits)/float64(s.Hits+s.Misses)), s.Keys, s.Size, s.Errors) +} + +// Nop is do-nothing implementation of LoadingCache +type Nop struct{} + +// NewNopCache makes new do-nothing cache +func NewNopCache() *Nop { + return &Nop{} +} + +// Get calls fn without any caching +func (n *Nop) Get(key string, fn func() (Value, error)) (Value, error) { return fn() } + +// Peek does nothing and always returns false +func (n *Nop) Peek(key string) (Value, bool) { return nil, false } + +// Invalidate does nothing for nop cache +func (n *Nop) Invalidate(fn func(key string) bool) {} + +// Purge does nothing for nop cache +func (n *Nop) Purge() {} + +// Stat always 0s for nop cache +func (n *Nop) Stat() CacheStat { + return CacheStat{} +} diff --git a/backend/vendor/github.com/go-pkgz/lcw/lru_cache.go b/backend/vendor/github.com/go-pkgz/lcw/lru_cache.go new file mode 100644 index 00000000..cb8c8ece --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/lru_cache.go @@ -0,0 +1,128 @@ +package lcw + +import ( + "sync/atomic" + + lru "github.com/hashicorp/golang-lru" + "github.com/pkg/errors" +) + +// LruCache wraps lru.LruCache with laoding cache Get and size limits +type LruCache struct { + options + CacheStat + backend *lru.Cache + currentSize int64 +} + +// NewLruCache makes LRU LoadingCache implementation, 1000 max keys by default +func NewLruCache(opts ...Option) (*LruCache, error) { + + res := LruCache{ + options: options{ + maxKeys: 1000, + maxValueSize: 0, + }, + } + for _, opt := range opts { + if err := opt(&res.options); err != nil { + return nil, errors.Wrap(err, "failed to set cache option") + } + } + + onEvicted := func(key interface{}, value interface{}) { + if s, ok := value.(Sizer); ok { + size := s.Size() + atomic.AddInt64(&res.currentSize, -1*int64(size)) + } + } + + var err error + // OnEvicted called automatically for expired and manually deleted + if res.backend, err = lru.NewWithEvict(res.maxKeys, onEvicted); err != nil { + return nil, errors.Wrap(err, "failed to make lru cache backend") + } + + return &res, nil +} + +// Get gets value by key or load with fn if not found in cache +func (c *LruCache) Get(key string, fn func() (Value, error)) (data Value, err error) { + + if v, ok := c.backend.Get(key); ok { + atomic.AddInt64(&c.Hits, 1) + return v, nil + } + + if data, err = fn(); err != nil { + atomic.AddInt64(&c.Errors, 1) + return data, err + } + + atomic.AddInt64(&c.Misses, 1) + + if c.allowed(key, data) { + c.backend.Add(key, data) + + if s, ok := 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 { + c.backend.RemoveOldest() + } + } + } + } + return data, nil +} + +// 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) (Value, bool) { + return c.backend.Peek(key) +} + +// Purge clears the cache completely. +func (c *LruCache) 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) { + 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) + } + } +} + +// Stat returns cache statistics +func (c *LruCache) Stat() CacheStat { + return CacheStat{ + Hits: c.Hits, + Misses: c.Misses, + Size: c.size(), + Keys: c.keys(), + Errors: c.Errors, + } +} + +func (c *LruCache) size() int64 { + return atomic.LoadInt64(&c.currentSize) +} + +func (c *LruCache) keys() int { + return c.backend.Len() +} + +func (c *LruCache) allowed(key string, data Value) bool { + if c.maxKeySize > 0 && len(key) > c.maxKeySize { + return false + } + if s, ok := data.(Sizer); ok { + if c.maxValueSize > 0 && s.Size() >= c.maxValueSize { + return false + } + } + return true +} diff --git a/backend/vendor/github.com/go-pkgz/lcw/options.go b/backend/vendor/github.com/go-pkgz/lcw/options.go new file mode 100644 index 00000000..83835c24 --- /dev/null +++ b/backend/vendor/github.com/go-pkgz/lcw/options.go @@ -0,0 +1,77 @@ +package lcw + +import ( + "errors" + "time" +) + +type options struct { + maxKeys int + maxValueSize int + maxKeySize int + maxCacheSize int64 + ttl time.Duration +} + +// Option func type +type Option func(o *options) error + +// 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 { + if max < 0 { + return errors.New("negative max value size") + } + o.maxValueSize = max + return nil + } +} + +// 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 { + if max < 0 { + return errors.New("negative max key size") + } + o.maxKeySize = max + return nil + } +} + +// 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 { + if max < 0 { + return errors.New("negative max keys") + } + o.maxKeys = max + return nil + } +} + +// 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 { + if max < 0 { + return errors.New("negative max cache size") + } + o.maxCacheSize = max + return nil + } +} + +// TTL functional option defines duration. +// Works for ExpirableCache only +func TTL(ttl time.Duration) Option { + return func(o *options) error { + if ttl < 0 { + return errors.New("negative ttl") + } + o.ttl = ttl + return nil + } +}