diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 788b47f0..81d3c25f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,10 +7,10 @@ jobs: strategy: matrix: os: [macOS-latest, ubuntu-latest] - goversion: [1.13, 1.14, 1.15] + goversion: [1.17, 1.18, '1.19.0-rc.2'] steps: - name: Set up Go ${{matrix.goversion}} on ${{matrix.os}} - uses: actions/setup-go@v1 + uses: actions/setup-go@v3 with: go-version: ${{matrix.goversion}} id: go diff --git a/atomic_int.go b/atomic_int.go new file mode 100644 index 00000000..afd6afef --- /dev/null +++ b/atomic_int.go @@ -0,0 +1,42 @@ +//go:build !go1.19 + +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package galaxycache + +import ( + "strconv" + "sync/atomic" +) + +// An AtomicInt is an int64 to be accessed atomically. +// It thinly wraps atomic.Int64 with go1.19+ +type AtomicInt int64 + +// Add atomically adds n to i. +func (i *AtomicInt) Add(n int64) { + atomic.AddInt64((*int64)(i), n) +} + +// Get atomically gets the value of i. +func (i *AtomicInt) Get() int64 { + return atomic.LoadInt64((*int64)(i)) +} + +func (i *AtomicInt) String() string { + return strconv.FormatInt(i.Get(), 10) +} diff --git a/atomic_int_go1.19.go b/atomic_int_go1.19.go new file mode 100644 index 00000000..c60f9cf1 --- /dev/null +++ b/atomic_int_go1.19.go @@ -0,0 +1,44 @@ +//go:build go1.19 + +/* +Copyright 2022 Vimeo Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package galaxycache + +import ( + "strconv" + "sync/atomic" +) + +// An AtomicInt is an int64 to be accessed atomically. +// It thinly wraps atomic.Int64 with go1.19+ +type AtomicInt struct { + v atomic.Int64 +} + +// Add atomically adds n to i. +func (i *AtomicInt) Add(n int64) { + i.v.Add(n) +} + +// Get atomically gets the value of i. +func (i *AtomicInt) Get() int64 { + return i.v.Load() +} + +func (i *AtomicInt) String() string { + return strconv.FormatInt(i.Get(), 10) +} diff --git a/galaxycache.go b/galaxycache.go index 626db13d..59209d36 100644 --- a/galaxycache.go +++ b/galaxycache.go @@ -28,13 +28,10 @@ import ( "context" "errors" "fmt" - "strconv" "sync" - "sync/atomic" "time" "unsafe" - "github.com/vimeo/galaxycache/lru" "github.com/vimeo/galaxycache/promoter" "github.com/vimeo/galaxycache/singleflight" @@ -153,22 +150,13 @@ func (universe *Universe) NewGalaxy(name string, cacheBytes int64, getter Backen opt.apply(&gOpts) } g := &Galaxy{ - name: name, - getter: getter, - peerPicker: universe.peerPicker, - cacheBytes: cacheBytes, - mainCache: cache{ - ctype: MainCache, - lru: lru.New(0), - }, - hotCache: cache{ - ctype: HotCache, - lru: lru.New(0), - }, - candidateCache: cache{ - ctype: CandidateCache, - lru: lru.New(gOpts.maxCandidates), - }, + name: name, + getter: getter, + peerPicker: universe.peerPicker, + cacheBytes: cacheBytes, + mainCache: newCache(MainCache), + hotCache: newCache(HotCache), + candidateCache: newCandidateCache(gOpts.maxCandidates), hcStatsWithTime: HCStatsWithTime{ hcs: &promoter.HCStats{ HCCapacity: cacheBytes / gOpts.hcRatio, @@ -178,9 +166,6 @@ func (universe *Universe) NewGalaxy(name string, cacheBytes int64, getter Backen } g.mainCache.setLRUOnEvicted(nil) g.hotCache.setLRUOnEvicted(g.candidateCache.addToCandidateCache) - g.candidateCache.lru.OnEvicted = func(key lru.Key, value interface{}) { - g.candidateCache.nevict++ - } g.parent = universe universe.galaxies[name] = g @@ -245,7 +230,7 @@ type Galaxy struct { // of key/value pairs that can be stored globally. hotCache cache - candidateCache cache + candidateCache candidateCache hcStatsWithTime HCStatsWithTime @@ -460,7 +445,7 @@ func (g *Galaxy) Get(ctx context.Context, key string, dest Codec) error { } type valWithLevel struct { - val *valWithStat + val valWithStat level hitLevel localAuthoritative bool peerErr error @@ -468,7 +453,7 @@ type valWithLevel struct { } // load loads key either by invoking the getter locally or by sending it to another machine. -func (g *Galaxy) load(ctx context.Context, key string, dest Codec) (value *valWithStat, destPopulated bool, err error) { +func (g *Galaxy) load(ctx context.Context, key string, dest Codec) (value valWithStat, destPopulated bool, err error) { g.Stats.Loads.Add(1) g.recordStats(ctx, nil, MLoads.M(1)) @@ -556,19 +541,18 @@ func (g *Galaxy) getLocally(ctx context.Context, key string, dest Codec) ([]byte return dest.MarshalBinary() } -func (g *Galaxy) getFromPeer(ctx context.Context, peer RemoteFetcher, key string) (*valWithStat, error) { +func (g *Galaxy) getFromPeer(ctx context.Context, peer RemoteFetcher, key string) (valWithStat, error) { data, err := peer.Fetch(ctx, g.name, key) if err != nil { - return nil, err + return valWithStat{}, err } - vi, ok := g.candidateCache.get(key) + kStats, ok := g.candidateCache.get(key) if !ok { - vi = g.addNewToCandidateCache(key) + kStats = g.addNewToCandidateCache(key) } g.maybeUpdateHotCacheStats() // will update if at least a second has passed since the last update - kStats := vi.(*keyStats) stats := promoter.Stats{ KeyQPS: kStats.val(), HCStats: g.hcStatsWithTime.hcs, @@ -580,23 +564,23 @@ func (g *Galaxy) getFromPeer(ctx context.Context, peer RemoteFetcher, key string return value, nil } -func (g *Galaxy) lookupCache(key string) (*valWithStat, hitLevel) { +func (g *Galaxy) lookupCache(key string) (valWithStat, hitLevel) { if g.cacheBytes <= 0 { - return nil, miss + return valWithStat{}, miss } vi, ok := g.mainCache.get(key) if ok { - return vi.(*valWithStat), hitMaincache + return vi, hitMaincache } vi, ok = g.hotCache.get(key) if !ok { - return nil, miss + return valWithStat{}, miss } g.Stats.HotcacheHits.Add(1) - return vi.(*valWithStat), hitHotcache + return vi, hitHotcache } -func (g *Galaxy) populateCache(ctx context.Context, key string, value *valWithStat, cache *cache) { +func (g *Galaxy) populateCache(ctx context.Context, key string, value valWithStat, cache *cache) { if g.cacheBytes <= 0 { return } @@ -661,24 +645,13 @@ func (g *Galaxy) CacheStats(which CacheType) CacheStats { case HotCache: return g.hotCache.stats() case CandidateCache: - return g.candidateCache.stats() + // not worth tracking this for the CandidateCache + return CacheStats{} default: return CacheStats{} } } -// cache is a wrapper around an *lru.Cache that adds synchronization -// and counts the size of all keys and values. Candidate cache only -// utilizes the lru.Cache and mutex, not the included stats. -type cache struct { - mu sync.Mutex - nbytes int64 // of all keys and values - lru *lru.Cache - nhit, nget int64 - nevict int64 // number of evictions - ctype CacheType -} - func (c *cache) stats() CacheStats { c.mu.Lock() defer c.mu.Unlock() @@ -699,44 +672,21 @@ type valWithStat struct { // sizeOfValWithStats returns the total size of the value in the hot/main // cache, including the data, key stats, and a pointer to the val itself func (v *valWithStat) size() int64 { + const statsSize = int64(unsafe.Sizeof(*v.stats)) + const ptrSize = int64(unsafe.Sizeof(v)) + const vwsSize = int64(unsafe.Sizeof(*v)) // using cap() instead of len() for data leads to inconsistency // after unmarshaling/marshaling the data - return int64(unsafe.Sizeof(*v.stats)) + int64(len(v.data)) + int64(unsafe.Sizeof(v)) + int64(unsafe.Sizeof(*v)) -} - -func (c *cache) setLRUOnEvicted(f func(key string, kStats *keyStats)) { - c.lru.OnEvicted = func(key lru.Key, value interface{}) { - val := value.(*valWithStat) - c.nbytes -= int64(len(key.(string))) + val.size() - c.nevict++ - if f != nil { - f(key.(string), val.stats) - } - } + return statsSize + ptrSize + vwsSize + int64(len(v.data)) } -func (c *cache) add(key string, value *valWithStat) { +func (c *cache) add(key string, value valWithStat) { c.mu.Lock() defer c.mu.Unlock() c.lru.Add(key, value) c.nbytes += int64(len(key)) + value.size() } -func (c *cache) get(key string) (vi interface{}, ok bool) { - c.mu.Lock() - defer c.mu.Unlock() - c.nget++ - if c.lru == nil { - return - } - vi, ok = c.lru.Get(key) - if !ok { - return - } - c.nhit++ - return vi, true -} - func (c *cache) removeOldest() { c.mu.Lock() defer c.mu.Unlock() @@ -765,23 +715,6 @@ func (c *cache) itemsLocked() int64 { return int64(c.lru.Len()) } -// An AtomicInt is an int64 to be accessed atomically. -type AtomicInt int64 - -// Add atomically adds n to i. -func (i *AtomicInt) Add(n int64) { - atomic.AddInt64((*int64)(i), n) -} - -// Get atomically gets the value of i. -func (i *AtomicInt) Get() int64 { - return atomic.LoadInt64((*int64)(i)) -} - -func (i *AtomicInt) String() string { - return strconv.FormatInt(i.Get(), 10) -} - // CacheStats are returned by stats accessors on Galaxy. type CacheStats struct { Bytes int64 diff --git a/galaxycache_test.go b/galaxycache_test.go index fac06279..c89cbabe 100644 --- a/galaxycache_test.go +++ b/galaxycache_test.go @@ -177,8 +177,6 @@ func (fetcher *TestFetcher) Close() error { return nil } -type testFetchers []RemoteFetcher - func (fetcher *TestFetcher) Fetch(ctx context.Context, galaxy string, key string) ([]byte, error) { if fetcher.fail { return nil, errors.New("simulated error from peer") @@ -553,7 +551,10 @@ func TestPromotion(t *testing.T) { } g.getFromPeer(ctx, tf, key) val, okHot := g.hotCache.get(key) - if string(val.(*valWithStat).data) != "got:"+testKey { + if !okHot { + t.Errorf("key %q missing from hot cache", key) + } + if string(val.data) != "got:"+testKey { t.Error("Did not promote from candidacy") } }, diff --git a/go.mod b/go.mod index 12af9092..d47f95cc 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,18 @@ module github.com/vimeo/galaxycache -go 1.12 +go 1.18 require ( github.com/golang/protobuf v1.4.3 go.opencensus.io v0.22.5 - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect google.golang.org/grpc v1.35.0 ) + +require ( + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.25.0 // indirect +) diff --git a/go.sum b/go.sum index 8f329b03..1a6b1974 100644 --- a/go.sum +++ b/go.sum @@ -8,13 +8,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -27,7 +25,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -60,7 +57,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd h1:r7DufRZuZbWB7j439YfAzP8RPDa9unLkpwQKUYbIMPI= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -78,13 +74,11 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb h1:i1Ppqkc3WQXikh8bXiwHqAN5Rv3/qDCcRk0/Otx73BY= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/hotcache.go b/hotcache.go index 9521734a..69e2111b 100644 --- a/hotcache.go +++ b/hotcache.go @@ -35,11 +35,11 @@ func (g *Galaxy) maybeUpdateHotCacheStats() { } mruEleQPS := 0.0 lruEleQPS := 0.0 - mruEle := g.hotCache.lru.MostRecent() - lruEle := g.hotCache.lru.LeastRecent() + mruEle := g.hotCache.mostRecent() + lruEle := g.hotCache.leastRecent() if mruEle != nil { // lru contains at least one element - mruEleQPS = mruEle.(*valWithStat).stats.val() - lruEleQPS = lruEle.(*valWithStat).stats.val() + mruEleQPS = mruEle.stats.val() + lruEleQPS = lruEle.stats.val() } newHCS := &promoter.HCStats{ @@ -57,12 +57,12 @@ type keyStats struct { dQPS dampedQPS } -func newValWithStat(data []byte, kStats *keyStats) *valWithStat { +func newValWithStat(data []byte, kStats *keyStats) valWithStat { if kStats == nil { kStats = &keyStats{dampedQPS{period: time.Second}} } - return &valWithStat{ + return valWithStat{ data: data, stats: kStats, } @@ -136,9 +136,3 @@ func (g *Galaxy) addNewToCandidateCache(key string) *keyStats { g.candidateCache.addToCandidateCache(key, kStats) return kStats } - -func (c *cache) addToCandidateCache(key string, kStats *keyStats) { - c.mu.Lock() - defer c.mu.Unlock() - c.lru.Add(key, kStats) -} diff --git a/lru/typed_ll.go b/lru/typed_ll.go new file mode 100644 index 00000000..5ee03f69 --- /dev/null +++ b/lru/typed_ll.go @@ -0,0 +1,110 @@ +//go:build go1.18 + +/* +Copyright 2022 Vimeo Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lru + +// LinkedList using generics to reduce the number of heap objects +// Used for the LRU stack in TypedCache +type linkedList[T any] struct { + head *llElem[T] + tail *llElem[T] + size int +} + +type llElem[T any] struct { + next, prev *llElem[T] + value T +} + +func (l *llElem[T]) Next() *llElem[T] { + return l.next +} + +func (l *llElem[T]) Prev() *llElem[T] { + return l.prev +} + +func (l *linkedList[T]) PushFront(val T) *llElem[T] { + elem := llElem[T]{ + next: l.head, + prev: nil, // first element + value: val, + } + if l.head != nil { + l.head.prev = &elem + } + if l.tail == nil { + l.tail = &elem + } + l.head = &elem + l.size++ + + return &elem +} + +func (l *linkedList[T]) MoveToFront(e *llElem[T]) { + if l.head == e { + // nothing to do + return + } + + if e.next != nil { + // update the previous pointer on the next element + e.next.prev = e.prev + } + if e.prev != nil { + e.prev.next = e.next + } + if l.head != nil { + l.head.prev = e + } + + if l.tail == e { + l.tail = e.prev + } +} + +func (l *linkedList[T]) Remove(e *llElem[T]) { + if l.tail == e { + l.tail = e.prev + } + if l.head == e { + l.head = e.next + } + + if e.next != nil { + // update the previous pointer on the next element + e.next.prev = e.prev + } + if e.prev != nil { + e.prev.next = e.next + } + l.size-- +} + +func (l *linkedList[T]) Len() int { + return l.size +} + +func (l *linkedList[T]) Front() *llElem[T] { + return l.head +} + +func (l *linkedList[T]) Back() *llElem[T] { + return l.tail +} diff --git a/lru/typed_lru.go b/lru/typed_lru.go new file mode 100644 index 00000000..89696301 --- /dev/null +++ b/lru/typed_lru.go @@ -0,0 +1,141 @@ +//go:build go1.18 + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package lru implements an LRU cache. +package lru // import "github.com/vimeo/galaxycache/lru" + +// TypedCache is an LRU cache. It is not safe for concurrent access. +type TypedCache[K comparable, V any] struct { + // MaxEntries is the maximum number of cache entries before + // an item is evicted. Zero means no limit. + MaxEntries int + + // OnEvicted optionally specificies a callback function to be + // executed when an typedEntry is purged from the cache. + OnEvicted func(key K, value V) + + ll linkedList[typedEntry[K, V]] + cache map[K]*llElem[typedEntry[K, V]] +} + +type typedEntry[K comparable, V any] struct { + key K + value V +} + +// TypedNew creates a new Cache (with types). +// If maxEntries is zero, the cache has no limit and it's assumed +// that eviction is done by the caller. +func TypedNew[K comparable, V any](maxEntries int) *TypedCache[K, V] { + return &TypedCache[K, V]{ + MaxEntries: maxEntries, + cache: make(map[K]*llElem[typedEntry[K, V]]), + } +} + +// Add adds a value to the cache. +func (c *TypedCache[K, V]) Add(key K, value V) { + if c.cache == nil { + c.cache = make(map[K]*llElem[typedEntry[K, V]]) + } + if ele, hit := c.cache[key]; hit { + c.ll.MoveToFront(ele) + ele.value.value = value + return + } + ele := c.ll.PushFront(typedEntry[K, V]{key, value}) + c.cache[key] = ele + if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries { + c.RemoveOldest() + } +} + +// Get looks up a key's value from the cache. +func (c *TypedCache[K, V]) Get(key K) (value V, ok bool) { + if c.cache == nil { + return + } + if ele, hit := c.cache[key]; hit { + c.ll.MoveToFront(ele) + return ele.value.value, true + } + return +} + +// MostRecent returns the most recently used element +func (c *TypedCache[K, V]) MostRecent() *V { + if c.Len() == 0 { + return nil + } + return &c.ll.Front().value.value +} + +// LeastRecent returns the least recently used element +func (c *TypedCache[K, V]) LeastRecent() *V { + if c.Len() == 0 { + return nil + } + return &c.ll.Back().value.value +} + +// Remove removes the provided key from the cache. +func (c *TypedCache[K, V]) Remove(key K) { + if c.cache == nil { + return + } + if ele, hit := c.cache[key]; hit { + c.removeElement(ele) + } +} + +// RemoveOldest removes the oldest item from the cache. +func (c *TypedCache[K, V]) RemoveOldest() { + if c.cache == nil { + return + } + ele := c.ll.Back() + if ele != nil { + c.removeElement(ele) + } +} + +func (c *TypedCache[K, V]) removeElement(e *llElem[typedEntry[K, V]]) { + c.ll.Remove(e) + kv := e.value + delete(c.cache, kv.key) + if c.OnEvicted != nil { + c.OnEvicted(kv.key, kv.value) + } +} + +// Len returns the number of items in the cache. +func (c *TypedCache[K, V]) Len() int { + return c.ll.Len() +} + +// Clear purges all stored items from the cache. +func (c *TypedCache[K, V]) Clear() { + if c.OnEvicted != nil { + for _, e := range c.cache { + kv := e.value + c.OnEvicted(kv.key, kv.value) + } + } + c.ll = linkedList[typedEntry[K, V]]{} + c.cache = nil +} diff --git a/lru/typed_lru_test.go b/lru/typed_lru_test.go new file mode 100644 index 00000000..f92ba45a --- /dev/null +++ b/lru/typed_lru_test.go @@ -0,0 +1,85 @@ +//go:build go1.18 + +/* +Copyright 2013 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lru + +import ( + "fmt" + "testing" +) + +func TestTypedGet(t *testing.T) { + getTests := []struct { + name string + keyToAdd string + keyToGet string + expectedOk bool + }{ + {"string_hit", "myKey", "myKey", true}, + {"string_miss", "myKey", "nonsense", false}, + } + + for _, tt := range getTests { + lru := TypedNew[string, int](0) + lru.Add(tt.keyToAdd, 1234) + val, ok := lru.Get(tt.keyToGet) + if ok != tt.expectedOk { + t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok) + } else if ok && val != 1234 { + t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val) + } + } +} + +func TestTypedRemove(t *testing.T) { + lru := TypedNew[string, int](0) + lru.Add("myKey", 1234) + if val, ok := lru.Get("myKey"); !ok { + t.Fatal("TestRemove returned no match") + } else if val != 1234 { + t.Fatalf("TestRemove failed. Expected %d, got %v", 1234, val) + } + + lru.Remove("myKey") + if _, ok := lru.Get("myKey"); ok { + t.Fatal("TestRemove returned a removed entry") + } +} + +func TestTypedEvict(t *testing.T) { + evictedKeys := make([]Key, 0) + onEvictedFun := func(key string, value int) { + evictedKeys = append(evictedKeys, key) + } + + lru := TypedNew[string, int](20) + lru.OnEvicted = onEvictedFun + for i := 0; i < 22; i++ { + lru.Add(fmt.Sprintf("myKey%d", i), 1234) + } + + if len(evictedKeys) != 2 { + t.Fatalf("got %d evicted keys; want 2", len(evictedKeys)) + } + if evictedKeys[0] != Key("myKey0") { + t.Fatalf("got %v in first evicted key; want %s", evictedKeys[0], "myKey0") + } + if evictedKeys[1] != Key("myKey1") { + t.Fatalf("got %v in second evicted key; want %s", evictedKeys[1], "myKey1") + } +} diff --git a/typed_caches.go b/typed_caches.go new file mode 100644 index 00000000..9b1edc2f --- /dev/null +++ b/typed_caches.go @@ -0,0 +1,105 @@ +//go:build go1.18 + +/* +Copyright 2022 Vimeo Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package galaxycache + +import ( + "sync" + + "github.com/vimeo/galaxycache/lru" +) + +type candidateCache struct { + mu sync.Mutex + lru *lru.TypedCache[string, *keyStats] +} + +func newCandidateCache(maxCandidates int) candidateCache { + return candidateCache{ + lru: lru.TypedNew[string, *keyStats](maxCandidates), + } +} + +func (c *candidateCache) addToCandidateCache(key string, kStats *keyStats) { + c.mu.Lock() + defer c.mu.Unlock() + c.lru.Add(key, kStats) +} + +func (c *candidateCache) get(key string) (*keyStats, bool) { + c.mu.Lock() + defer c.mu.Unlock() + return c.lru.Get(key) +} + +// cache is a wrapper around an *lru.Cache that adds synchronization +// and counts the size of all keys and values. Candidate cache only +// utilizes the lru.Cache and mutex, not the included stats. +type cache struct { + mu sync.Mutex + lru *lru.TypedCache[string, valWithStat] + nbytes int64 // of all keys and values + nhit, nget int64 + nevict int64 // number of evictions + ctype CacheType +} + +func newCache(kind CacheType) cache { + return cache{ + lru: lru.TypedNew[string, valWithStat](0), + ctype: kind, + } +} + +func (c *cache) setLRUOnEvicted(f func(key string, kStats *keyStats)) { + c.lru.OnEvicted = func(key string, value valWithStat) { + val := value + c.nbytes -= int64(len(key)) + val.size() + c.nevict++ + if f != nil { + f(key, val.stats) + } + } +} + +func (c *cache) get(key string) (valWithStat, bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.nget++ + if c.lru == nil { + return valWithStat{}, false + } + vi, ok := c.lru.Get(key) + if !ok { + return valWithStat{}, false + } + c.nhit++ + return vi, true +} + +func (c *cache) mostRecent() *valWithStat { + c.mu.Lock() + defer c.mu.Unlock() + return c.lru.MostRecent() +} + +func (c *cache) leastRecent() *valWithStat { + c.mu.Lock() + defer c.mu.Unlock() + return c.lru.LeastRecent() +} diff --git a/untyped_caches.go b/untyped_caches.go new file mode 100644 index 00000000..430bb59a --- /dev/null +++ b/untyped_caches.go @@ -0,0 +1,119 @@ +//go:build !go1.18 + +/* +Copyright 2022 Vimeo Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package galaxycache + +import ( + "sync" + + "github.com/vimeo/galaxycache/lru" +) + +type candidateCache struct { + mu sync.Mutex + lru *lru.Cache +} + +func newCandidateCache(maxCandidates int) candidateCache { + return candidateCache{ + lru: lru.New(maxCandidates), + } +} + +func (c *candidateCache) addToCandidateCache(key string, kStats *keyStats) { + c.mu.Lock() + defer c.mu.Unlock() + c.lru.Add(key, kStats) +} + +func (c *candidateCache) get(key string) (*keyStats, bool) { + c.mu.Lock() + defer c.mu.Unlock() + val, ok := c.lru.Get(key) + if !ok { + return nil, false + } + return val.(*keyStats), true +} + +// cache is a wrapper around an *lru.Cache that adds synchronization +// and counts the size of all keys and values. Candidate cache only +// utilizes the lru.Cache and mutex, not the included stats. +type cache struct { + mu sync.Mutex + lru *lru.Cache + nbytes int64 // of all keys and values + nhit, nget int64 + nevict int64 // number of evictions + ctype CacheType +} + +func newCache(kind CacheType) cache { + return cache{ + lru: lru.New(0), + ctype: kind, + } +} + +func (c *cache) setLRUOnEvicted(f func(key string, kStats *keyStats)) { + c.lru.OnEvicted = func(key lru.Key, value interface{}) { + val := value.(valWithStat) + c.nbytes -= int64(len(key.(string))) + val.size() + c.nevict++ + if f != nil { + f(key.(string), val.stats) + } + } +} + +func (c *cache) get(key string) (valWithStat, bool) { + c.mu.Lock() + defer c.mu.Unlock() + c.nget++ + if c.lru == nil { + return valWithStat{}, false + } + vi, ok := c.lru.Get(key) + if !ok { + return valWithStat{}, false + } + c.nhit++ + return vi.(valWithStat), true +} + +func (c *cache) mostRecent() *valWithStat { + c.mu.Lock() + defer c.mu.Unlock() + v := c.lru.MostRecent() + val, ok := v.(*valWithStat) + if !ok { + return nil + } + return val +} + +func (c *cache) leastRecent() *valWithStat { + c.mu.Lock() + defer c.mu.Unlock() + v := c.lru.LeastRecent() + val, ok := v.(*valWithStat) + if !ok { + return nil + } + return val +}