mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-12 19:24:40 +00:00
47b3476eb7
Updates tailscale/corp#20109 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: I751a669251a70f0134dd1540c19b274a97608a93
217 lines
5.6 KiB
Go
217 lines
5.6 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package lru contains a typed Least-Recently-Used cache.
|
|
package lru
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"io"
|
|
)
|
|
|
|
// Cache is container type keyed by K, storing V, optionally evicting the least
|
|
// recently used items if a maximum size is exceeded.
|
|
//
|
|
// The zero value is valid to use.
|
|
//
|
|
// It is not safe for concurrent access.
|
|
//
|
|
// The current implementation is just the traditional LRU linked list; a future
|
|
// implementation may be more advanced to avoid pathological cases.
|
|
type Cache[K comparable, V any] struct {
|
|
// MaxEntries is the maximum number of cache entries before
|
|
// an item is evicted. Zero means no limit.
|
|
MaxEntries int
|
|
|
|
// head is a ring of LRU values. head points to the most recently
|
|
// used element, head.prev is the least recently used.
|
|
//
|
|
// An LRU is technically a simple list rather than a ring, but
|
|
// implementing it as a ring makes the list manipulation
|
|
// operations more regular, because the first/last positions in
|
|
// the list stop being special.
|
|
//
|
|
// head is nil when the LRU is empty.
|
|
head *entry[K, V]
|
|
// lookup is a map of all the LRU entries contained in
|
|
// head. lookup and head always contain exactly the same elements;
|
|
// lookup is just there to allow O(1) lookups of keys.
|
|
lookup map[K]*entry[K, V]
|
|
}
|
|
|
|
// entry is an entry of Cache.
|
|
type entry[K comparable, V any] struct {
|
|
prev, next *entry[K, V]
|
|
key K
|
|
value V
|
|
}
|
|
|
|
// Set adds or replaces a value to the cache, set or updating its associated
|
|
// value.
|
|
//
|
|
// If MaxEntries is non-zero and the length of the cache is greater
|
|
// after any addition, the least recently used value is evicted.
|
|
func (c *Cache[K, V]) Set(key K, value V) {
|
|
if c.lookup == nil {
|
|
c.lookup = make(map[K]*entry[K, V])
|
|
}
|
|
if ent, ok := c.lookup[key]; ok {
|
|
c.moveToFront(ent)
|
|
ent.value = value
|
|
return
|
|
}
|
|
ent := c.newAtFront(key, value)
|
|
c.lookup[key] = ent
|
|
if c.MaxEntries != 0 && c.Len() > c.MaxEntries {
|
|
c.deleteOldest()
|
|
}
|
|
}
|
|
|
|
// Clear removes all items from the cache.
|
|
func (c *Cache[K, V]) Clear() {
|
|
c.head = nil
|
|
c.lookup = nil
|
|
}
|
|
|
|
// Get looks up a key's value from the cache, returning either
|
|
// the value or the zero value if it not present.
|
|
//
|
|
// If found, key is moved to the front of the LRU.
|
|
func (c *Cache[K, V]) Get(key K) V {
|
|
v, _ := c.GetOk(key)
|
|
return v
|
|
}
|
|
|
|
// Contains reports whether c contains key.
|
|
//
|
|
// If found, key is moved to the front of the LRU.
|
|
func (c *Cache[K, V]) Contains(key K) bool {
|
|
_, ok := c.GetOk(key)
|
|
return ok
|
|
}
|
|
|
|
// GetOk looks up a key's value from the cache, also reporting whether
|
|
// it was present.
|
|
//
|
|
// If found, key is moved to the front of the LRU.
|
|
func (c *Cache[K, V]) GetOk(key K) (value V, ok bool) {
|
|
if ent, hit := c.lookup[key]; hit {
|
|
c.moveToFront(ent)
|
|
return ent.value, true
|
|
}
|
|
var zero V
|
|
return zero, false
|
|
}
|
|
|
|
// PeekOk looks up the key's value from the cache, also reporting
|
|
// whether it was present.
|
|
//
|
|
// Unlike GetOk, PeekOk does not move key to the front of the
|
|
// LRU. This should mostly be used for non-intrusive debug inspection
|
|
// of the cache.
|
|
func (c *Cache[K, V]) PeekOk(key K) (value V, ok bool) {
|
|
if ent, hit := c.lookup[key]; hit {
|
|
return ent.value, true
|
|
}
|
|
var zero V
|
|
return zero, false
|
|
}
|
|
|
|
// Delete removes the provided key from the cache if it was present.
|
|
func (c *Cache[K, V]) Delete(key K) {
|
|
if ent, ok := c.lookup[key]; ok {
|
|
c.deleteElement(ent)
|
|
}
|
|
}
|
|
|
|
// DeleteOldest removes the item from the cache that was least
|
|
// recently accessed. It is a no-op if the cache is empty.
|
|
func (c *Cache[K, V]) DeleteOldest() {
|
|
if c.head != nil {
|
|
c.deleteOldest()
|
|
}
|
|
}
|
|
|
|
// Len returns the number of items in the cache.
|
|
func (c *Cache[K, V]) Len() int { return len(c.lookup) }
|
|
|
|
// newAtFront creates a new LRU entry using key and value, and inserts
|
|
// it at the front of c.head.
|
|
func (c *Cache[K, V]) newAtFront(key K, value V) *entry[K, V] {
|
|
ret := &entry[K, V]{key: key, value: value}
|
|
if c.head == nil {
|
|
ret.prev = ret
|
|
ret.next = ret
|
|
} else {
|
|
ret.next = c.head
|
|
ret.prev = c.head.prev
|
|
c.head.prev.next = ret
|
|
c.head.prev = ret
|
|
}
|
|
c.head = ret
|
|
return ret
|
|
}
|
|
|
|
// moveToFront moves ent, which must be an existing element of the
|
|
// cache, to the front of c.head.
|
|
func (c *Cache[K, V]) moveToFront(ent *entry[K, V]) {
|
|
if c.head == ent {
|
|
return
|
|
}
|
|
ent.prev.next = ent.next
|
|
ent.next.prev = ent.prev
|
|
ent.prev = c.head.prev
|
|
ent.next = c.head
|
|
c.head.prev.next = ent
|
|
c.head.prev = ent
|
|
c.head = ent
|
|
}
|
|
|
|
// deleteOldest removes the oldest entry in the cache. It panics if
|
|
// there are no entries in the cache.
|
|
func (c *Cache[K, V]) deleteOldest() { c.deleteElement(c.head.prev) }
|
|
|
|
// deleteElement removes ent from the cache. ent must be an existing
|
|
// current element of the cache.
|
|
func (c *Cache[K, V]) deleteElement(ent *entry[K, V]) {
|
|
if ent.next == ent {
|
|
c.head = nil
|
|
} else {
|
|
ent.next.prev = ent.prev
|
|
ent.prev.next = ent.next
|
|
if c.head == ent {
|
|
c.head = ent.next
|
|
}
|
|
}
|
|
delete(c.lookup, ent.key)
|
|
}
|
|
|
|
// ForEach calls fn for each entry in the cache, from most recently
|
|
// used to least recently used.
|
|
func (c *Cache[K, V]) ForEach(fn func(K, V)) {
|
|
if c.head == nil {
|
|
return
|
|
}
|
|
cur := c.head
|
|
for {
|
|
fn(cur.key, cur.value)
|
|
cur = cur.next
|
|
if cur == c.head {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// DumpHTML writes the state of the cache to the given writer,
|
|
// formatted as an HTML table.
|
|
func (c *Cache[K, V]) DumpHTML(w io.Writer) {
|
|
io.WriteString(w, "<table><tr><th>Key</th><th>Value</th></tr>")
|
|
c.ForEach(func(k K, v V) {
|
|
kStr := html.EscapeString(fmt.Sprint(k))
|
|
vStr := html.EscapeString(fmt.Sprint(v))
|
|
fmt.Fprintf(w, "<tr><td>%s</td><td>%v</td></tr>", kStr, vStr)
|
|
})
|
|
io.WriteString(w, "</table>")
|
|
}
|