commit 7a03538a662167aaf830ad7f15516ca685f9c0fb Author: Kyle Isom Date: Tue Feb 8 20:05:49 2022 -0800 Initial import. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..09098f8 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.sr.ht/~kisom/mru + +go 1.17 + +require github.com/benbjohnson/clock v1.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c284fd6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= diff --git a/mru.go b/mru.go new file mode 100644 index 0000000..6ef403d --- /dev/null +++ b/mru.go @@ -0,0 +1,128 @@ +package mru + +import ( + "errors" + "fmt" + "sort" + + "github.com/benbjohnson/clock" +) + +type Item struct { + V interface{} + access int64 +} + +type Cache struct { + store map[string]*Item + access *timestamps + cap int + clock clock.Clock +} + +func New(cap int) *Cache { + return &Cache{ + store: map[string]*Item{}, + access: newTimestamps(cap), + cap: cap, + clock: clock.New(), + } +} + +func (c *Cache) Len() int { + return len(c.store) +} + +// evict should remove the least-recently-used cache item. +func (c *Cache) evict() { + k := c.access.K(0) + c.evictKey(k) +} + +// evictKey should remove the entry given by the key item. +func (c *Cache) evictKey(k string) { + delete(c.store, k) + i, ok := c.access.Find(k) + if !ok { + return + } + + c.access.Delete(i) +} + +func (c *Cache) 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())) + } +} + +func (c *Cache) ConsistencyCheck() error { + 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 +} + +func (c *Cache) Store(k string, v interface{}) { + c.sanityCheck() + + if len(c.store) == c.cap { + c.evict() + } + + if _, ok := c.store[k]; ok { + c.evictKey(k) + } + + itm := &Item{ + V: v, + access: c.clock.Now().UnixNano(), + } + + c.store[k] = itm + c.access.Update(k, itm.access) +} + +func (c *Cache) Get(k string) (interface{}, bool) { + c.sanityCheck() + + itm, ok := c.store[k] + if !ok { + return nil, false + } + + c.store[k].access = c.clock.Now().UnixNano() + c.access.Update(k, itm.access) + return itm.V, true +} + +// Has will not update the timestamp on the item. +func (c *Cache) Has(k string) bool { + c.sanityCheck() + + _, ok := c.store[k] + return ok +} diff --git a/mru_test.go b/mru_test.go new file mode 100644 index 0000000..d823b5b --- /dev/null +++ b/mru_test.go @@ -0,0 +1,90 @@ +package mru + +import ( + "testing" + "time" + + "github.com/benbjohnson/clock" +) + +func TestBasicCacheEviction(t *testing.T) { + mock := clock.NewMock() + c := New(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.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, ok := v.(int) + if !ok { + t.Fatalf("stored item is not an int; have %T", 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'") + } +} diff --git a/timestamps.go b/timestamps.go new file mode 100644 index 0000000..37881ff --- /dev/null +++ b/timestamps.go @@ -0,0 +1,100 @@ +package mru + +import ( + "errors" + "fmt" + "sort" +) + +// timestamps contains datastructures for maintaining a list of keys sortable +// by timestamp. + +type timestamp struct { + t int64 + k string +} + +type timestamps struct { + ts []timestamp + cap int +} + +func newTimestamps(cap int) *timestamps { + return ×tamps{ + ts: make([]timestamp, 0, cap), + cap: cap, + } +} + +func (ts *timestamps) K(i int) string { + return ts.ts[i].k +} + +func (ts *timestamps) T(i int) int64 { + return ts.ts[i].t +} + +func (ts *timestamps) Len() int { + return len(ts.ts) +} + +func (ts *timestamps) Less(i, j int) bool { + return ts.ts[i].t < ts.ts[j].t +} + +func (ts *timestamps) Swap(i, j int) { + ts.ts[i], ts.ts[j] = ts.ts[j], ts.ts[i] +} + +func (ts *timestamps) Find(k string) (int, bool) { + for i := 0; i < len(ts.ts); i++ { + if ts.ts[i].k == k { + return i, true + } + } + return -1, false +} + +func (ts *timestamps) Update(k string, t int64) bool { + i, ok := ts.Find(k) + if !ok { + ts.ts = append(ts.ts, timestamp{t, k}) + sort.Sort(ts) + return false + } + + ts.ts[i].t = t + sort.Sort(ts) + return true +} + +func (ts *timestamps) ConsistencyCheck() error { + if !sort.IsSorted(ts) { + return errors.New("mru: timestamps are not sorted") + } + + keys := map[string]bool{} + for i := range ts.ts { + if keys[ts.ts[i].k] { + return fmt.Errorf("duplicate key %s 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) Delete(i int) { + ts.ts = append(ts.ts[:i], ts.ts[i+1:]...) +} + +func (ts *timestamps) Dump() { + for i := range ts.ts { + fmt.Printf("%d: %s, %d\n", i, ts.K(i), ts.T(i)) + } +} diff --git a/timestamps_test.go b/timestamps_test.go new file mode 100644 index 0000000..13e3bbf --- /dev/null +++ b/timestamps_test.go @@ -0,0 +1,50 @@ +package mru + +import ( + "testing" + "time" + + "github.com/benbjohnson/clock" +) + +func TestTimestamps(t *testing.T) { + ts := newTimestamps(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)) + } + +}