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") + } +}