Skip to content

Commit

Permalink
Merge pull request #12 from arthhhhh23/feat/setifpresent
Browse files Browse the repository at this point in the history
feat: implement SetIfPresent
  • Loading branch information
C-Pro authored Oct 18, 2024
2 parents 0242bfa + dda3ec5 commit b63990f
Show file tree
Hide file tree
Showing 17 changed files with 605 additions and 168 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea/*
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Implementations are as simple as possible to be predictable in max latency, memo

## Examples

Interface is quite simple with five methods: `Set`, `Get`, `Del`, `Snapshot` and `Len`. Here's a quick example for a ring buffer holding 10k records.
Interface is quite simple with six methods: `Set`, `Get`, `Del`, `SetIfPresent`, `Snapshot` and `Len`. Here's a quick example for a ring buffer holding 10k records.

```go
package main
Expand All @@ -37,7 +37,18 @@ func main() {
fmt.Println(err)
return
}


// will update the value associated to key 1
previousVal, updated := c.SetIfPresent(1, "two")
// will print "one"
fmt.Println(previousVal)
// will print "true"
fmt.Println(updated)

// will not have any effect
c.SetIfPresent(2, "dua")

// will print 2
fmt.Println(v)
}
```
Expand Down
101 changes: 100 additions & 1 deletion bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ func benchmarkSet(c Geche[string, string], testData []testCase, b *testing.B) {
}
}

func benchmarkSetIfPresent(c Geche[string, string], testKeys []string, b *testing.B) {
for i := 0; i < b.N; i++ {
c.SetIfPresent(testKeys[i%len(testKeys)], "value")
}
}

func benchmarkFuzz(
c Geche[string, string],
testData []testCase,
Expand Down Expand Up @@ -112,6 +118,99 @@ func BenchmarkSet(b *testing.B) {
})
}

func BenchmarkSetIfPresentOnlyHits(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tab := []struct {
name string
imp Geche[string, string]
}{
{
"MapCache",
NewMapCache[string, string](),
},
{
"StringCache",
newStringCache(),
},
{
"UnsafeCache",
newUnsafeCache(),
},
{
"MapTTLCache",
NewMapTTLCache[string, string](ctx, time.Minute, time.Minute),
},
{
"RingBuffer",
NewRingBuffer[string, string](1000000),
},
{
"KVMapCache",
NewKV[string](NewMapCache[string, string]()),
},
}

testKeys := make([]string, 10_000_000)
for i := 0; i < len(testKeys); i++ {
testKeys[i] = strconv.Itoa(i)
}

b.ResetTimer()

for _, c := range tab {
b.Run(c.name, func(b *testing.B) {
benchmarkSetIfPresent(c.imp, testKeys, b)
})
}
}

func BenchmarkSetIfPresentOnlyMisses(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

tab := []struct {
name string
imp Geche[string, string]
}{
{
"MapCache",
NewMapCache[string, string](),
},
{
"StringCache",
newStringCache(),
},
{
"UnsafeCache",
newUnsafeCache(),
},
{
"MapTTLCache",
NewMapTTLCache[string, string](ctx, time.Minute, time.Minute),
},
{
"RingBuffer",
NewRingBuffer[string, string](1000000),
},
{
"KVMapCache",
NewKV[string](NewMapCache[string, string]()),
},
}

b.ResetTimer()

for _, c := range tab {
b.Run(c.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
c.imp.SetIfPresent("absent", "never set")
}
})
}
}

// BenchmarkEverything performs different operations randomly.
// Ratio for get/set/del is 90/5/5
func BenchmarkEverything(b *testing.B) {
Expand Down Expand Up @@ -203,7 +302,7 @@ func BenchmarkKVListByPrefix(b *testing.B) {
c := NewKV[string](NewMapCache[string, string]())
keys := make([]string, 100_000)
for i := 0; i < 100_000; i++ {
l := rand.Intn(15)+15
l := rand.Intn(15) + 15
unique := randomString(l)
keys[i] = unique
for j := 0; j < 10; j++ {
Expand Down
34 changes: 34 additions & 0 deletions common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,38 @@ func testSetGet(t *testing.T, imp Geche[string, string]) {
}
}

func testSetThenSetIfPresentThenGet(t *testing.T, imp Geche[string, string]) {
imp.Set("key", "value")
old, inserted := imp.SetIfPresent("key", "value2")
if !inserted {
t.Errorf("expected SetIfPresent to insert a new value for existing key")
}

if old != "value" {
t.Errorf("expected old value %q to be returned from SetIfPresent, got %q", "value", old)
}

val, err := imp.Get("key")
if err != nil {
t.Errorf("unexpected error in Get: %v", err)
}

if val != "value2" {
t.Errorf("expected value %q, got %q", "value2", val)
}
}

func testSetIfPresentThenGet(t *testing.T, imp Geche[string, string]) {
if _, inserted := imp.SetIfPresent("key", "value"); inserted {
t.Errorf("expected SetIfPresent to not insert a new value for non-existing key")
}

val, err := imp.Get("key")
if err == nil {
t.Errorf("expected error not found in Get: %v", val)
}
}

func testGetNonExist(t *testing.T, imp Geche[string, string]) {
_, err := imp.Get("key")
if err != ErrNotFound {
Expand Down Expand Up @@ -200,6 +232,8 @@ func TestCommon(t *testing.T) {
{"Del", testDel},
{"DelOdd", testDelOdd},
{"SnapshotLen", testSnapshotLen},
{"SetSetIfPresentGet", testSetThenSetIfPresentThenGet},
{"SetIfPresentGet", testSetIfPresentThenGet},
}
for _, ci := range caches {
for _, tc := range tab {
Expand Down
2 changes: 2 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var ErrNotFound = errors.New("not found")
// Geche interface is a common interface for all cache implementations.
type Geche[K comparable, V any] interface {
Set(K, V)
// SetIfPresent sets the kv only if the key was already present, and returns the previous value (if any) and whether the insertion was performed
SetIfPresent(K, V) (V, bool)
Get(K) (V, error)
Del(K) error
Snapshot() map[K]V
Expand Down
23 changes: 23 additions & 0 deletions dummy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ func (s *stringCache) Set(key, value string) {
s.data[key] = value
}

func (s *stringCache) SetIfPresent(key, value string) (string, bool) {
s.mux.Lock()
defer s.mux.Unlock()

old, ok := s.data[key]
if !ok {
return "", false
}

s.data[key] = value
return old, true
}

func (s *stringCache) Get(key string) (string, error) {
s.mux.RLock()
defer s.mux.RUnlock()
Expand Down Expand Up @@ -71,6 +84,16 @@ func (u *unsafeCache) Set(key, value string) {
u.data[key] = value
}

func (u *unsafeCache) SetIfPresent(key, value string) (string, bool) {
old, err := u.Get(key)
if err != nil {
return "", false
}

u.Set(key, value)
return old, true
}

func (u *unsafeCache) Get(key string) (string, error) {
v, ok := u.data[key]
if !ok {
Expand Down
Loading

0 comments on commit b63990f

Please sign in to comment.