Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,42 @@ func testSetIfPresentThenGet(t *testing.T, imp Geche[string, string]) {
}
}

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

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

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

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

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

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

if val != "value" {
t.Errorf("expected inserted value %q to be found, but got %q", "value", val)
}
}

func testGetNonExist(t *testing.T, imp Geche[string, string]) {
_, err := imp.Get("key")
if err != ErrNotFound {
Expand Down Expand Up @@ -236,6 +272,8 @@ func TestCommon(t *testing.T) {
{"SnapshotLen", testSnapshotLen},
{"SetSetIfPresentGet", testSetThenSetIfPresentThenGet},
{"SetIfPresentGet", testSetIfPresentThenGet},
{"SetSetIfAbsentGet", testSetThenSetIfAbsentThenGet},
{"SetIfAbsentGet", testSetIfAbsentThenGet},
}
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 @@ -11,6 +11,8 @@ 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)
// SetIfAbsent sets the kv only if the key didn't exist yet, and returns the existing value (if any) and whether the insertion was performed
SetIfAbsent(K, V) (V, bool)
Get(K) (V, error)
Del(K) error
Snapshot() map[K]V
Expand Down
13 changes: 13 additions & 0 deletions kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ func (kv *KV[V]) SetIfPresent(key string, value V) (V, bool) {
return previousVal, false
}

func (kv *KV[V]) SetIfAbsent(key string, value V) (V, bool) {
kv.mux.Lock()
defer kv.mux.Unlock()

previousVal, err := kv.data.Get(key)
if err == nil {
return previousVal, false
}

kv.set(key, value)
return previousVal, true
}

// Set key-value pair while updating the trie.
// Panics if key is empty.
func (kv *KV[V]) Set(key string, value V) {
Expand Down
13 changes: 13 additions & 0 deletions kv_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ func (kv *KVCache[K, V]) SetIfPresent(key K, value V) (V, bool) {
return kv.zero, false
}

func (kv *KVCache[K, V]) SetIfAbsent(key K, value V) (V, bool) {
kv.mux.Lock()
defer kv.mux.Unlock()

old, found := kv.get(key)
if found {
return old, false
}

kv.insert(key, value)
return kv.zero, true
}

// Get retrieves a value by key.
func (kv *KVCache[K, V]) Get(key K) (V, error) {
kv.mux.RLock()
Expand Down
32 changes: 32 additions & 0 deletions kv_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,38 @@ func TestKVCacheSetIfPresent(t *testing.T) {
}
}

func TestKVCacheSetIfAbsent(t *testing.T) {
cache := NewKVCache[string, string]()
cache.Set("a", "test2")

old, inserted := cache.SetIfAbsent("a", "test5")
if inserted {
t.Errorf("key \"a\" is present in cache, SetIfAbsent should return false")
}

if old != "test2" {
t.Errorf("expected %q, got %q", "test2", old)
}

old, inserted = cache.SetIfAbsent("b", "test6")
if !inserted {
t.Errorf("key \"b\" is absent from cache, SetIfAbsent should return true")
}

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

old, inserted = cache.SetIfAbsent("b", "test7")
if inserted {
t.Errorf("key \"b\" is present in cache, SetIfAbsent should return false")
}

if old != "test6" {
t.Errorf("previous value should be %q, but got %q", "test6", old)
}
}

func TestKVCacheSetIfPresentConcurrent(t *testing.T) {
cache := NewKVCache[string, string]()
cache.Set("a", "startA")
Expand Down
28 changes: 28 additions & 0 deletions kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ type MockErrCache struct{}

func (m *MockErrCache) Set(key string, value string) {}
func (m *MockErrCache) SetIfPresent(key string, value string) (string, bool) { return "", false }
func (m *MockErrCache) SetIfAbsent(key string, value string) (string, bool) { return "", true }
func (m *MockErrCache) Del(key string) error { return nil }
func (m *MockErrCache) Snapshot() map[string]string { return nil }
func (m *MockErrCache) Len() int { return 0 }
Expand Down Expand Up @@ -762,6 +763,33 @@ func TestSetIfPresent(t *testing.T) {
}
}

func TestSetIfAbsent(t *testing.T) {
kv := NewKV[string](NewMapCache[string, string]())
kv.Set("a", "test2")

old, inserted := kv.SetIfAbsent("a", "test5")
if inserted {
t.Errorf("key \"a\" is present in kv, SetIfAbsent should return false")
}

if old != "test2" {
t.Errorf("old value is %q, SetIfAbsent should return true", old)
}

old, inserted = kv.SetIfAbsent("d", "test3")
if !inserted {
t.Errorf("key \"d\" is not present in kv, SetIfAbsent should return true")
}

if old != "" {
t.Errorf("there was no old value for \"d\", SetIfAbsent should have returned zero-value for old value")
}

if _, inserted := kv.SetIfAbsent("d", "test3"); inserted {
t.Errorf("key \"d\" is present in kv, SetIfPresent should return false")
}
}

func TestSetIfPresentConcurrent(t *testing.T) {
kv := NewKV[string](NewMapCache[string, string]())
kv.Set("a", "startA")
Expand Down
10 changes: 10 additions & 0 deletions locker.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ func (tx *Tx[K, V]) SetIfPresent(key K, value V) (V, bool) {
return tx.cache.SetIfPresent(key, value)
}

func (tx *Tx[K, V]) SetIfAbsent(key K, value V) (V, bool) {
if atomic.LoadInt32(&tx.unlocked) == 1 {
panic("cannot use unlocked transaction")
}
if !tx.writable {
panic("cannot set in read-only transaction")
}
return tx.cache.SetIfAbsent(key, value)
}

// Get value by key from the underlying sharded cache.
func (tx *Tx[K, V]) Get(key K) (V, error) {
if atomic.LoadInt32(&tx.unlocked) == 1 {
Expand Down
8 changes: 8 additions & 0 deletions locker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func TestLockerRPanics(t *testing.T) {
t.Errorf("expected panic (SetIfPresent on RLocked)")
}

if !panics(func() { tx.SetIfAbsent(1, 1) }) {
t.Errorf("expected panic (SetIfAbsent on RLocked)")
}

tx.Unlock()
if !panics(func() { tx.Unlock() }) {
t.Errorf("expected panic (Unlock on already unlocked)")
Expand Down Expand Up @@ -125,6 +129,10 @@ func TestLockerRPanics(t *testing.T) {
if !panics(func() { tx.SetIfPresent(1, 1) }) {
t.Errorf("expected panic (SetIfPresent on already unlocked)")
}

if !panics(func() { tx.SetIfAbsent(1, 1) }) {
t.Errorf("expected panic (SetIfAbsent on already unlocked)")
}
}

func TestLockerRWPanics(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ func (c *MapCache[K, V]) SetIfPresent(key K, value V) (V, bool) {
return old, false
}

func (c *MapCache[K, V]) SetIfAbsent(key K, value V) (V, bool) {
c.mux.Lock()
defer c.mux.Unlock()

old, ok := c.data[key]
if ok {
return old, false
}

c.data[key] = value
return old, true
Copy link
Copy Markdown

@judit-nugroho judit-nugroho Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use value instead bang Arthur?

Suggested change
return old, true
return value, true

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment in the inteface says:
// SetIfAbsent sets the kv only if the key didn't exist yet, and returns the existing value (if any) and whether the insertion was performed

We insert only when previous value did not exist, so old would be zero value in this case, which is consistent with other implementations.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaa isee, got it thanks bang Serger for the explanation

}

// Get returns ErrNotFound if key does not exist in the cache.
func (c *MapCache[K, V]) Get(key K) (V, error) {
c.mux.RLock()
Expand Down
13 changes: 13 additions & 0 deletions map_ttl.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ func (c *MapTTLCache[K, V]) SetIfPresent(key K, value V) (V, bool) {
return old, false
}

func (c *MapTTLCache[K, V]) SetIfAbsent(key K, value V) (V, bool) {
c.mux.Lock()
defer c.mux.Unlock()

old, err := c.get(key)
if err == nil {
return old, false
}

c.set(key, value)
return old, true
}

// Get returns ErrNotFound if key is not found in the cache or record is outdated.
func (c *MapTTLCache[K, V]) Get(key K) (V, error) {
c.mux.RLock()
Expand Down
17 changes: 17 additions & 0 deletions ring.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func NewRingBuffer[K comparable, V any](size int) *RingBuffer[K, V] {
func (c *RingBuffer[K, V]) Set(key K, value V) {
c.mux.Lock()
defer c.mux.Unlock()
c.set(key, value)
}

func (c *RingBuffer[K, V]) set(key K, value V) {
// Remove the key which value we are overwriting
// from the map. GC does not cleanup preallocated map,
// so no pressure here.
Expand Down Expand Up @@ -72,6 +76,19 @@ func (c *RingBuffer[K, V]) SetIfPresent(key K, value V) (V, bool) {
return c.zeroV, false
}

func (c *RingBuffer[K, V]) SetIfAbsent(key K, value V) (V, bool) {
c.mux.Lock()
defer c.mux.Unlock()

i, present := c.index[key]
if present {
return c.data[i].V, false
}

c.set(key, value)
return c.zeroV, true
}

// Get returns cached value for the key, or ErrNotFound if the key does not exist.
func (c *RingBuffer[K, V]) Get(key K) (V, error) {
c.mux.RLock()
Expand Down
4 changes: 4 additions & 0 deletions shard.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func (s *Sharded[K, V]) SetIfPresent(key K, value V) (V, bool) {
return s.shards[s.mapper.Map(key, s.N)].SetIfPresent(key, value)
}

func (s *Sharded[K, V]) SetIfAbsent(key K, value V) (V, bool) {
return s.shards[s.mapper.Map(key, s.N)].SetIfAbsent(key, value)
}

// Get value by key from the underlying sharded cache.
func (s *Sharded[K, V]) Get(key K) (V, error) {
return s.shards[s.mapper.Map(key, s.N)].Get(key)
Expand Down
4 changes: 4 additions & 0 deletions updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func (u *Updater[K, V]) SetIfPresent(key K, value V) (V, bool) {
return u.cache.SetIfPresent(key, value)
}

func (u *Updater[K, V]) SetIfAbsent(key K, value V) (V, bool) {
return u.cache.SetIfAbsent(key, value)
}

// Get returns value from the cache. If the value is not in the cache,
// it calls updateFn to get the value and update the cache first.
// Since updateFn can return error, Get is not guaranteed to always return the value.
Expand Down