This implements a new module named lru which provides a concurrency safe least-recently-used cache with nearly early O(1) lookups, inserts, and deletions. The cache is limited to a maximum number of items with eviction for the oldest entry when the limit is exceeded. This will allow the various concrete implementations to be updated to make use of this shared module instead of duplicating code. There is no additional performance hit to making use of this generic version because the internal list already causes the item to be boxed anyway, so the addition of the interface for the type does not cause an additional alloc. This can be seen by comparing this generic implementation to the existing concrete nonce implementation in peer: BenchmarkCache 1 allocs/op BenchmarkLruNonceList 1 allocs/op It also includes comprehensive tests, a benchmark, full package documentation, and a basic usage example.
155 lines
4.6 KiB
Go
155 lines
4.6 KiB
Go
// Copyright (c) 2019 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package lru
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestCache ensures the LRU Cache behaves as expected including limiting,
|
|
// eviction of least-recently used entries, specific entry removal, and
|
|
// existence tests.
|
|
func TestCache(t *testing.T) {
|
|
// Create a bunch of fake nonces to use in testing the lru nonce code.
|
|
numNonces := 10
|
|
nonces := make([]uint64, 0, numNonces)
|
|
for i := 0; i < numNonces; i++ {
|
|
nonces = append(nonces, uint64(i))
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
limit int
|
|
}{
|
|
{name: "limit 0", limit: 0},
|
|
{name: "limit 1", limit: 1},
|
|
{name: "limit 5", limit: 5},
|
|
{name: "limit 7", limit: 7},
|
|
{name: "limit one less than available", limit: numNonces - 1},
|
|
{name: "limit all available", limit: numNonces},
|
|
}
|
|
|
|
testLoop:
|
|
for i, test := range tests {
|
|
// Create a new lru cache limited by the specified test limit and add
|
|
// all of the test vectors. This will cause evicition since there are
|
|
// more test items than the limits.
|
|
cache := NewCache(uint(test.limit))
|
|
for j := 0; j < numNonces; j++ {
|
|
cache.Add(nonces[j])
|
|
}
|
|
|
|
// Ensure the limited number of most recent entries in the list exist.
|
|
for j := numNonces - test.limit; j < numNonces; j++ {
|
|
if !cache.Contains(nonces[j]) {
|
|
t.Errorf("Contains #%d (%s) entry %d does not exist", i,
|
|
test.name, nonces[j])
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Ensure the entries before the limited number of most recent entries
|
|
// in the list do not exist.
|
|
for j := 0; j < numNonces-test.limit; j++ {
|
|
if cache.Contains(nonces[j]) {
|
|
t.Errorf("Contains #%d (%s) entry %d exists", i, test.name,
|
|
nonces[j])
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Access the entry that should currently be the least-recently used
|
|
// entry so it becomes the most-recently used entry, then force an
|
|
// eviction by adding an entry that doesn't exist and ensure the evicted
|
|
// entry is the new least-recently used entry.
|
|
//
|
|
// This check needs at least 2 entries.
|
|
if test.limit > 1 {
|
|
origLruIndex := numNonces - test.limit
|
|
_ = cache.Contains(nonces[origLruIndex])
|
|
|
|
cache.Add(uint64(numNonces) + 1)
|
|
|
|
// Ensure the original lru entry still exists since it was updated
|
|
// and should've have become the lru entry.
|
|
if !cache.Contains(nonces[origLruIndex]) {
|
|
t.Errorf("Contains #%d (%s) entry %d does not exist", i, test.name,
|
|
nonces[origLruIndex])
|
|
continue testLoop
|
|
}
|
|
|
|
// Ensure the entry that should've become the new lru entry was
|
|
// evicted.
|
|
newLruIndex := origLruIndex + 1
|
|
if cache.Contains(nonces[newLruIndex]) {
|
|
t.Errorf("Contains #%d (%s) entry %d exists", i, test.name,
|
|
nonces[newLruIndex])
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Add the entry that should currently be the least-recently used entry
|
|
// again so it becomes the most-recently used entry, then force an
|
|
// eviction by adding an entry that doesn't exist and ensure the evicted
|
|
// entry is the new least-recently used entry.
|
|
//
|
|
// This check needs at least 2 entries.
|
|
if test.limit > 1 {
|
|
origLruIndex := numNonces - test.limit
|
|
cache.Add(nonces[origLruIndex])
|
|
|
|
cache.Add(uint64(numNonces) + 2)
|
|
|
|
// Ensure the original lru entry still exists since it was updated
|
|
// and should've have become the lru entry.
|
|
if !cache.Contains(nonces[origLruIndex]) {
|
|
t.Errorf("Contains #%d (%s) entry %d does not exist", i, test.name,
|
|
nonces[origLruIndex])
|
|
continue testLoop
|
|
}
|
|
|
|
// Ensure the entry that should've become the new lru entry was
|
|
// evicted.
|
|
newLruIndex := origLruIndex + 1
|
|
if cache.Contains(nonces[newLruIndex]) {
|
|
t.Errorf("Contains #%d (%s) entry %d exists", i, test.name,
|
|
nonces[newLruIndex])
|
|
continue testLoop
|
|
}
|
|
}
|
|
|
|
// Delete all of the entries in the list, including those that don't
|
|
// exist in the cache, and ensure they no longer exist.
|
|
for j := 0; j < numNonces; j++ {
|
|
cache.Delete(nonces[j])
|
|
if cache.Contains(nonces[j]) {
|
|
t.Errorf("Delete #%d (%s) entry %d exists", i, test.name,
|
|
nonces[j])
|
|
continue testLoop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BenchmarkCache performs basic benchmarks on the least recently used cache
|
|
// handling.
|
|
func BenchmarkCache(b *testing.B) {
|
|
// Create a bunch of fake nonces to use in benchmarking the lru nonce code.
|
|
b.StopTimer()
|
|
numNonces := 100000
|
|
nonces := make([]uint64, 0, numNonces)
|
|
for i := 0; i < numNonces; i++ {
|
|
nonces = append(nonces, uint64(i))
|
|
}
|
|
b.StartTimer()
|
|
|
|
// Benchmark the add plus eviction code.
|
|
limit := uint(20000)
|
|
cache := NewCache(limit)
|
|
for i := 0; i < b.N; i++ {
|
|
cache.Add(nonces[i%numNonces])
|
|
}
|
|
}
|