Skip to content

Commit

Permalink
Add CompareAndSwap method (#50)
Browse files Browse the repository at this point in the history
* Add CompareAndSwap method

* Remove misleading doc
  • Loading branch information
erni27 committed Jul 30, 2023
1 parent c2f6d27 commit 4312a64
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 61 deletions.
92 changes: 87 additions & 5 deletions imcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,33 @@ func (c *Cache[K, V]) Replace(key K, val V, exp Expiration) (present bool) {
return true
}

// Number is a constraint that permits any numeric type except complex ones.
//
// Deprecated: Number constraint is deprecated. It is easy to write your own
// constraint. imcache's goal is to be simple. Creating artifical types
// or functions that are not even needed conflicts with this goal.
type Number interface {
~float32 | ~float64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Increment increments the given number by one.
//
// Deprecated: Increment function is deprecated. It is easy to write your own
// function. imcache's goal is to be simple. Creating artifical types
// or functions that are not even needed conflicts with this goal.
func Increment[V Number](old V) V {
return old + 1
}

// Decrement decrements the given number by one.
//
// Deprecated: Decrement function is deprecated. It is easy to write your own
// function. imcache's goal is to be simple. Creating artifical types
// or functions that are not even needed conflicts with this goal.
func Decrement[V Number](old V) V {
return old - 1
}

// ReplaceWithFunc replaces the value for the given key with the result
// of the given function that takes the current value as an argument.
// It returns true if the value is present and replaced, otherwise it returns
Expand All @@ -353,14 +380,15 @@ func (c *Cache[K, V]) Replace(key K, val V, exp Expiration) (present bool) {
// If you want to replace the value with a new value not depending on
// the current value, use the Replace method instead.
//
// imcache provides the Increment and Decrement functions that can be used as f
// to increment or decrement the current numeric type value.
//
// Example:
// Example showing how to increment the value by 1 using ReplaceWithFunc:
//
// var c imcache.Cache[string, int32]
// c.Set("foo", 997, imcache.WithNoExpiration())
// _ = c.ReplaceWithFunc("foo", imcache.Increment[int32], imcache.WithNoExpiration())
// _ = c.ReplaceWithFunc(
// "foo",
// func(current int32) int32 { return current + 1 },
// imcache.WithNoExpiration(),
// )
func (c *Cache[K, V]) ReplaceWithFunc(key K, f func(current V) (new V), exp Expiration) (present bool) {
now := time.Now()
c.mu.Lock()
Expand Down Expand Up @@ -449,6 +477,50 @@ func (c *Cache[K, V]) ReplaceKey(old, new K, exp Expiration) (present bool) {
return true
}

// CompareAndSwap replaces the value for the given key if the current value
// is equal to the expected value.
//
// Equality is defined by the given compare function.
//
// If it encounters an expired entry, the expired entry is evicted.
func (c *Cache[K, V]) CompareAndSwap(key K, expected, new V, compare func(V, V) bool, exp Expiration) (swapped, present bool) {
now := time.Now()
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return false, false
}
currentEntry, ok := c.m[key]
if !ok {
c.mu.Unlock()
return false, false
}
c.queue.Remove(currentEntry.node)
if currentEntry.HasExpired(now) {
delete(c.m, key)
c.mu.Unlock()
if c.onEviction != nil {
go c.onEviction(key, currentEntry.val, EvictionReasonExpired)
}
return false, false
}
if !compare(currentEntry.val, expected) {
c.queue.Add(currentEntry.node)
c.mu.Unlock()
return false, true
}
newEntry := entry[K, V]{val: new, node: currentEntry.node}
exp.apply(&newEntry.exp)
newEntry.SetDefaultOrNothing(now, c.defaultExp, c.sliding)
c.queue.Add(newEntry.node)
c.m[key] = newEntry
c.mu.Unlock()
if c.onEviction != nil {
go c.onEviction(key, currentEntry.val, EvictionReasonReplaced)
}
return true, true
}

// Remove removes the cache entry for the given key.
//
// It returns true if the entry is present and removed,
Expand Down Expand Up @@ -922,6 +994,16 @@ func (s *Sharded[K, V]) ReplaceKey(old, new K, exp Expiration) (present bool) {
return true
}

// CompareAndSwap replaces the value for the given key if the current value
// is equal to the expected value.
//
// Equality is defined by the given compare function.
//
// If it encounters an expired entry, the expired entry is evicted.
func (s *Sharded[K, V]) CompareAndSwap(key K, expected, new V, compare func(V, V) bool, exp Expiration) (swapped, present bool) {
return s.shard(key).CompareAndSwap(key, expected, new, compare, exp)
}

// Remove removes the cache entry for the given key.
//
// It returns true if the entry is present and removed,
Expand Down
56 changes: 25 additions & 31 deletions imcache_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package imcache

import (
"fmt"
"math/rand"
"sync"
"testing"
"time"
)

func init() {
rand.Seed(time.Now().UnixNano())
}

type token struct {
ID int `json:"id"`
}
Expand All @@ -25,7 +19,7 @@ func BenchmarkCache_Get(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = v
}
}
Expand All @@ -42,7 +36,7 @@ func BenchmarkSharded_Get(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = v
}
}
Expand All @@ -59,7 +53,7 @@ func BenchmarkCache_Get_MaxEntriesLimit(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = v
}
}
Expand All @@ -76,7 +70,7 @@ func BenchmarkSharded_Get_MaxEntriesLimit(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = v
}
}
Expand All @@ -95,7 +89,7 @@ func BenchmarkMap_Get(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
mu.Lock()
v, ok := m[fmt.Sprintf("key-%d", rand.Intn(b.N))]
v, ok := m[fmt.Sprintf("key-%d", random.Intn(b.N))]
if ok {
_ = (token)(v)
}
Expand All @@ -113,7 +107,7 @@ func BenchmarkCache_Get_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = (token)(v)
}
}
Expand All @@ -132,7 +126,7 @@ func BenchmarkSharded_Get_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = (token)(v)
}
}
Expand All @@ -151,7 +145,7 @@ func BenchmarkCache_Get_MaxEntriesLimit_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = (token)(v)
}
}
Expand All @@ -170,7 +164,7 @@ func BenchmarkSharded_Get_MaxEntriesLimit_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if v, ok := c.Get(fmt.Sprintf("key-%d", rand.Intn(b.N))); ok {
if v, ok := c.Get(fmt.Sprintf("key-%d", random.Intn(b.N))); ok {
_ = (token)(v)
}
}
Expand All @@ -191,7 +185,7 @@ func BenchmarkMap_Get_Parallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
v, ok := m[fmt.Sprintf("key-%d", rand.Intn(b.N))]
v, ok := m[fmt.Sprintf("key-%d", random.Intn(b.N))]
if ok {
_ = (token)(v)
}
Expand All @@ -206,7 +200,7 @@ func BenchmarkCache_Set(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", rand.Intn(b.N)), token{ID: i}, WithNoExpiration())
c.Set(fmt.Sprintf("key-%d", random.Intn(b.N)), token{ID: i}, WithNoExpiration())
}
}

Expand All @@ -218,7 +212,7 @@ func BenchmarkSharded_Set(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", rand.Intn(b.N)), token{ID: i}, WithNoExpiration())
c.Set(fmt.Sprintf("key-%d", random.Intn(b.N)), token{ID: i}, WithNoExpiration())
}
})
}
Expand All @@ -230,7 +224,7 @@ func BenchmarkCache_Set_MaxEntriesLimit(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", rand.Intn(b.N)), token{ID: i}, WithNoExpiration())
c.Set(fmt.Sprintf("key-%d", random.Intn(b.N)), token{ID: i}, WithNoExpiration())
}
}

Expand All @@ -242,7 +236,7 @@ func BenchmarkSharded_Set_MaxEntriesLimit(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.Set(fmt.Sprintf("key-%d", rand.Intn(b.N)), token{ID: i}, WithNoExpiration())
c.Set(fmt.Sprintf("key-%d", random.Intn(b.N)), token{ID: i}, WithNoExpiration())
}
})
}
Expand All @@ -256,7 +250,7 @@ func BenchmarkMap_Set(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
mu.Lock()
m[fmt.Sprintf("key-%d", rand.Intn(b.N))] = token{ID: i}
m[fmt.Sprintf("key-%d", random.Intn(b.N))] = token{ID: i}
mu.Unlock()
}
}
Expand All @@ -268,7 +262,7 @@ func BenchmarkCache_Set_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}
})
Expand All @@ -283,7 +277,7 @@ func BenchmarkSharded_Set_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}
})
Expand All @@ -298,7 +292,7 @@ func BenchmarkCache_Set_MaxEntriesLimit_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}
})
Expand All @@ -313,7 +307,7 @@ func BenchmarkSharded_Set_MaxEntriesLimit_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
c.Set(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
}
})
Expand All @@ -329,7 +323,7 @@ func BenchmarkMap_Set_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
mu.Lock()
m[fmt.Sprintf("key-%d", i)] = token{ID: i}
mu.Unlock()
Expand All @@ -343,7 +337,7 @@ func BenchmarkCache_GetOrSet(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
i := rand.Intn(b.N)
i := random.Intn(b.N)
v, ok := c.GetOrSet(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
if ok {
_ = (token)(v)
Expand All @@ -359,7 +353,7 @@ func BenchmarkSharded_GetOrSet(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
i := rand.Intn(b.N)
i := random.Intn(b.N)
v, ok := c.GetOrSet(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
if ok {
_ = (token)(v)
Expand All @@ -376,7 +370,7 @@ func BenchmarkCache_GetOrSet_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
v, ok := c.GetOrSet(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
if ok {
_ = (token)(v)
Expand All @@ -394,7 +388,7 @@ func Benchmark_Sharded_GetOrSet_Parallel(b *testing.B) {
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := rand.Intn(b.N)
i := random.Intn(b.N)
v, ok := c.GetOrSet(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration())
if ok {
_ = (token)(v)
Expand All @@ -414,7 +408,7 @@ func BenchmarkCache_Replace(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
i := rand.Intn(b.N)
i := random.Intn(b.N)
if ok := c.Replace(fmt.Sprintf("key-%d", i), token{ID: i}, WithNoExpiration()); !ok {
b.Fatalf("Replace(_, _, _) = %t", ok)
}
Expand Down

0 comments on commit 4312a64

Please sign in to comment.