Skip to content

Commit

Permalink
[#63] Add removal listener
Browse files Browse the repository at this point in the history
  • Loading branch information
maypok86 committed Mar 7, 2024
1 parent 80a12be commit 26dc222
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 30 deletions.
30 changes: 30 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type baseOptions[K comparable, V any] struct {
statsEnabled bool
withCost bool
costFunc func(key K, value V) uint32
removalListener func(key K, value V, cause RemovalCause)
}

func (o *baseOptions[K, V]) collectStats() {
Expand All @@ -57,6 +58,10 @@ func (o *baseOptions[K, V]) setInitialCapacity(initialCapacity int) {
o.initialCapacity = initialCapacity
}

func (o *baseOptions[K, V]) setRemovalListener(removalListener func(key K, value V, cause RemovalCause)) {
o.removalListener = removalListener
}

func (o *baseOptions[K, V]) validate() error {
if o.initialCapacity <= 0 && o.initialCapacity != unsetCapacity {
return ErrIllegalInitialCapacity
Expand All @@ -78,6 +83,7 @@ func (o *baseOptions[K, V]) toConfig() core.Config[K, V] {
StatsEnabled: o.statsEnabled,
CostFunc: o.costFunc,
WithCost: o.withCost,
RemovalListener: o.removalListener,
}
}

Expand Down Expand Up @@ -169,6 +175,14 @@ func (b *Builder[K, V]) Cost(costFunc func(key K, value V) uint32) *Builder[K, V
return b
}

// RemovalListener specifies a listener instance that caches should notify each time an entry is removed for any
// RemovalCause reason. The cache will invoke this listener in the background goroutine
// after the entry's removal operation has completed.
func (b *Builder[K, V]) RemovalListener(removalListener func(key K, value V, cause RemovalCause)) *Builder[K, V] {
b.setRemovalListener(removalListener)
return b
}

// WithTTL specifies that each item should be automatically removed from the cache once a fixed duration
// has elapsed after the item's creation.
func (b *Builder[K, V]) WithTTL(ttl time.Duration) *ConstTTLBuilder[K, V] {
Expand Down Expand Up @@ -231,6 +245,14 @@ func (b *ConstTTLBuilder[K, V]) Cost(costFunc func(key K, value V) uint32) *Cons
return b
}

// RemovalListener specifies a listener instance that caches should notify each time an entry is removed for any
// RemovalCause reason. The cache will invoke this listener in the background goroutine
// after the entry's removal operation has completed.
func (b *ConstTTLBuilder[K, V]) RemovalListener(removalListener func(key K, value V, cause RemovalCause)) *ConstTTLBuilder[K, V] {
b.setRemovalListener(removalListener)
return b
}

// Build creates a configured cache or
// returns an error if invalid parameters were passed to the builder.
func (b *ConstTTLBuilder[K, V]) Build() (Cache[K, V], error) {
Expand Down Expand Up @@ -270,6 +292,14 @@ func (b *VariableTTLBuilder[K, V]) Cost(costFunc func(key K, value V) uint32) *V
return b
}

// RemovalListener specifies a listener instance that caches should notify each time an entry is removed for any
// RemovalCause reason. The cache will invoke this listener in the background goroutine
// after the entry's removal operation has completed.
func (b *VariableTTLBuilder[K, V]) RemovalListener(removalListener func(key K, value V, cause RemovalCause)) *VariableTTLBuilder[K, V] {
b.setRemovalListener(removalListener)
return b
}

// Build creates a configured cache or
// returns an error if invalid parameters were passed to the builder.
func (b *VariableTTLBuilder[K, V]) Build() (CacheWithVariableTTL[K, V], error) {
Expand Down
4 changes: 4 additions & 0 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func TestBuilder_BuildSuccess(t *testing.T) {

cc, err := b.WithTTL(time.Minute).CollectStats().Cost(func(key int, value int) uint32 {
return 2
}).RemovalListener(func(key int, value int, cause RemovalCause) {
fmt.Println("const ttl")
}).Build()
if err != nil {
t.Fatalf("builded cache with error: %v", err)
Expand All @@ -103,6 +105,8 @@ func TestBuilder_BuildSuccess(t *testing.T) {

cv, err := b.WithVariableTTL().CollectStats().Cost(func(key int, value int) uint32 {
return 2
}).RemovalListener(func(key int, value int, cause RemovalCause) {
fmt.Println("variable ttl")
}).Build()
if err != nil {
t.Fatalf("builded cache with error: %v", err)
Expand Down
14 changes: 14 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ import (
"github.com/maypok86/otter/internal/core"
)

// RemovalCause the cause why a cached entry was removed.
type RemovalCause = core.RemovalCause

const (
// Explicit the entry was manually removed by the user.
Explicit = core.Explicit
// Replaced the entry itself was not actually removed, but its value was replaced by the user.
Replaced = core.Replaced
// Size the entry was evicted due to size constraints.
Size = core.Size
// Expired the entry's expiration timestamp has passed.
Expired = core.Expired
)

type baseCache[K comparable, V any] struct {
cache *core.Cache[K, V]
}
Expand Down
130 changes: 126 additions & 4 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ import (
)

func TestCache_Set(t *testing.T) {
const size = 100
c, err := MustBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build()
const size = 256
var mutex sync.Mutex
m := make(map[RemovalCause]int)
c, err := MustBuilder[int, int](size).
WithTTL(time.Minute).
CollectStats().
RemovalListener(func(key int, value int, cause RemovalCause) {
mutex.Lock()
m[cause]++
mutex.Unlock()
}).
Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}
Expand Down Expand Up @@ -72,6 +82,12 @@ func TestCache_Set(t *testing.T) {
if ratio != 1.0 {
t.Fatalf("cache hit ratio should be 1.0, but got %v", ratio)
}

mutex.Lock()
defer mutex.Unlock()
if len(m) != 1 || m[Replaced] != size {
t.Fatalf("cache was supposed to replace %d, but replaced %d entries", size, m[Replaced])
}
}

func TestCache_SetIfAbsent(t *testing.T) {
Expand Down Expand Up @@ -133,9 +149,16 @@ func TestCache_SetIfAbsent(t *testing.T) {

func TestCache_SetWithTTL(t *testing.T) {
size := 256
var mutex sync.Mutex
m := make(map[RemovalCause]int)
c, err := MustBuilder[int, int](size).
InitialCapacity(size).
WithTTL(time.Second).
RemovalListener(func(key int, value int, cause RemovalCause) {
mutex.Lock()
m[cause]++
mutex.Unlock()
}).
Build()
if err != nil {
t.Fatalf("can not create builder: %v", err)
Expand All @@ -158,7 +181,23 @@ func TestCache_SetWithTTL(t *testing.T) {
t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0)
}

cc, err := MustBuilder[int, int](size).WithVariableTTL().CollectStats().Build()
mutex.Lock()
if e := m[Expired]; len(m) != 1 || e != size {
mutex.Unlock()
t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, e)
}
mutex.Unlock()

m = make(map[RemovalCause]int)
cc, err := MustBuilder[int, int](size).
WithVariableTTL().
CollectStats().
RemovalListener(func(key int, value int, cause RemovalCause) {
mutex.Lock()
m[cause]++
mutex.Unlock()
}).
Build()
if err != nil {
t.Fatalf("can not create builder: %v", err)
}
Expand All @@ -183,13 +222,71 @@ func TestCache_SetWithTTL(t *testing.T) {
if misses := cc.Stats().Misses(); misses != int64(size) {
t.Fatalf("c.Stats().Misses() = %d, want = %d", misses, size)
}
mutex.Lock()
defer mutex.Unlock()
if len(m) != 1 || m[Expired] != size {
t.Fatalf("cache was supposed to expire %d, but expired %d entries", size, m[Expired])
}
}

func TestCache_Delete(t *testing.T) {
size := 256
var mutex sync.Mutex
m := make(map[RemovalCause]int)
c, err := MustBuilder[int, int](size).
InitialCapacity(size).
WithTTL(time.Hour).
RemovalListener(func(key int, value int, cause RemovalCause) {
mutex.Lock()
m[cause]++
mutex.Unlock()
}).
Build()
if err != nil {
t.Fatalf("can not create builder: %v", err)
}

for i := 0; i < size; i++ {
c.Set(i, i)
}

for i := 0; i < size; i++ {
if !c.Has(i) {
t.Fatalf("key should exists: %d", i)
}
}

for i := 0; i < size; i++ {
c.Delete(i)
}

for i := 0; i < size; i++ {
if c.Has(i) {
t.Fatalf("key should not exists: %d", i)
}
}

time.Sleep(time.Second)

mutex.Lock()
defer mutex.Unlock()
if len(m) != 1 || m[Explicit] != size {
t.Fatalf("cache was supposed to remove %d, but removed %d entries", size, m[Explicit])
}
}

func TestCache_DeleteByFunc(t *testing.T) {
size := 256
var mutex sync.Mutex
m := make(map[RemovalCause]int)
c, err := MustBuilder[int, int](size).
InitialCapacity(size).
WithTTL(time.Hour).
RemovalListener(func(key int, value int, cause RemovalCause) {
mutex.Lock()
m[cause]++
mutex.Unlock()
}).
Build()
if err != nil {
t.Fatalf("can not create builder: %v", err)
Expand All @@ -209,10 +306,28 @@ func TestCache_DeleteByFunc(t *testing.T) {
}
return true
})

time.Sleep(time.Second)

expected := size / 2
mutex.Lock()
defer mutex.Unlock()
if len(m) != 1 || m[Explicit] != expected {
t.Fatalf("cache was supposed to remove %d, but removed %d entries", expected, m[Explicit])
}
}

func TestCache_Ratio(t *testing.T) {
c, err := MustBuilder[uint64, uint64](100).CollectStats().Build()
var mutex sync.Mutex
m := make(map[RemovalCause]int)
c, err := MustBuilder[uint64, uint64](100).
CollectStats().
RemovalListener(func(key uint64, value uint64, cause RemovalCause) {
mutex.Lock()
m[cause]++
mutex.Unlock()
}).
Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}
Expand All @@ -231,6 +346,13 @@ func TestCache_Ratio(t *testing.T) {

t.Logf("actual size: %d, capacity: %d", c.Size(), c.Capacity())
t.Logf("actual: %.2f, optimal: %.2f", c.Stats().Ratio(), o.Ratio())

mutex.Lock()
defer mutex.Unlock()
t.Logf("evicted: %d", m[Size])
if len(m) != 1 || m[Size] <= 0 || m[Size] > 5000 {
t.Fatalf("cache was supposed to evict positive number of entries, but evicted %d entries", m[Size])
}
}

type optimal struct {
Expand Down
Loading

0 comments on commit 26dc222

Please sign in to comment.