Skip to content

Commit

Permalink
[#63] Add deletion listener
Browse files Browse the repository at this point in the history
  • Loading branch information
maypok86 committed Mar 9, 2024
1 parent 80a12be commit f2b403b
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 49 deletions.
50 changes: 40 additions & 10 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ var (
)

type baseOptions[K comparable, V any] struct {
capacity int
initialCapacity int
statsEnabled bool
withCost bool
costFunc func(key K, value V) uint32
capacity int
initialCapacity int
statsEnabled bool
withCost bool
costFunc func(key K, value V) uint32
deletionListener func(key K, value V, cause DeletionCause)
}

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]) setDeletionListener(deletionListener func(key K, value V, cause DeletionCause)) {
o.deletionListener = deletionListener
}

func (o *baseOptions[K, V]) validate() error {
if o.initialCapacity <= 0 && o.initialCapacity != unsetCapacity {
return ErrIllegalInitialCapacity
Expand All @@ -73,11 +78,12 @@ func (o *baseOptions[K, V]) toConfig() core.Config[K, V] {
initialCapacity = &o.initialCapacity
}
return core.Config[K, V]{
Capacity: o.capacity,
InitialCapacity: initialCapacity,
StatsEnabled: o.statsEnabled,
CostFunc: o.costFunc,
WithCost: o.withCost,
Capacity: o.capacity,
InitialCapacity: initialCapacity,
StatsEnabled: o.statsEnabled,
CostFunc: o.costFunc,
WithCost: o.withCost,
DeletionListener: o.deletionListener,
}
}

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
}

// DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any
// DeletionCause cause. The cache will invoke this listener in the background goroutine
// after the entry's deletion operation has completed.
func (b *Builder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *Builder[K, V] {
b.setDeletionListener(deletionListener)
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
}

// DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any
// DeletionCause cause. The cache will invoke this listener in the background goroutine
// after the entry's deletion operation has completed.
func (b *ConstTTLBuilder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *ConstTTLBuilder[K, V] {
b.setDeletionListener(deletionListener)
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
}

// DeletionListener specifies a listener instance that caches should notify each time an entry is deleted for any
// DeletionCause cause. The cache will invoke this listener in the background goroutine
// after the entry's deletion operation has completed.
func (b *VariableTTLBuilder[K, V]) DeletionListener(deletionListener func(key K, value V, cause DeletionCause)) *VariableTTLBuilder[K, V] {
b.setDeletionListener(deletionListener)
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
}).DeletionListener(func(key int, value int, cause DeletionCause) {
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
}).DeletionListener(func(key int, value int, cause DeletionCause) {
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"
)

// DeletionCause the cause why a cached entry was deleted.
type DeletionCause = core.DeletionCause

const (
// Explicit the entry was manually deleted by the user.
Explicit = core.Explicit
// Replaced the entry itself was not actually deleted, 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[DeletionCause]int)
c, err := MustBuilder[int, int](size).
WithTTL(time.Minute).
CollectStats().
DeletionListener(func(key int, value int, cause DeletionCause) {
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[DeletionCause]int)
c, err := MustBuilder[int, int](size).
InitialCapacity(size).
WithTTL(time.Second).
DeletionListener(func(key int, value int, cause DeletionCause) {
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[DeletionCause]int)
cc, err := MustBuilder[int, int](size).
WithVariableTTL().
CollectStats().
DeletionListener(func(key int, value int, cause DeletionCause) {
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[DeletionCause]int)
c, err := MustBuilder[int, int](size).
InitialCapacity(size).
WithTTL(time.Hour).
DeletionListener(func(key int, value int, cause DeletionCause) {
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 delete %d, but deleted %d entries", size, m[Explicit])
}
}

func TestCache_DeleteByFunc(t *testing.T) {
size := 256
var mutex sync.Mutex
m := make(map[DeletionCause]int)
c, err := MustBuilder[int, int](size).
InitialCapacity(size).
WithTTL(time.Hour).
DeletionListener(func(key int, value int, cause DeletionCause) {
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 delete %d, but deleted %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[DeletionCause]int)
c, err := MustBuilder[uint64, uint64](100).
CollectStats().
DeletionListener(func(key uint64, value uint64, cause DeletionCause) {
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 f2b403b

Please sign in to comment.