Skip to content

Commit

Permalink
Add support for CLOCK cache
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick Brandt authored and nakkamarra committed Jul 25, 2024
1 parent cb54caf commit 72c21e5
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 0 deletions.
138 changes: 138 additions & 0 deletions clock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//go:build go1.19

/*
Copyright 2024 Vimeo Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package clock

import "sync/atomic"

type bufferItem[K comparable, V any] struct {
key K
value V
}

type buffer[K comparable, V any] []*bufferItem[K, V]

func (b buffer[K, V]) insertAt(index int, key K, value V) {
b[index] = &bufferItem[K, V]{
key: key,
value: value,
}
}

// Cache is a cache based on the CLOCK cache policy.
// It stores elements in a ring buffer, and stores a
// touch count for each element in the cache which
// is uses to determine whether or not a given element
// should be evicted (lower touches means more likely
// to be evicted).
type Cache[K comparable, V any] struct {
MaxEntries int
OnEvicted func(key K, val V)

indices map[K]int
buf buffer[K, V]
touches []atomic.Int64
clockhand int
}

func New[K comparable, V any](maxEntries int) *Cache[K, V] {
return &Cache[K, V]{
MaxEntries: maxEntries,
indices: make(map[K]int, maxEntries),
buf: make(buffer[K, V], 0, maxEntries),
touches: make([]atomic.Int64, maxEntries),
clockhand: 0,
}
}

// Add inserts a given key-value pair into the cache,
// evicting a previous entry if necessary
func (c *Cache[K, V]) Add(key K, val V) {
c.checkAndInit()
if index, hit := c.indices[key]; hit {
c.touches[index].Add(1)
return
}
// Not full yet, insert at clock hand
if !c.IsFull() {
c.buf = append(c.buf, &bufferItem[K, V]{
key: key,
value: val,
})
c.indices[key] = len(c.buf) - 1
c.clockhand = (c.clockhand + 1) % len(c.buf)
return
}
// Full, evict by reference bit then replace
hand := c.clockhand
for c.touches[hand].Load() > 0 {
c.touches[hand].Add(-1)
hand = (hand + 1) % len(c.buf)
}
c.Evict(c.buf[hand].key)
c.buf.insertAt(hand, key, val)
c.indices[key] = hand
c.clockhand = hand
}

// Get returns the value for a given key, if present.
// ok bool will be false if the key does not exist
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
c.checkAndInit()
if index, hit := c.indices[key]; hit {
c.touches[index].Add(1)
return c.buf[index].value, true
}
return
}

// Evict will remove the given key, if present, from
// the cache
func (c *Cache[K, V]) Evict(key K) {
c.checkAndInit()
index, ok := c.indices[key]
if !ok {
return
}
delete(c.indices, key)
if c.OnEvicted != nil {
c.OnEvicted(key, c.buf[index].value)
}
c.buf[index] = nil
c.touches[index].Store(0)
}

// IsFull returns whether or not the cache is at capacity,
// as defined by the cache's max entries
func (c *Cache[K, V]) IsFull() bool {
return len(c.buf) == c.MaxEntries
}

// checkAndInit ensures that the backing map and slices
// of the cache are not nil
func (c *Cache[K, V]) checkAndInit() {
if c.indices == nil {
c.indices = make(map[K]int, c.MaxEntries)
}
if c.buf == nil {
c.buf = make(buffer[K, V], 0, c.MaxEntries)
}
if c.touches == nil {
c.buf = make(buffer[K, V], c.MaxEntries)
}
}
46 changes: 46 additions & 0 deletions clock/clock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build go1.19

/*
Copyright 2024 Vimeo Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package clock

import "testing"

func TestCacheGet(t *testing.T) {
tests := []struct {
name string
keyToAdd string
keyToGet string
valueToAdd int
expectedOk bool
}{
{"string_hit", "myKey", "myKey", 1234, true},
{"string_miss", "myKey", "nonsense", 1234, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cache := New[string, int](5)
cache.Add(test.keyToAdd, test.valueToAdd)
value, ok := cache.Get(test.keyToGet)
if ok != test.expectedOk {
t.Fatalf("cache hit = %v; want %v", ok, test.expectedOk)
} else if ok && value != test.valueToAdd {
t.Fatalf("got %v; want %v", value, test.valueToAdd)
}
})
}
}

0 comments on commit 72c21e5

Please sign in to comment.