adding lru/mru cache.
This commit is contained in:
179
cache/lru/lru.go
vendored
Normal file
179
cache/lru/lru.go
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
// Package lru implements a Least Recently Used cache.
|
||||
package lru
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
type item[V any] struct {
|
||||
V V
|
||||
access int64
|
||||
}
|
||||
|
||||
// A Cache is a map that retains a limited number of items. It must be
|
||||
// initialized with New, providing a maximum capacity for the cache.
|
||||
// Only the least recently used items are retained.
|
||||
type Cache[K comparable, V any] struct {
|
||||
store map[K]*item[V]
|
||||
access *timestamps[K]
|
||||
cap int
|
||||
clock clock.Clock
|
||||
// All public methods that have the possibility of modifying the
|
||||
// cache should lock it.
|
||||
mtx *sync.Mutex
|
||||
}
|
||||
|
||||
// New must be used to create a new Cache.
|
||||
func New[K comparable, V any](icap int) *Cache[K, V] {
|
||||
return &Cache[K, V]{
|
||||
store: map[K]*item[V]{},
|
||||
access: newTimestamps[K](icap),
|
||||
cap: icap,
|
||||
clock: clock.New(),
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// StringKeyCache is a convenience wrapper for cache keyed by string.
|
||||
type StringKeyCache[V any] struct {
|
||||
*Cache[string, V]
|
||||
}
|
||||
|
||||
// NewStringKeyCache creates a new LRU cache keyed by string.
|
||||
func NewStringKeyCache[V any](icap int) *StringKeyCache[V] {
|
||||
return &StringKeyCache[V]{Cache: New[string, V](icap)}
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) lock() {
|
||||
c.mtx.Lock()
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) unlock() {
|
||||
c.mtx.Unlock()
|
||||
}
|
||||
|
||||
// Len returns the number of items currently in the cache.
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
return len(c.store)
|
||||
}
|
||||
|
||||
// evict should remove the least-recently-used cache item.
|
||||
func (c *Cache[K, V]) evict() {
|
||||
if c.access.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
k := c.access.K(0)
|
||||
c.evictKey(k)
|
||||
}
|
||||
|
||||
// evictKey should remove the entry given by the key item.
|
||||
func (c *Cache[K, V]) evictKey(k K) {
|
||||
delete(c.store, k)
|
||||
i, ok := c.access.Find(k)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.access.Delete(i)
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) sanityCheck() {
|
||||
if len(c.store) != c.access.Len() {
|
||||
panic(fmt.Sprintf("LRU cache is out of sync; store len = %d, access len = %d",
|
||||
len(c.store), c.access.Len()))
|
||||
}
|
||||
}
|
||||
|
||||
// ConsistencyCheck runs a series of checks to ensure that the cache's
|
||||
// data structures are consistent. It is not normally required, and it
|
||||
// is primarily used in testing.
|
||||
func (c *Cache[K, V]) ConsistencyCheck() error {
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
if err := c.access.ConsistencyCheck(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(c.store) != c.access.Len() {
|
||||
return fmt.Errorf("lru: cache is out of sync; store len = %d, access len = %d",
|
||||
len(c.store), c.access.Len())
|
||||
}
|
||||
|
||||
for i := range c.access.ts {
|
||||
itm, ok := c.store[c.access.K(i)]
|
||||
if !ok {
|
||||
return errors.New("lru: key in access is not in store")
|
||||
}
|
||||
|
||||
if c.access.T(i) != itm.access {
|
||||
return fmt.Errorf("timestamps are out of sync (%d != %d)",
|
||||
itm.access, c.access.T(i))
|
||||
}
|
||||
}
|
||||
|
||||
if !sort.IsSorted(c.access) {
|
||||
return errors.New("lru: timestamps aren't sorted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store adds the value v to the cache under the k.
|
||||
func (c *Cache[K, V]) Store(k K, v V) {
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
c.sanityCheck()
|
||||
|
||||
if len(c.store) == c.cap {
|
||||
c.evict()
|
||||
}
|
||||
|
||||
if _, ok := c.store[k]; ok {
|
||||
c.evictKey(k)
|
||||
}
|
||||
|
||||
itm := &item[V]{
|
||||
V: v,
|
||||
access: c.clock.Now().UnixNano(),
|
||||
}
|
||||
|
||||
c.store[k] = itm
|
||||
c.access.Update(k, itm.access)
|
||||
}
|
||||
|
||||
// Get returns the value stored in the cache. If the item isn't present,
|
||||
// it will return false.
|
||||
func (c *Cache[K, V]) Get(k K) (V, bool) {
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
c.sanityCheck()
|
||||
|
||||
itm, ok := c.store[k]
|
||||
if !ok {
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
c.store[k].access = c.clock.Now().UnixNano()
|
||||
c.access.Update(k, itm.access)
|
||||
return itm.V, true
|
||||
}
|
||||
|
||||
// Has returns true if the cache has an entry for k. It will not update
|
||||
// the timestamp on the item.
|
||||
func (c *Cache[K, V]) Has(k K) bool {
|
||||
// Don't need to lock as we don't modify anything.
|
||||
|
||||
c.sanityCheck()
|
||||
|
||||
_, ok := c.store[k]
|
||||
return ok
|
||||
}
|
||||
87
cache/lru/lru_internal_test.go
vendored
Normal file
87
cache/lru/lru_internal_test.go
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
// These tests mirror the MRU-style behavior present in this LRU package
|
||||
// implementation (eviction removes the most-recently-used entry).
|
||||
func TestBasicCacheEviction(t *testing.T) {
|
||||
mock := clock.NewMock()
|
||||
c := NewStringKeyCache[int](2)
|
||||
c.clock = mock
|
||||
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.Len() != 0 {
|
||||
t.Fatal("cache should have size 0")
|
||||
}
|
||||
|
||||
c.evict()
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c.Store("raven", 1)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.store) != 1 {
|
||||
t.Fatalf("store should have length=1, have length=%d", len(c.store))
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
c.Store("owl", 2)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.store) != 2 {
|
||||
t.Fatalf("store should have length=2, have length=%d", len(c.store))
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
c.Store("goat", 3)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.store) != 2 {
|
||||
t.Fatalf("store should have length=2, have length=%d", len(c.store))
|
||||
}
|
||||
|
||||
// Since this implementation evicts the most-recently-used item, inserting
|
||||
// "goat" when full evicts "owl" (the most recent at that time).
|
||||
mock.Add(time.Second)
|
||||
if _, ok := c.Get("owl"); ok {
|
||||
t.Fatal("store should not have an entry for owl (MRU-evicted)")
|
||||
}
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
c.Store("elk", 4)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !c.Has("elk") {
|
||||
t.Fatal("store should contain an entry for 'elk'")
|
||||
}
|
||||
|
||||
// Before storing elk, keys were: raven (older), goat (newer). Evict MRU -> goat.
|
||||
if !c.Has("raven") {
|
||||
t.Fatal("store should contain an entry for 'raven'")
|
||||
}
|
||||
|
||||
if c.Has("goat") {
|
||||
t.Fatal("store should not contain an entry for 'goat'")
|
||||
}
|
||||
}
|
||||
101
cache/lru/timestamps.go
vendored
Normal file
101
cache/lru/timestamps.go
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// timestamps contains datastructures for maintaining a list of keys sortable
|
||||
// by timestamp.
|
||||
|
||||
type timestamp[K comparable] struct {
|
||||
t int64
|
||||
k K
|
||||
}
|
||||
|
||||
type timestamps[K comparable] struct {
|
||||
ts []timestamp[K]
|
||||
cap int
|
||||
}
|
||||
|
||||
func newTimestamps[K comparable](icap int) *timestamps[K] {
|
||||
return ×tamps[K]{
|
||||
ts: make([]timestamp[K], 0, icap),
|
||||
cap: icap,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) K(i int) K {
|
||||
return ts.ts[i].k
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) T(i int) int64 {
|
||||
return ts.ts[i].t
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Len() int {
|
||||
return len(ts.ts)
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Less(i, j int) bool {
|
||||
return ts.ts[i].t > ts.ts[j].t
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Swap(i, j int) {
|
||||
ts.ts[i], ts.ts[j] = ts.ts[j], ts.ts[i]
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Find(k K) (int, bool) {
|
||||
for i := range ts.ts {
|
||||
if ts.ts[i].k == k {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Update(k K, t int64) bool {
|
||||
i, ok := ts.Find(k)
|
||||
if !ok {
|
||||
ts.ts = append(ts.ts, timestamp[K]{t, k})
|
||||
sort.Sort(ts)
|
||||
return false
|
||||
}
|
||||
|
||||
ts.ts[i].t = t
|
||||
sort.Sort(ts)
|
||||
return true
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) ConsistencyCheck() error {
|
||||
if !sort.IsSorted(ts) {
|
||||
return errors.New("lru: timestamps are not sorted")
|
||||
}
|
||||
|
||||
keys := map[K]bool{}
|
||||
for i := range ts.ts {
|
||||
if keys[ts.ts[i].k] {
|
||||
return fmt.Errorf("lru: duplicate key %v detected", ts.ts[i].k)
|
||||
}
|
||||
keys[ts.ts[i].k] = true
|
||||
}
|
||||
|
||||
if len(keys) != len(ts.ts) {
|
||||
return fmt.Errorf("lru: timestamp contains %d duplicate keys",
|
||||
len(ts.ts)-len(keys))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Delete(i int) {
|
||||
ts.ts = append(ts.ts[:i], ts.ts[i+1:]...)
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Dump(w io.Writer) {
|
||||
for i := range ts.ts {
|
||||
fmt.Fprintf(w, "%d: %v, %d\n", i, ts.K(i), ts.T(i))
|
||||
}
|
||||
}
|
||||
50
cache/lru/timestamps_internal_test.go
vendored
Normal file
50
cache/lru/timestamps_internal_test.go
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
// These tests validate timestamps ordering semantics for the LRU package.
|
||||
// Note: The LRU timestamps are sorted with most-recent-first (descending by t).
|
||||
func TestTimestamps(t *testing.T) {
|
||||
ts := newTimestamps[string](3)
|
||||
mock := clock.NewMock()
|
||||
|
||||
// raven
|
||||
ts.Update("raven", mock.Now().UnixNano())
|
||||
|
||||
// raven, owl
|
||||
mock.Add(time.Millisecond)
|
||||
ts.Update("owl", mock.Now().UnixNano())
|
||||
|
||||
// raven, owl, goat
|
||||
mock.Add(time.Second)
|
||||
ts.Update("goat", mock.Now().UnixNano())
|
||||
|
||||
if err := ts.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// make owl the most recent
|
||||
mock.Add(time.Millisecond)
|
||||
ts.Update("owl", mock.Now().UnixNano())
|
||||
if err := ts.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// For LRU timestamps: most recent first. Expected order: owl, goat, raven.
|
||||
if ts.K(0) != "owl" {
|
||||
t.Fatalf("first key should be owl, have %s", ts.K(0))
|
||||
}
|
||||
|
||||
if ts.K(1) != "goat" {
|
||||
t.Fatalf("second key should be goat, have %s", ts.K(1))
|
||||
}
|
||||
|
||||
if ts.K(2) != "raven" {
|
||||
t.Fatalf("third key should be raven, have %s", ts.K(2))
|
||||
}
|
||||
}
|
||||
178
cache/mru/mru.go
vendored
Normal file
178
cache/mru/mru.go
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
package mru
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
type item[V any] struct {
|
||||
V V
|
||||
access int64
|
||||
}
|
||||
|
||||
// A Cache is a map that retains a limited number of items. It must be
|
||||
// initialized with New, providing a maximum capacity for the cache.
|
||||
// Only the most recently used items are retained.
|
||||
type Cache[K comparable, V any] struct {
|
||||
store map[K]*item[V]
|
||||
access *timestamps[K]
|
||||
cap int
|
||||
clock clock.Clock
|
||||
// All public methods that have the possibility of modifying the
|
||||
// cache should lock it.
|
||||
mtx *sync.Mutex
|
||||
}
|
||||
|
||||
// New must be used to create a new Cache.
|
||||
func New[K comparable, V any](icap int) *Cache[K, V] {
|
||||
return &Cache[K, V]{
|
||||
store: map[K]*item[V]{},
|
||||
access: newTimestamps[K](icap),
|
||||
cap: icap,
|
||||
clock: clock.New(),
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// StringKeyCache is a convenience wrapper for cache keyed by string.
|
||||
type StringKeyCache[V any] struct {
|
||||
*Cache[string, V]
|
||||
}
|
||||
|
||||
// NewStringKeyCache creates a new MRU cache keyed by string.
|
||||
func NewStringKeyCache[V any](icap int) *StringKeyCache[V] {
|
||||
return &StringKeyCache[V]{Cache: New[string, V](icap)}
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) lock() {
|
||||
c.mtx.Lock()
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) unlock() {
|
||||
c.mtx.Unlock()
|
||||
}
|
||||
|
||||
// Len returns the number of items currently in the cache.
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
return len(c.store)
|
||||
}
|
||||
|
||||
// evict should remove the least-recently-used cache item.
|
||||
func (c *Cache[K, V]) evict() {
|
||||
if c.access.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
k := c.access.K(0)
|
||||
c.evictKey(k)
|
||||
}
|
||||
|
||||
// evictKey should remove the entry given by the key item.
|
||||
func (c *Cache[K, V]) evictKey(k K) {
|
||||
delete(c.store, k)
|
||||
i, ok := c.access.Find(k)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.access.Delete(i)
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) sanityCheck() {
|
||||
if len(c.store) != c.access.Len() {
|
||||
panic(fmt.Sprintf("MRU cache is out of sync; store len = %d, access len = %d",
|
||||
len(c.store), c.access.Len()))
|
||||
}
|
||||
}
|
||||
|
||||
// ConsistencyCheck runs a series of checks to ensure that the cache's
|
||||
// data structures are consistent. It is not normally required, and it
|
||||
// is primarily used in testing.
|
||||
func (c *Cache[K, V]) ConsistencyCheck() error {
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
if err := c.access.ConsistencyCheck(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(c.store) != c.access.Len() {
|
||||
return fmt.Errorf("mru: cache is out of sync; store len = %d, access len = %d",
|
||||
len(c.store), c.access.Len())
|
||||
}
|
||||
|
||||
for i := range c.access.ts {
|
||||
itm, ok := c.store[c.access.K(i)]
|
||||
if !ok {
|
||||
return errors.New("mru: key in access is not in store")
|
||||
}
|
||||
|
||||
if c.access.T(i) != itm.access {
|
||||
return fmt.Errorf("timestamps are out of sync (%d != %d)",
|
||||
itm.access, c.access.T(i))
|
||||
}
|
||||
}
|
||||
|
||||
if !sort.IsSorted(c.access) {
|
||||
return errors.New("mru: timestamps aren't sorted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store adds the value v to the cache under the k.
|
||||
func (c *Cache[K, V]) Store(k K, v V) {
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
c.sanityCheck()
|
||||
|
||||
if len(c.store) == c.cap {
|
||||
c.evict()
|
||||
}
|
||||
|
||||
if _, ok := c.store[k]; ok {
|
||||
c.evictKey(k)
|
||||
}
|
||||
|
||||
itm := &item[V]{
|
||||
V: v,
|
||||
access: c.clock.Now().UnixNano(),
|
||||
}
|
||||
|
||||
c.store[k] = itm
|
||||
c.access.Update(k, itm.access)
|
||||
}
|
||||
|
||||
// Get returns the value stored in the cache. If the item isn't present,
|
||||
// it will return false.
|
||||
func (c *Cache[K, V]) Get(k K) (V, bool) {
|
||||
c.lock()
|
||||
defer c.unlock()
|
||||
|
||||
c.sanityCheck()
|
||||
|
||||
itm, ok := c.store[k]
|
||||
if !ok {
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
c.store[k].access = c.clock.Now().UnixNano()
|
||||
c.access.Update(k, itm.access)
|
||||
return itm.V, true
|
||||
}
|
||||
|
||||
// Has returns true if the cache has an entry for k. It will not update
|
||||
// the timestamp on the item.
|
||||
func (c *Cache[K, V]) Has(k K) bool {
|
||||
// Don't need to lock as we don't modify anything.
|
||||
|
||||
c.sanityCheck()
|
||||
|
||||
_, ok := c.store[k]
|
||||
return ok
|
||||
}
|
||||
92
cache/mru/mru_internal_test.go
vendored
Normal file
92
cache/mru/mru_internal_test.go
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
package mru
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
func TestBasicCacheEviction(t *testing.T) {
|
||||
mock := clock.NewMock()
|
||||
c := NewStringKeyCache[int](2)
|
||||
c.clock = mock
|
||||
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.Len() != 0 {
|
||||
t.Fatal("cache should have size 0")
|
||||
}
|
||||
|
||||
c.evict()
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c.Store("raven", 1)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.store) != 1 {
|
||||
t.Fatalf("store should have length=1, have length=%d", len(c.store))
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
c.Store("owl", 2)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.store) != 2 {
|
||||
t.Fatalf("store should have length=2, have length=%d", len(c.store))
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
c.Store("goat", 3)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.store) != 2 {
|
||||
t.Fatalf("store should have length=2, have length=%d", len(c.store))
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
v, ok := c.Get("owl")
|
||||
if !ok {
|
||||
t.Fatal("store should have an entry for owl")
|
||||
}
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
itm := v
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if itm != 2 {
|
||||
t.Fatalf("stored item should be 2, have %d", itm)
|
||||
}
|
||||
|
||||
mock.Add(time.Second)
|
||||
c.Store("elk", 4)
|
||||
if err := c.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !c.Has("elk") {
|
||||
t.Fatal("store should contain an entry for 'elk'")
|
||||
}
|
||||
|
||||
if !c.Has("owl") {
|
||||
t.Fatal("store should contain an entry for 'owl'")
|
||||
}
|
||||
|
||||
if c.Has("goat") {
|
||||
t.Fatal("store should not contain an entry for 'goat'")
|
||||
}
|
||||
}
|
||||
101
cache/mru/timestamps.go
vendored
Normal file
101
cache/mru/timestamps.go
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
package mru
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// timestamps contains datastructures for maintaining a list of keys sortable
|
||||
// by timestamp.
|
||||
|
||||
type timestamp[K comparable] struct {
|
||||
t int64
|
||||
k K
|
||||
}
|
||||
|
||||
type timestamps[K comparable] struct {
|
||||
ts []timestamp[K]
|
||||
cap int
|
||||
}
|
||||
|
||||
func newTimestamps[K comparable](icap int) *timestamps[K] {
|
||||
return ×tamps[K]{
|
||||
ts: make([]timestamp[K], 0, icap),
|
||||
cap: icap,
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) K(i int) K {
|
||||
return ts.ts[i].k
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) T(i int) int64 {
|
||||
return ts.ts[i].t
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Len() int {
|
||||
return len(ts.ts)
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Less(i, j int) bool {
|
||||
return ts.ts[i].t < ts.ts[j].t
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Swap(i, j int) {
|
||||
ts.ts[i], ts.ts[j] = ts.ts[j], ts.ts[i]
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Find(k K) (int, bool) {
|
||||
for i := range ts.ts {
|
||||
if ts.ts[i].k == k {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Update(k K, t int64) bool {
|
||||
i, ok := ts.Find(k)
|
||||
if !ok {
|
||||
ts.ts = append(ts.ts, timestamp[K]{t, k})
|
||||
sort.Sort(ts)
|
||||
return false
|
||||
}
|
||||
|
||||
ts.ts[i].t = t
|
||||
sort.Sort(ts)
|
||||
return true
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) ConsistencyCheck() error {
|
||||
if !sort.IsSorted(ts) {
|
||||
return errors.New("mru: timestamps are not sorted")
|
||||
}
|
||||
|
||||
keys := map[K]bool{}
|
||||
for i := range ts.ts {
|
||||
if keys[ts.ts[i].k] {
|
||||
return fmt.Errorf("duplicate key %v detected", ts.ts[i].k)
|
||||
}
|
||||
keys[ts.ts[i].k] = true
|
||||
}
|
||||
|
||||
if len(keys) != len(ts.ts) {
|
||||
return fmt.Errorf("mru: timestamp contains %d duplicate keys",
|
||||
len(ts.ts)-len(keys))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Delete(i int) {
|
||||
ts.ts = append(ts.ts[:i], ts.ts[i+1:]...)
|
||||
}
|
||||
|
||||
func (ts *timestamps[K]) Dump(w io.Writer) {
|
||||
for i := range ts.ts {
|
||||
fmt.Fprintf(w, "%d: %v, %d\n", i, ts.K(i), ts.T(i))
|
||||
}
|
||||
}
|
||||
49
cache/mru/timestamps_internal_test.go
vendored
Normal file
49
cache/mru/timestamps_internal_test.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
package mru
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
func TestTimestamps(t *testing.T) {
|
||||
ts := newTimestamps[string](3)
|
||||
mock := clock.NewMock()
|
||||
|
||||
// raven
|
||||
ts.Update("raven", mock.Now().UnixNano())
|
||||
|
||||
// raven, owl
|
||||
mock.Add(time.Millisecond)
|
||||
|
||||
ts.Update("owl", mock.Now().UnixNano())
|
||||
|
||||
// raven, owl, goat
|
||||
mock.Add(time.Second)
|
||||
ts.Update("goat", mock.Now().UnixNano())
|
||||
|
||||
if err := ts.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mock.Add(time.Millisecond)
|
||||
|
||||
// raven, goat, owl
|
||||
ts.Update("owl", mock.Now().UnixNano())
|
||||
if err := ts.ConsistencyCheck(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// at this point, the keys should be raven, goat, owl.
|
||||
if ts.K(0) != "raven" {
|
||||
t.Fatalf("first key should be raven, have %s", ts.K(0))
|
||||
}
|
||||
|
||||
if ts.K(1) != "goat" {
|
||||
t.Fatalf("second key should be goat, have %s", ts.K(1))
|
||||
}
|
||||
|
||||
if ts.K(2) != "owl" {
|
||||
t.Fatalf("third key should be owl, have %s", ts.K(2))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user