Skip to content

Commit b0a984d

Browse files
committed
util/lru: add a package for a typed LRU cache
Updates tailscale/corp#7355 Signed-off-by: Brad Fitzpatrick <[email protected]>
1 parent 626f650 commit b0a984d

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

util/lru/lru.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
// Package lru contains a typed Least-Recently-Used cache.
5+
package lru
6+
7+
import (
8+
"container/list"
9+
)
10+
11+
// Cache is container type keyed by K, storing V, optionally evicting the least
12+
// recently used items if a maximum size is exceeded.
13+
//
14+
// The zero value is valid to use.
15+
//
16+
// It is not safe for concurrent access.
17+
//
18+
// The current implementation is just the traditional LRU linked list; a future
19+
// implementation may be more advanced to avoid pathological cases.
20+
type Cache[K comparable, V any] struct {
21+
// MaxEntries is the maximum number of cache entries before
22+
// an item is evicted. Zero means no limit.
23+
MaxEntries int
24+
25+
ll *list.List
26+
m map[K]*list.Element // of *entry[K,V]
27+
}
28+
29+
// entry is the element type for the container/list.Element.
30+
type entry[K comparable, V any] struct {
31+
key K
32+
value V
33+
}
34+
35+
// Set adds or replaces a value to the cache, set or updating its associated
36+
// value.
37+
//
38+
// If MaxEntries is non-zero and the length of the cache is greater
39+
// after any addition, the least recently used value is evicted.
40+
func (c *Cache[K, V]) Set(key K, value V) {
41+
if c.m == nil {
42+
c.m = make(map[K]*list.Element)
43+
c.ll = list.New()
44+
}
45+
if ee, ok := c.m[key]; ok {
46+
c.ll.MoveToFront(ee)
47+
ee.Value.(*entry[K, V]).value = value
48+
return
49+
}
50+
ele := c.ll.PushFront(&entry[K, V]{key, value})
51+
c.m[key] = ele
52+
if c.MaxEntries != 0 && c.Len() > c.MaxEntries {
53+
c.DeleteOldest()
54+
}
55+
}
56+
57+
// Get looks up a key's value from the cache, returning either
58+
// the value or the zero value if it not present.
59+
//
60+
// If found, key is moved to the front of the LRU.
61+
func (c *Cache[K, V]) Get(key K) V {
62+
v, _ := c.GetOk(key)
63+
return v
64+
}
65+
66+
// Contains reports whether c contains key.
67+
//
68+
// If found, key is moved to the front of the LRU.
69+
func (c *Cache[K, V]) Contains(key K) bool {
70+
_, ok := c.GetOk(key)
71+
return ok
72+
}
73+
74+
// GetOk looks up a key's value from the cache, also reporting
75+
// whether it was present.
76+
//
77+
// If found, key is moved to the front of the LRU.
78+
func (c *Cache[K, V]) GetOk(key K) (value V, ok bool) {
79+
if ele, hit := c.m[key]; hit {
80+
c.ll.MoveToFront(ele)
81+
return ele.Value.(*entry[K, V]).value, true
82+
}
83+
var zero V
84+
return zero, false
85+
}
86+
87+
// Delete removes the provided key from the cache if it was present.
88+
func (c *Cache[K, V]) Delete(key K) {
89+
if e, ok := c.m[key]; ok {
90+
c.deleteElement(e)
91+
}
92+
}
93+
94+
// DeleteOldest removes the item from the cache that was least recently
95+
// accessed. It is a no-op if the cache is empty.
96+
func (c *Cache[K, V]) DeleteOldest() {
97+
if c.ll != nil {
98+
if e := c.ll.Back(); e != nil {
99+
c.deleteElement(e)
100+
}
101+
}
102+
}
103+
104+
func (c *Cache[K, V]) deleteElement(e *list.Element) {
105+
c.ll.Remove(e)
106+
delete(c.m, e.Value.(*entry[K, V]).key)
107+
}
108+
109+
// Len returns the number of items in the cache.
110+
func (c *Cache[K, V]) Len() int { return len(c.m) }

util/lru/lru_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package lru
5+
6+
import "testing"
7+
8+
func TestLRU(t *testing.T) {
9+
var c Cache[int, string]
10+
c.Set(1, "one")
11+
c.Set(2, "two")
12+
if g, w := c.Get(1), "one"; g != w {
13+
t.Errorf("got %q; want %q", g, w)
14+
}
15+
if g, w := c.Get(2), "two"; g != w {
16+
t.Errorf("got %q; want %q", g, w)
17+
}
18+
c.DeleteOldest()
19+
if g, w := c.Get(1), ""; g != w {
20+
t.Errorf("got %q; want %q", g, w)
21+
}
22+
if g, w := c.Len(), 1; g != w {
23+
t.Errorf("Len = %d; want %d", g, w)
24+
}
25+
c.MaxEntries = 2
26+
c.Set(1, "one")
27+
c.Set(2, "two")
28+
c.Set(3, "three")
29+
if c.Contains(1) {
30+
t.Errorf("contains 1; should not")
31+
}
32+
if !c.Contains(2) {
33+
t.Errorf("doesn't contain 2; should")
34+
}
35+
if !c.Contains(3) {
36+
t.Errorf("doesn't contain 3; should")
37+
}
38+
c.Delete(3)
39+
if c.Contains(3) {
40+
t.Errorf("contains 3; should not")
41+
}
42+
}

0 commit comments

Comments
 (0)