Skip to content

Commit 3aa371a

Browse files
committed
Implement Update method
The update method allows atomically mutating a value in the map. If the key does not exist, the zero value for the value type is mutated and saved.
1 parent 85296bc commit 3aa371a

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

concurrent_map.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ func (m ConcurrentMap[K, V]) Set(key K, value V) {
7575
shard.Unlock()
7676
}
7777

78+
// Callback to update an element in the map.
79+
// If the element doesn't exist in the map, the parameter will receive the zero value for the value type.
80+
// The returned value will be stored in the map replacing the existing value.
81+
// Returning false for the second return value aborts the update.
82+
// It is called while lock is held, therefore it MUST NOT
83+
// try to access other keys in same map, as it can lead to deadlock since
84+
// Go sync.RWLock is not reentrant
85+
type UpdateCb[V any] func(exist bool, valueInMap V) (V, bool)
86+
87+
// Update an existing element using UpdateCb, assuming the key exists.
88+
// If it does not, the zero value for the value type is passed to the callback.
89+
// if the callback return false for the second return value, the map will not be updated.
90+
func (m ConcurrentMap[K, V]) Update(key K, cb UpdateCb[V]) (res V) {
91+
shard := m.GetShard(key)
92+
shard.Lock()
93+
v, ok := shard.items[key]
94+
res, update := cb(ok, v)
95+
if update {
96+
shard.items[key] = res
97+
}
98+
shard.Unlock()
99+
return res
100+
}
101+
78102
// Callback to return new element to be inserted into the map
79103
// It is called while lock is held, therefore it MUST NOT
80104
// try to access other keys in same map, as it can lead to deadlock since

concurrent_map_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,68 @@ func TestFnv32(t *testing.T) {
479479

480480
}
481481

482+
func TestUpdate(t *testing.T) {
483+
m := New[Animal]()
484+
lion := Animal{"lion"}
485+
486+
m.Set("safari", lion)
487+
m.Update("safari", func(exists bool, valueInMap Animal) (Animal, bool) {
488+
if !exists {
489+
t.Error("Update recieved false exists flag for existing key")
490+
}
491+
valueInMap.name = "tiger"
492+
return valueInMap, true
493+
})
494+
safari, ok := m.Get("safari")
495+
if safari.name != "tiger" || !ok {
496+
t.Error("Set, then Update failed")
497+
}
498+
499+
m.Update("marine", func(exists bool, valueInMap Animal) (Animal, bool) {
500+
if exists {
501+
t.Error("Update recieved exists flag for empty key")
502+
}
503+
if valueInMap.name != "" {
504+
t.Error("Update did not receive zero value for non existing key")
505+
}
506+
valueInMap.name = "whale"
507+
return valueInMap, true
508+
})
509+
marineAnimals, ok := m.Get("marine")
510+
if marineAnimals.name != "whale" || !ok {
511+
t.Error("Update on non-existing key failed")
512+
}
513+
514+
// return false to prevent updateing map
515+
m.Set("safari", lion)
516+
m.Update("safari", func(exists bool, valueInMap Animal) (Animal, bool) {
517+
if !exists {
518+
t.Error("Update recieved false exists flag for existing key")
519+
}
520+
valueInMap.name = "tiger"
521+
return valueInMap, false
522+
})
523+
safari, ok = m.Get("safari")
524+
if safari.name != "lion" || !ok {
525+
t.Error("Set, then aborting Update failed")
526+
}
527+
528+
m.Update("tundra", func(exists bool, valueInMap Animal) (Animal, bool) {
529+
if exists {
530+
t.Error("Update recieved exists flag for empty key")
531+
}
532+
if valueInMap.name != "" {
533+
t.Error("Update did not receive zero value for non existing key")
534+
}
535+
valueInMap.name = "moose"
536+
return valueInMap, false
537+
})
538+
_, ok = m.Get("tundra")
539+
if ok {
540+
t.Error("Update aborting on non-existing key failed")
541+
}
542+
}
543+
482544
func TestUpsert(t *testing.T) {
483545
dolphin := Animal{"dolphin"}
484546
whale := Animal{"whale"}

0 commit comments

Comments
 (0)