internal/lru: add a really simple LRU cache implementation
To replace github.com/hashicorp/golang-lru. The size used with the
cache in fetchdatasource is 100, so the efficiency of the cache is not
super important.
For golang/go#61399
Change-Id: I48383a4d1c00c4d153c0bad7b0a1a44c026b2314
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/524458
TryBot-Result: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Run-TryBot: Michael Matloob <matloob@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
kokoro-CI: kokoro <noreply+kokoro@google.com>
diff --git a/go.mod b/go.mod
index 31dcecb..0855b35 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,6 @@
github.com/google/go-replayers/httpreplay v1.0.0
github.com/google/licensecheck v0.3.1
github.com/google/safehtml v0.0.3-0.20211026203422-d6f0e11a5516
- github.com/hashicorp/golang-lru v0.5.1
github.com/jackc/pgconn v1.10.1
github.com/jackc/pgx/v4 v4.14.1
github.com/jba/templatecheck v0.6.0
diff --git a/go.sum b/go.sum
index 8804a21..9167f54 100644
--- a/go.sum
+++ b/go.sum
@@ -617,7 +617,6 @@
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
diff --git a/internal/fetchdatasource/fetchdatasource.go b/internal/fetchdatasource/fetchdatasource.go
index 8aef0a6..88ab138 100644
--- a/internal/fetchdatasource/fetchdatasource.go
+++ b/internal/fetchdatasource/fetchdatasource.go
@@ -16,12 +16,12 @@
"strings"
"time"
- lru "github.com/hashicorp/golang-lru"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/fetch"
"golang.org/x/pkgsite/internal/log"
+ "golang.org/x/pkgsite/internal/lru"
"golang.org/x/pkgsite/internal/proxy"
"golang.org/x/pkgsite/internal/version"
)
@@ -30,7 +30,7 @@
// fetch.ModuleGetters to fetch modules and caching the results.
type FetchDataSource struct {
opts Options
- cache *lru.Cache
+ cache *lru.Cache[internal.Modver, cacheEntry]
}
// Options are parameters for creating a new FetchDataSource.
@@ -45,11 +45,8 @@
// New creates a new FetchDataSource from the options.
func (o Options) New() *FetchDataSource {
- cache, err := lru.New(maxCachedModules)
- if err != nil {
- // Can only happen if size is bad, and we control it.
- panic(err)
- }
+ cache := lru.New[internal.Modver, cacheEntry](maxCachedModules)
+
opts := o
// Copy getters slice so caller doesn't modify us.
opts.Getters = make([]fetch.ModuleGetter, len(opts.Getters))
@@ -75,7 +72,6 @@
// directory-based or GOPATH-mode module.
for _, v := range []string{version, fetch.LocalVersion} {
if e, ok := ds.cache.Get(internal.Modver{Path: path, Version: v}); ok {
- e := e.(cacheEntry)
return e.g, e.module, e.err
}
}
@@ -84,7 +80,7 @@
// cachePut puts information into the cache.
func (ds *FetchDataSource) cachePut(g fetch.ModuleGetter, path, version string, m *internal.Module, err error) {
- ds.cache.Add(internal.Modver{Path: path, Version: version}, cacheEntry{g, m, err})
+ ds.cache.Put(internal.Modver{Path: path, Version: version}, cacheEntry{g, m, err})
}
// getModule gets the module at the given path and version. It first checks the
diff --git a/internal/lru/lru.go b/internal/lru/lru.go
new file mode 100644
index 0000000..98630b8
--- /dev/null
+++ b/internal/lru/lru.go
@@ -0,0 +1,82 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package lru provides an LRU cache.
+package lru
+
+import (
+ "fmt"
+ "math"
+ "sync"
+)
+
+// Cache is an LRU cache.
+type Cache[K comparable, V any] struct {
+ mu sync.Mutex
+ size int
+ entries map[K]*entry[V]
+ tick uint // increases every time an entry is used
+}
+
+type entry[V any] struct {
+ lastUsed uint // the tick of the last operation
+ v V
+}
+
+// New returns a new Cache. Size must be positive or it will panic.
+func New[K comparable, V any](size int) *Cache[K, V] {
+ if size < 1 {
+ panic(fmt.Errorf("lru.New called with non-positive size %v", size))
+ }
+ return &Cache[K, V]{
+ size: size,
+ entries: map[K]*entry[V]{},
+ }
+}
+
+// Get gets the entry for k in the Cache.
+func (c *Cache[K, V]) Get(k K) (V, bool) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ entry, ok := c.entries[k]
+ if !ok {
+ var zero V
+ return zero, false
+ }
+ c.tick++
+ entry.lastUsed = c.tick
+ return entry.v, true
+}
+
+// Put puts in an entry for k, v in Cache, evicting
+// the least recently used entry if necessary.
+func (c *Cache[K, V]) Put(k K, v V) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ _, ok := c.entries[k]
+ if !ok {
+ // k not already in c.entries. We need to evict the least recently
+ // used entry.
+ if c.size < 1 {
+ panic("attempting to insert into an uninitialized cache.")
+ }
+ if len(c.entries) > c.size {
+ panic(fmt.Errorf("size of cache, %d, has grown beyond size limit %d", len(c.entries), c.size))
+ }
+ if len(c.entries) == c.size {
+ // evict least recently used element.
+ var oldestTick uint = math.MaxUint
+ var oldestKey K
+ for k, e := range c.entries {
+ if e.lastUsed <= oldestTick {
+ oldestTick = e.lastUsed
+ oldestKey = k
+ }
+ }
+ delete(c.entries, oldestKey)
+ }
+ }
+ c.tick++
+ c.entries[k] = &entry[V]{lastUsed: c.tick, v: v}
+}
diff --git a/internal/lru/lru_test.go b/internal/lru/lru_test.go
new file mode 100644
index 0000000..346d2f1
--- /dev/null
+++ b/internal/lru/lru_test.go
@@ -0,0 +1,96 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lru
+
+import "testing"
+
+func TestSizeOne(t *testing.T) {
+ c := New[int, int](1)
+ c.Put(1, 1)
+ got, gotOK := c.Get(1)
+ if got != 1 && gotOK != true {
+ t.Errorf("c.Get(1): got %v, %v, want %v, %v", got, gotOK, 1, true)
+ }
+ c.Put(2, 2)
+ got, gotOK = c.Get(1)
+ if got != 0 || gotOK != false {
+ t.Errorf("c.Get(1): got %v, %v, want %v, %v", got, gotOK, 0, false)
+ }
+ got, gotOK = c.Get(2)
+ if got != 2 && gotOK != true {
+ t.Errorf("c.Get(2): got %v, %v, want %v, %v", got, gotOK, 2, true)
+ }
+}
+
+func TestSizeFive(t *testing.T) {
+ c := New[int, int](5)
+ c.Put(1, 1)
+ c.Put(2, 2)
+ c.Put(3, 3)
+ c.Put(4, 4)
+ c.Put(5, 5)
+
+ getHasKey := func(k int, has bool) {
+ t.Helper()
+
+ got, ok := c.Get(k)
+ if has == false {
+ if ok == true || got != 0 {
+ t.Errorf("c.Get(%v): got %v, %v, want %v, %v", k, got, ok, 0, false)
+ }
+ } else if got != k || ok != true {
+ t.Errorf("c.Get(%v): got %v, %v, want %v, %v", k, got, ok, k, true)
+ }
+ }
+
+ getHasKey(3, true)
+ getHasKey(2, true)
+ getHasKey(1, true)
+ getHasKey(5, true)
+ getHasKey(4, true)
+ c.Put(6, 6) // 3 gets evicted
+
+ getHasKey(3, false)
+ getHasKey(1, true)
+ getHasKey(2, true)
+ getHasKey(4, true)
+ getHasKey(5, true)
+ getHasKey(6, true)
+ c.Put(7, 7)
+ c.Put(8, 8) // 1 and 2 get evicted
+
+ getHasKey(1, false)
+ getHasKey(2, false)
+ getHasKey(8, true)
+ getHasKey(7, true)
+ getHasKey(4, true)
+ getHasKey(5, true)
+ getHasKey(6, true)
+ c.Put(9, 9) // 8 gets evicted
+ c.Put(10, 10) // 7 gets evicted
+ c.Put(11, 11) // 4 gets evicted
+ c.Put(12, 12) // 5 gets evicted
+ c.Put(13, 13) // 6 gets evicted
+ c.Put(14, 14) // 9 gets evicted
+
+ getHasKey(4, false)
+ getHasKey(5, false)
+ getHasKey(6, false)
+ getHasKey(7, false)
+ getHasKey(8, false)
+ getHasKey(9, false)
+ getHasKey(10, true)
+ getHasKey(11, true)
+ getHasKey(12, true)
+ getHasKey(13, true)
+ getHasKey(14, true)
+ c.Put(12, 12)
+
+ getHasKey(10, true)
+ getHasKey(11, true)
+ getHasKey(12, true)
+ getHasKey(13, true)
+ getHasKey(14, true)
+}