Skip to content

Commit

Permalink
Replace ItemsToPrune with PercentToPrune
Browse files Browse the repository at this point in the history
  • Loading branch information
karlseguin committed Dec 26, 2024
1 parent f9779b4 commit 2c9c102
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 98 deletions.
46 changes: 26 additions & 20 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,27 @@ import (
type Cache[T any] struct {
*Configuration[T]
control
list *List[T]
size int64
buckets []*bucket[T]
bucketMask uint32
deletables chan *Item[T]
promotables chan *Item[T]
list *List[T]
size int64
pruneTargetSize int64
buckets []*bucket[T]
bucketMask uint32
deletables chan *Item[T]
promotables chan *Item[T]
}

// Create a new cache with the specified configuration
// See ccache.Configure() for creating a configuration
func New[T any](config *Configuration[T]) *Cache[T] {
c := &Cache[T]{
list: NewList[T](),
Configuration: config,
control: newControl(),
bucketMask: uint32(config.buckets) - 1,
buckets: make([]*bucket[T], config.buckets),
deletables: make(chan *Item[T], config.deleteBuffer),
promotables: make(chan *Item[T], config.promoteBuffer),
list: NewList[T](),
Configuration: config,
control: newControl(),
bucketMask: uint32(config.buckets) - 1,
buckets: make([]*bucket[T], config.buckets),
deletables: make(chan *Item[T], config.deleteBuffer),
promotables: make(chan *Item[T], config.promoteBuffer),
pruneTargetSize: config.maxSize - config.maxSize*int64(config.percentToPrune)/100,
}
for i := 0; i < config.buckets; i++ {
c.buckets[i] = &bucket[T]{
Expand Down Expand Up @@ -249,7 +251,9 @@ func (c *Cache[T]) worker() {
msg.res <- dropped
dropped = 0
case controlSetMaxSize:
c.maxSize = msg.size
newMaxSize := msg.size
c.maxSize = newMaxSize
c.pruneTargetSize = newMaxSize - newMaxSize*int64(c.percentToPrune)/100
if c.size > c.maxSize {
dropped += c.gc()
}
Expand Down Expand Up @@ -362,19 +366,21 @@ func (c *Cache[T]) gc() int {
dropped := 0
item := c.list.Tail

itemsToPrune := int64(c.itemsToPrune)
if min := c.size - c.maxSize; min > itemsToPrune {
itemsToPrune = min
}
prunedSize := int64(0)
sizeToPrune := c.size - c.pruneTargetSize

for i := int64(0); i < itemsToPrune; i++ {
for prunedSize < sizeToPrune {
if item == nil {
return dropped
}
// fmt.Println(item.key)
prev := item.prev
if !c.tracking || atomic.LoadInt32(&item.refCount) == 0 {
c.bucket(item.key).delete(item.key)
c.size -= item.size
itemSize := item.size
c.size -= itemSize
prunedSize += itemSize

c.list.Remove(item)
if c.onDelete != nil {
c.onDelete(item)
Expand Down
103 changes: 70 additions & 33 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ func Test_CacheOnDeleteCallbackCalled(t *testing.T) {
}

cache := New(Configure[string]().OnDelete(onDeleteFn))
defer cache.Stop()

cache.Set("spice", "flow", time.Minute)
cache.Set("worm", "sand", time.Minute)

Expand All @@ -150,6 +152,8 @@ func Test_CacheOnDeleteCallbackCalled(t *testing.T) {

func Test_CacheFetchesExpiredItems(t *testing.T) {
cache := New(Configure[string]())
defer cache.Stop()

fn := func() (string, error) { return "moo-moo", nil }

cache.Set("beef", "moo", time.Second*-1)
Expand All @@ -160,20 +164,24 @@ func Test_CacheFetchesExpiredItems(t *testing.T) {
}

func Test_CacheGCsTheOldestItems(t *testing.T) {
cache := New(Configure[int]().ItemsToPrune(10))
for i := 0; i < 500; i++ {
cache := New(Configure[int]().MaxSize(100).PercentToPrune(10))
defer cache.Stop()

for i := 0; i < 100; i++ {
cache.Set(strconv.Itoa(i), i, time.Minute)
}
cache.SyncUpdates()
cache.GC()
assert.Equal(t, cache.Get("9"), nil)
assert.Equal(t, cache.Get("10").Value(), 10)
assert.Equal(t, cache.ItemCount(), 490)
assert.Equal(t, cache.ItemCount(), 90)
}

func Test_CachePromotedItemsDontGetPruned(t *testing.T) {
cache := New(Configure[int]().ItemsToPrune(10).GetsPerPromote(1))
for i := 0; i < 500; i++ {
cache := New(Configure[int]().MaxSize(100).PercentToPrune(10).GetsPerPromote(1))
defer cache.Stop()

for i := 0; i < 100; i++ {
cache.Set(strconv.Itoa(i), i, time.Minute)
}
cache.SyncUpdates()
Expand All @@ -186,8 +194,10 @@ func Test_CachePromotedItemsDontGetPruned(t *testing.T) {
}

func Test_GetWithoutPromoteDoesNotPromote(t *testing.T) {
cache := New(Configure[int]().ItemsToPrune(10).GetsPerPromote(1))
for i := 0; i < 500; i++ {
cache := New(Configure[int]().MaxSize(100).PercentToPrune(10).GetsPerPromote(1))
defer cache.Stop()

for i := 0; i < 100; i++ {
cache.Set(strconv.Itoa(i), i, time.Minute)
}
cache.SyncUpdates()
Expand All @@ -200,18 +210,28 @@ func Test_GetWithoutPromoteDoesNotPromote(t *testing.T) {
}

func Test_CacheTrackerDoesNotCleanupHeldInstance(t *testing.T) {
cache := New(Configure[int]().ItemsToPrune(11).Track())
cache := New(Configure[int]().MaxSize(10).PercentToPrune(10).Track())
defer cache.Stop()

item0 := cache.TrackingSet("0", 0, time.Minute)
for i := 1; i < 11; i++ {

cache.Set("1", 1, time.Minute)
item1 := cache.TrackingGet("1")

for i := 2; i < 11; i++ {
cache.Set(strconv.Itoa(i), i, time.Minute)
}
item1 := cache.TrackingGet("1")

cache.SyncUpdates()
cache.GC()
assert.Equal(t, cache.Get("0").Value(), 0)
assert.Equal(t, cache.Get("1").Value(), 1)
item0.Release()
item1.Release()

for i := 1; i < 5; i++ {
cache.Set(strconv.Itoa(i+20), i, time.Minute)
}
cache.GC()
assert.Equal(t, cache.Get("0"), nil)
assert.Equal(t, cache.Get("1"), nil)
Expand All @@ -225,7 +245,9 @@ func Test_CacheRemovesOldestItemWhenFull(t *testing.T) {
}
}

cache := New(Configure[int]().MaxSize(5).ItemsToPrune(1).OnDelete(onDeleteFn))
cache := New(Configure[int]().MaxSize(5).PercentToPrune(1).OnDelete(onDeleteFn))
defer cache.Stop()

for i := 0; i < 7; i++ {
cache.Set(strconv.Itoa(i), i, time.Minute)
}
Expand All @@ -238,11 +260,14 @@ func Test_CacheRemovesOldestItemWhenFull(t *testing.T) {
}

func Test_CacheRemovesOldestItemWhenFullBySizer(t *testing.T) {
cache := New(Configure[*SizedItem]().MaxSize(9).ItemsToPrune(2))
for i := 0; i < 7; i++ {
cache := New(Configure[*SizedItem]().MaxSize(50).PercentToPrune(15))
defer cache.Stop()

for i := 0; i < 25; i++ {
cache.Set(strconv.Itoa(i), &SizedItem{i, 2}, time.Minute)
}
cache.SyncUpdates()
cache.GC()
assert.Equal(t, cache.Get("0"), nil)
assert.Equal(t, cache.Get("1"), nil)
assert.Equal(t, cache.Get("2"), nil)
Expand All @@ -254,6 +279,8 @@ func Test_CacheRemovesOldestItemWhenFullBySizer(t *testing.T) {

func Test_CacheSetUpdatesSizeOnDelta(t *testing.T) {
cache := New(Configure[*SizedItem]())
defer cache.Stop()

cache.Set("a", &SizedItem{0, 2}, time.Minute)
cache.Set("b", &SizedItem{0, 3}, time.Minute)
cache.SyncUpdates()
Expand All @@ -274,6 +301,8 @@ func Test_CacheSetUpdatesSizeOnDelta(t *testing.T) {

func Test_CacheReplaceDoesNotchangeSizeIfNotSet(t *testing.T) {
cache := New(Configure[*SizedItem]())
defer cache.Stop()

cache.Set("1", &SizedItem{1, 2}, time.Minute)
cache.Set("2", &SizedItem{1, 2}, time.Minute)
cache.Set("3", &SizedItem{1, 2}, time.Minute)
Expand All @@ -284,6 +313,8 @@ func Test_CacheReplaceDoesNotchangeSizeIfNotSet(t *testing.T) {

func Test_CacheReplaceChangesSize(t *testing.T) {
cache := New(Configure[*SizedItem]())
defer cache.Stop()

cache.Set("1", &SizedItem{1, 2}, time.Minute)
cache.Set("2", &SizedItem{1, 2}, time.Minute)

Expand All @@ -301,39 +332,42 @@ func Test_CacheReplaceChangesSize(t *testing.T) {
}

func Test_CacheResizeOnTheFly(t *testing.T) {
cache := New(Configure[int]().MaxSize(9).ItemsToPrune(1))
for i := 0; i < 5; i++ {
cache := New(Configure[int]().MaxSize(50).PercentToPrune(10))
defer cache.Stop()

for i := 0; i < 50; i++ {
cache.Set(strconv.Itoa(i), i, time.Minute)
}
cache.SetMaxSize(3)
cache.SyncUpdates()
assert.Equal(t, cache.GetDropped(), 2)
assert.Equal(t, cache.Get("0"), nil)
assert.Equal(t, cache.Get("1"), nil)
assert.Equal(t, cache.Get("2").Value(), 2)
assert.Equal(t, cache.Get("3").Value(), 3)
assert.Equal(t, cache.Get("4").Value(), 4)
assert.Equal(t, cache.GetDropped(), 47)
assert.Equal(t, cache.Get("46"), nil)
assert.Equal(t, cache.Get("47").Value(), 47)
assert.Equal(t, cache.Get("48").Value(), 48)
assert.Equal(t, cache.Get("49").Value(), 49)

cache.Set("5", 5, time.Minute)
cache.Set("50", 50, time.Minute)
cache.SyncUpdates()
assert.Equal(t, cache.GetDropped(), 1)
assert.Equal(t, cache.Get("2"), nil)
assert.Equal(t, cache.Get("3").Value(), 3)
assert.Equal(t, cache.Get("4").Value(), 4)
assert.Equal(t, cache.Get("5").Value(), 5)
assert.Equal(t, cache.Get("47"), nil)
assert.Equal(t, cache.Get("48").Value(), 48)
assert.Equal(t, cache.Get("49").Value(), 49)
assert.Equal(t, cache.Get("50").Value(), 50)

cache.SetMaxSize(10)
cache.Set("6", 6, time.Minute)
cache.Set("51", 51, time.Minute)
cache.SyncUpdates()
assert.Equal(t, cache.GetDropped(), 0)
assert.Equal(t, cache.Get("3").Value(), 3)
assert.Equal(t, cache.Get("4").Value(), 4)
assert.Equal(t, cache.Get("5").Value(), 5)
assert.Equal(t, cache.Get("6").Value(), 6)
assert.Equal(t, cache.Get("48").Value(), 48)
assert.Equal(t, cache.Get("49").Value(), 49)
assert.Equal(t, cache.Get("50").Value(), 50)
assert.Equal(t, cache.Get("51").Value(), 51)
}

func Test_CacheForEachFunc(t *testing.T) {
cache := New(Configure[int]().MaxSize(3).ItemsToPrune(1))
cache := New(Configure[int]().MaxSize(3).PercentToPrune(1))
defer cache.Stop()

assert.List(t, forEachKeys[int](cache), []string{})

cache.Set("1", 1, time.Minute)
Expand Down Expand Up @@ -362,7 +396,9 @@ func Test_CacheForEachFunc(t *testing.T) {

func Test_CachePrune(t *testing.T) {
maxSize := int64(500)
cache := New(Configure[string]().MaxSize(maxSize).ItemsToPrune(50))
cache := New(Configure[string]().MaxSize(maxSize).PercentToPrune(50))
defer cache.Stop()

epoch := 0
for i := 0; i < 10000; i++ {
epoch += 1
Expand Down Expand Up @@ -445,6 +481,7 @@ func Test_ConcurrentClearAndSet(t *testing.T) {
}
t.Errorf("cache list and lookup are not consistent")
t.FailNow()
cache.Stop()
}
}

Expand Down
15 changes: 10 additions & 5 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type Configuration[T any] struct {
maxSize int64
buckets int
itemsToPrune int
percentToPrune int
deleteBuffer int
promoteBuffer int
getsPerPromote int32
Expand All @@ -17,7 +18,8 @@ type Configuration[T any] struct {
func Configure[T any]() *Configuration[T] {
return &Configuration[T]{
buckets: 16,
itemsToPrune: 500,
itemsToPrune: 0,
percentToPrune: 10,
deleteBuffer: 1024,
getsPerPromote: 3,
promoteBuffer: 1024,
Expand All @@ -44,10 +46,13 @@ func (c *Configuration[T]) Buckets(count uint32) *Configuration[T] {
return c
}

// The number of items to prune when memory is low
// [500]
func (c *Configuration[T]) ItemsToPrune(count uint32) *Configuration[T] {
c.itemsToPrune = int(count)
// The percent of the max size to prune when memory is low.
// [10]
func (c *Configuration[T]) PercentToPrune(percent uint8) *Configuration[T] {
if percent > 100 {
percent = 20
}
c.percentToPrune = int(percent)
return c
}

Expand Down
Loading

0 comments on commit 2c9c102

Please sign in to comment.