Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: hashicorp/golang-lru
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.0.4
Choose a base ref
...
head repository: hashicorp/golang-lru
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.0.5
Choose a head ref
  • 13 commits
  • 11 files changed
  • 3 contributors

Commits on Jun 21, 2023

  1. Copy the full SHA
    9af07c6 View commit details

Commits on Jul 5, 2023

  1. Fix ARC tests

    jefferai committed Jul 5, 2023
    Copy the full SHA
    f4cd393 View commit details

Commits on Aug 1, 2023

  1. Verified

    This commit was signed with the committer’s verified signature.
    mgaffney Michael Gaffney
    Copy the full SHA
    78003ed View commit details

Commits on Aug 5, 2023

  1. add expirable LRU implementation

    Thread-safe. It could be used in place of simplelru.LRU but shouldn't
    as it has built-in locks, whereas simplelru.LRU doesn't, which allows
    more effective locking on top of it in top-level package
    cache implementations.
    paskal committed Aug 5, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    738ef57 View commit details
  2. switch expirable LRU from purgeEvery to 1/100th of TTL buckets

    That sets the memory overhead to approximately 1% mark at the cost of
    expiring the cache entries up to 1% faster than their TTL expires.
    
    Previously, all entries were scanned for expiration by purgeEvery
    interval, which created computation overhead, and after this commit, we
    delete entries we want to delete without checking extra ones.
    paskal committed Aug 5, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    cd39ba4 View commit details
  3. Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    73f395c View commit details
  4. Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    f0d41e0 View commit details

Commits on Aug 6, 2023

  1. Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    1154eab View commit details

Commits on Aug 7, 2023

  1. Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    f2e3b29 View commit details
  2. Verified

    This commit was signed with the committer’s verified signature.
    paskal Dmitry V
    Copy the full SHA
    575866d View commit details
  3. Verified

    This commit was signed with the committer’s verified signature.
    mgaffney Michael Gaffney
    Copy the full SHA
    3b3d259 View commit details

Commits on Aug 8, 2023

  1. Verified

    This commit was signed with the committer’s verified signature.
    mgaffney Michael Gaffney
    Copy the full SHA
    8d433cc View commit details
  2. test: increase sleep time in expirable tests

    The github workflows are failing.
    mgaffney committed Aug 8, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    mgaffney Michael Gaffney
    Copy the full SHA
    99d7b17 View commit details
Showing with 1,128 additions and 182 deletions.
  1. +12 −2 .github/workflows/ci.yml
  2. +19 −6 .golangci.yml
  3. +56 −11 README.md
  4. +13 −2 arc/arc_test.go
  5. +1 −1 arc/go.mod
  6. +2 −2 arc/go.sum
  7. +335 −0 expirable/expirable_lru.go
  8. +516 −0 expirable/expirable_lru_test.go
  9. +142 −0 internal/list.go
  10. +0 −128 simplelru/list.go
  11. +32 −30 simplelru/lru.go
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -21,12 +21,22 @@ jobs:
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0

- name: build and test
run: |
go test -timeout=60s -race ./...
go build -race ./...
- name: build and test ARC
working-directory: ./arc
run: |
go test -timeout=60s -race
go build -race
- name: install golangci-lint
run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.50.1
run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GITHUB_WORKSPACE v1.53.3

- name: run golangci-lint
run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions
run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions ./... ./simplelru/... ./expirable/...

- name: run golangci-lint on ARC
working-directory: ./arc
run: $GITHUB_WORKSPACE/golangci-lint run --out-format=github-actions ./...
25 changes: 19 additions & 6 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -2,12 +2,13 @@
# SPDX-License-Identifier: MPL-2.0

linters:
fast: false
disable-all: true
enable:
- megacheck
- revive
- megacheck
- govet
- unconvert
- megacheck
- gas
- gocyclo
- dupl
@@ -16,18 +17,30 @@ linters:
- unused
- typecheck
- ineffassign
- stylecheck
# - stylecheck
- exportloopref
- gocritic
- nakedret
- gosimple
- prealloc
fast: false
disable-all: true

# golangci-lint configuration file
linters-settings:
revive:
ignore-generated-header: true
severity: warning
rules:
- name: package-comments
severity: warning
disabled: true
- name: exported
severity: warning
disabled: false
arguments: ["checkPrivateReceivers", "disableStutteringCheck"]

issues:
exclude-use-default: false
exclude-rules:
- path: _test\.go
linters:
- dupl
exclude-use-default: false
67 changes: 56 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -9,10 +9,8 @@ Documentation

Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2)

Example
=======

Using the LRU is very simple:
LRU cache example
=================

```go
package main
@@ -23,12 +21,59 @@ import (
)

func main() {
l, _ := lru.New[int, any](128)
for i := 0; i < 256; i++ {
l.Add(i, nil)
}
if l.Len() != 128 {
panic(fmt.Sprintf("bad len: %v", l.Len()))
}
l, _ := lru.New[int, any](128)
for i := 0; i < 256; i++ {
l.Add(i, nil)
}
if l.Len() != 128 {
panic(fmt.Sprintf("bad len: %v", l.Len()))
}
}
```

Expirable LRU cache example
===========================

```go
package main

import (
"fmt"
"time"

"github.com/hashicorp/golang-lru/v2/expirable"
)

func main() {
// make cache with 10ms TTL and 5 max keys
cache := expirable.NewLRU[string, string](5, nil, time.Millisecond*10)


// set value under key1.
cache.Add("key1", "val1")

// get value under key1
r, ok := cache.Get("key1")

// check for OK value
if ok {
fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r)
}

// wait for cache to expire
time.Sleep(time.Millisecond * 12)

// get value under key1 after key expiration
r, ok = cache.Get("key1")
fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r)

// set value under key2, would evict old entry because it is already expired.
cache.Add("key2", "val2")

fmt.Printf("Cache len: %d\n", cache.Len())
// Output:
// value before expiration is found: true, value: "val1"
// value after expiration is found: false, value: ""
// Cache len: 1
}
```
15 changes: 13 additions & 2 deletions arc/arc_test.go
Original file line number Diff line number Diff line change
@@ -4,13 +4,24 @@
package arc

import (
"math/rand"
"crypto/rand"
"math"
"math/big"
mathrand "math/rand"
"testing"
"time"
)

func getRand(tb testing.TB) int64 {
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
tb.Fatal(err)
}
return out.Int64()
}

func init() {
rand.Seed(time.Now().Unix())
mathrand.Seed(time.Now().Unix())
}

func BenchmarkARC_Rand(b *testing.B) {
2 changes: 1 addition & 1 deletion arc/go.mod
Original file line number Diff line number Diff line change
@@ -2,4 +2,4 @@ module github.com/hashicorp/golang-lru/arc/v2

go 1.18

require github.com/hashicorp/golang-lru/v2 v2.0.3
require github.com/hashicorp/golang-lru/v2 v2.0.4
4 changes: 2 additions & 2 deletions arc/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/hashicorp/golang-lru/v2 v2.0.3 h1:kmRrRLlInXvng0SmLxmQpQkpbYAvcXm7NPDrgxJa9mE=
github.com/hashicorp/golang-lru/v2 v2.0.3/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0=
github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
335 changes: 335 additions & 0 deletions expirable/expirable_lru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
package expirable

import (
"sync"
"time"

"github.com/hashicorp/golang-lru/v2/internal"
)

// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback[K comparable, V any] func(key K, value V)

// LRU implements a thread-safe LRU with expirable entries.
type LRU[K comparable, V any] struct {
size int
evictList *internal.LruList[K, V]
items map[K]*internal.Entry[K, V]
onEvict EvictCallback[K, V]

// expirable options
mu sync.Mutex
ttl time.Duration
done chan struct{}

// buckets for expiration
buckets []bucket[K, V]
// uint8 because it's number between 0 and numBuckets
nextCleanupBucket uint8
}

// bucket is a container for holding entries to be expired
type bucket[K comparable, V any] struct {
entries map[K]*internal.Entry[K, V]
newestEntry time.Time
}

// noEvictionTTL - very long ttl to prevent eviction
const noEvictionTTL = time.Hour * 24 * 365 * 10

// because of uint8 usage for nextCleanupBucket, should not exceed 256.
// casting it as uint8 explicitly requires type conversions in multiple places
const numBuckets = 100

// NewLRU returns a new thread-safe cache with expirable entries.
//
// Size parameter set to 0 makes cache of unlimited size, e.g. turns LRU mechanism off.
//
// Providing 0 TTL turns expiring off.
//
// Delete expired entries every 1/100th of ttl value. Goroutine which deletes expired entries runs indefinitely.
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V], ttl time.Duration) *LRU[K, V] {
if size < 0 {
size = 0
}
if ttl <= 0 {
ttl = noEvictionTTL
}

res := LRU[K, V]{
ttl: ttl,
size: size,
evictList: internal.NewList[K, V](),
items: make(map[K]*internal.Entry[K, V]),
onEvict: onEvict,
done: make(chan struct{}),
}

// initialize the buckets
res.buckets = make([]bucket[K, V], numBuckets)
for i := 0; i < numBuckets; i++ {
res.buckets[i] = bucket[K, V]{entries: make(map[K]*internal.Entry[K, V])}
}

// enable deleteExpired() running in separate goroutine for cache with non-zero TTL
//
// Important: done channel is never closed, so deleteExpired() goroutine will never exit,
// it's decided to add functionality to close it in the version later than v2.
if res.ttl != noEvictionTTL {
go func(done <-chan struct{}) {
ticker := time.NewTicker(res.ttl / numBuckets)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
res.deleteExpired()
}
}
}(res.done)
}
return &res
}

// Purge clears the cache completely.
// onEvict is called for each evicted key.
func (c *LRU[K, V]) Purge() {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.items {
if c.onEvict != nil {
c.onEvict(k, v.Value)
}
delete(c.items, k)
}
for _, b := range c.buckets {
for _, ent := range b.entries {
delete(b.entries, ent.Key)
}
}
c.evictList.Init()
}

// Add adds a value to the cache. Returns true if an eviction occurred.
// Returns false if there was no eviction: the item was already in the cache,
// or the size was not exceeded.
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()

// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
c.removeFromBucket(ent) // remove the entry from its current bucket as expiresAt is renewed
ent.Value = value
ent.ExpiresAt = now.Add(c.ttl)
c.addToBucket(ent)
return false
}

// Add new item
ent := c.evictList.PushFrontExpirable(key, value, now.Add(c.ttl))
c.items[key] = ent
c.addToBucket(ent) // adds the entry to the appropriate bucket and sets entry.expireBucket

evict := c.size > 0 && c.evictList.Length() > c.size
// Verify size not exceeded
if evict {
c.removeOldest()
}
return evict
}

// Get looks up a key's value from the cache.
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
// Expired item check
if time.Now().After(ent.ExpiresAt) {
return
}
c.evictList.MoveToFront(ent)
return ent.Value, true
}
return
}

// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale.
func (c *LRU[K, V]) Contains(key K) (ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
_, ok = c.items[key]
return ok
}

// Peek returns the key value (or undefined if not found) without updating
// the "recently used"-ness of the key.
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
// Expired item check
if time.Now().After(ent.ExpiresAt) {
return
}
return ent.Value, true
}
return
}

// Remove removes the provided key from the cache, returning if the
// key was contained.
func (c *LRU[K, V]) Remove(key K) bool {
c.mu.Lock()
defer c.mu.Unlock()
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
return true
}
return false
}

// RemoveOldest removes the oldest item from the cache.
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
return ent.Key, ent.Value, true
}
return
}

// GetOldest returns the oldest entry
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
if ent := c.evictList.Back(); ent != nil {
return ent.Key, ent.Value, true
}
return
}

// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *LRU[K, V]) Keys() []K {
c.mu.Lock()
defer c.mu.Unlock()
keys := make([]K, 0, len(c.items))
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
keys = append(keys, ent.Key)
}
return keys
}

// Values returns a slice of the values in the cache, from oldest to newest.
// Expired entries are filtered out.
func (c *LRU[K, V]) Values() []V {
c.mu.Lock()
defer c.mu.Unlock()
values := make([]V, len(c.items))
i := 0
now := time.Now()
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
if now.After(ent.ExpiresAt) {
continue
}
values[i] = ent.Value
i++
}
return values
}

// Len returns the number of items in the cache.
func (c *LRU[K, V]) Len() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.evictList.Length()
}

// Resize changes the cache size. Size of 0 means unlimited.
func (c *LRU[K, V]) Resize(size int) (evicted int) {
c.mu.Lock()
defer c.mu.Unlock()
if size <= 0 {
c.size = 0
return 0
}
diff := c.evictList.Length() - size
if diff < 0 {
diff = 0
}
for i := 0; i < diff; i++ {
c.removeOldest()
}
c.size = size
return diff
}

// Close destroys cleanup goroutine. To clean up the cache, run Purge() before Close().
// func (c *LRU[K, V]) Close() {
// c.mu.Lock()
// defer c.mu.Unlock()
// select {
// case <-c.done:
// return
// default:
// }
// close(c.done)
// }

// removeOldest removes the oldest item from the cache. Has to be called with lock!
func (c *LRU[K, V]) removeOldest() {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
}
}

// removeElement is used to remove a given list element from the cache. Has to be called with lock!
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
c.evictList.Remove(e)
delete(c.items, e.Key)
c.removeFromBucket(e)
if c.onEvict != nil {
c.onEvict(e.Key, e.Value)
}
}

// deleteExpired deletes expired records from the oldest bucket, waiting for the newest entry
// in it to expire first.
func (c *LRU[K, V]) deleteExpired() {
c.mu.Lock()
bucketIdx := c.nextCleanupBucket
timeToExpire := time.Until(c.buckets[bucketIdx].newestEntry)
// wait for newest entry to expire before cleanup without holding lock
if timeToExpire > 0 {
c.mu.Unlock()
time.Sleep(timeToExpire)
c.mu.Lock()
}
for _, ent := range c.buckets[bucketIdx].entries {
c.removeElement(ent)
}
c.nextCleanupBucket = (c.nextCleanupBucket + 1) % numBuckets
c.mu.Unlock()
}

// addToBucket adds entry to expire bucket so that it will be cleaned up when the time comes. Has to be called with lock!
func (c *LRU[K, V]) addToBucket(e *internal.Entry[K, V]) {
bucketID := (numBuckets + c.nextCleanupBucket - 1) % numBuckets
e.ExpireBucket = bucketID
c.buckets[bucketID].entries[e.Key] = e
if c.buckets[bucketID].newestEntry.Before(e.ExpiresAt) {
c.buckets[bucketID].newestEntry = e.ExpiresAt
}
}

// removeFromBucket removes the entry from its corresponding bucket. Has to be called with lock!
func (c *LRU[K, V]) removeFromBucket(e *internal.Entry[K, V]) {
delete(c.buckets[e.ExpireBucket].entries, e.Key)
}
516 changes: 516 additions & 0 deletions expirable/expirable_lru_test.go

Large diffs are not rendered by default.

142 changes: 142 additions & 0 deletions internal/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2009 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_list file.

package internal

import "time"

// Entry is an LRU Entry
type Entry[K comparable, V any] struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Entry[K, V]

// The list to which this element belongs.
list *LruList[K, V]

// The LRU Key of this element.
Key K

// The Value stored with this element.
Value V

// The time this element would be cleaned up, optional
ExpiresAt time.Time

// The expiry bucket item was put in, optional
ExpireBucket uint8
}

// PrevEntry returns the previous list element or nil.
func (e *Entry[K, V]) PrevEntry() *Entry[K, V] {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}

// LruList represents a doubly linked list.
// The zero Value for LruList is an empty list ready to use.
type LruList[K comparable, V any] struct {
root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used
len int // current list Length excluding (this) sentinel element
}

// Init initializes or clears list l.
func (l *LruList[K, V]) Init() *LruList[K, V] {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}

// NewList returns an initialized list.
func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() }

// Length returns the number of elements of list l.
// The complexity is O(1).
func (l *LruList[K, V]) Length() int { return l.len }

// Back returns the last element of list l or nil if the list is empty.
func (l *LruList[K, V]) Back() *Entry[K, V] {
if l.len == 0 {
return nil
}
return l.root.prev
}

// lazyInit lazily initializes a zero List Value.
func (l *LruList[K, V]) lazyInit() {
if l.root.next == nil {
l.Init()
}
}

// insert inserts e after at, increments l.len, and returns e.
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}

// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at).
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
}

// Remove removes e from its list, decrements l.len
func (l *LruList[K, V]) Remove(e *Entry[K, V]) V {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--

return e.Value
}

// move moves e to next to at.
func (l *LruList[K, V]) move(e, at *Entry[K, V]) {
if e == at {
return
}
e.prev.next = e.next
e.next.prev = e.prev

e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
}

// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
l.lazyInit()
return l.insertValue(k, v, time.Time{}, &l.root)
}

// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e.
func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] {
l.lazyInit()
return l.insertValue(k, v, expiresAt, &l.root)
}

// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {
if e.list != l || l.root.next == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root)
}
128 changes: 0 additions & 128 deletions simplelru/list.go

This file was deleted.

62 changes: 32 additions & 30 deletions simplelru/lru.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ package simplelru

import (
"errors"

"github.com/hashicorp/golang-lru/v2/internal"
)

// EvictCallback is used to get a callback when a cache entry is evicted
@@ -13,8 +15,8 @@ type EvictCallback[K comparable, V any] func(key K, value V)
// LRU implements a non-thread safe fixed size LRU cache
type LRU[K comparable, V any] struct {
size int
evictList *lruList[K, V]
items map[K]*entry[K, V]
evictList *internal.LruList[K, V]
items map[K]*internal.Entry[K, V]
onEvict EvictCallback[K, V]
}

@@ -26,8 +28,8 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K,

c := &LRU[K, V]{
size: size,
evictList: newList[K, V](),
items: make(map[K]*entry[K, V]),
evictList: internal.NewList[K, V](),
items: make(map[K]*internal.Entry[K, V]),
onEvict: onEvict,
}
return c, nil
@@ -37,30 +39,30 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K,
func (c *LRU[K, V]) Purge() {
for k, v := range c.items {
if c.onEvict != nil {
c.onEvict(k, v.value)
c.onEvict(k, v.Value)
}
delete(c.items, k)
}
c.evictList.init()
c.evictList.Init()
}

// Add adds a value to the cache. Returns true if an eviction occurred.
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.moveToFront(ent)
c.evictList.MoveToFront(ent)
if c.onEvict != nil {
c.onEvict(key, ent.value)
c.onEvict(key, ent.Value)
}
ent.value = value
ent.Value = value
return false
}

// Add new item
ent := c.evictList.pushFront(key, value)
ent := c.evictList.PushFront(key, value)
c.items[key] = ent

evict := c.evictList.length() > c.size
evict := c.evictList.Length() > c.size
// Verify size not exceeded
if evict {
c.removeOldest()
@@ -71,8 +73,8 @@ func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
// Get looks up a key's value from the cache.
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
if ent, ok := c.items[key]; ok {
c.evictList.moveToFront(ent)
return ent.value, true
c.evictList.MoveToFront(ent)
return ent.Value, true
}
return
}
@@ -87,9 +89,9 @@ func (c *LRU[K, V]) Contains(key K) (ok bool) {
// Peek returns the key value (or undefined if not found) without updating
// the "recently used"-ness of the key.
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
var ent *entry[K, V]
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
return ent.value, true
return ent.Value, true
}
return
}
@@ -106,27 +108,27 @@ func (c *LRU[K, V]) Remove(key K) (present bool) {

// RemoveOldest removes the oldest item from the cache.
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
if ent := c.evictList.back(); ent != nil {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
return ent.key, ent.value, true
return ent.Key, ent.Value, true
}
return
}

// GetOldest returns the oldest entry
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
if ent := c.evictList.back(); ent != nil {
return ent.key, ent.value, true
if ent := c.evictList.Back(); ent != nil {
return ent.Key, ent.Value, true
}
return
}

// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *LRU[K, V]) Keys() []K {
keys := make([]K, c.evictList.length())
keys := make([]K, c.evictList.Length())
i := 0
for ent := c.evictList.back(); ent != nil; ent = ent.prevEntry() {
keys[i] = ent.key
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
keys[i] = ent.Key
i++
}
return keys
@@ -136,16 +138,16 @@ func (c *LRU[K, V]) Keys() []K {
func (c *LRU[K, V]) Values() []V {
values := make([]V, len(c.items))
i := 0
for ent := c.evictList.back(); ent != nil; ent = ent.prevEntry() {
values[i] = ent.value
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
values[i] = ent.Value
i++
}
return values
}

// Len returns the number of items in the cache.
func (c *LRU[K, V]) Len() int {
return c.evictList.length()
return c.evictList.Length()
}

// Resize changes the cache size.
@@ -163,16 +165,16 @@ func (c *LRU[K, V]) Resize(size int) (evicted int) {

// removeOldest removes the oldest item from the cache.
func (c *LRU[K, V]) removeOldest() {
if ent := c.evictList.back(); ent != nil {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
}
}

// removeElement is used to remove a given list element from the cache
func (c *LRU[K, V]) removeElement(e *entry[K, V]) {
c.evictList.remove(e)
delete(c.items, e.key)
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
c.evictList.Remove(e)
delete(c.items, e.Key)
if c.onEvict != nil {
c.onEvict(e.key, e.value)
c.onEvict(e.Key, e.Value)
}
}