switch title to lcw cache, optimize error hits
This commit is contained in:
Generated
+9
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
+12
@@ -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
|
||||
+21
@@ -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
|
||||
+21
@@ -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.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# Loading Cache Wrapper [](https://travis-ci.org/go-pkgz/lcw) [](https://coveralls.io/github/go-pkgz/lcw?branch=master) [](https://godoc.org/github.com/go-pkgz/lcw)
|
||||
|
||||
|
||||
The library adds a thin layer on top of [lru cache](https://github.com/hashicorp/golang-lru) and [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`.
|
||||
+133
@@ -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
|
||||
}
|
||||
+9
@@ -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
|
||||
)
|
||||
+15
@@ -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=
|
||||
+61
@@ -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{}
|
||||
}
|
||||
+128
@@ -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
|
||||
}
|
||||
+77
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user