From 1c11576655b69271197a95da1eec510eb3ae06a8 Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Fri, 2 Oct 2026 16:01:39 +0300 Subject: [PATCH 1/2] fix: prevent stale deleted elements from causing lost items in concurrent GetOrInsert Under concurrent GetOrInsert operations, an item could be inserted adjacent to a concurrently deleted element or a deleted element could be matched during list search, leading to elements being omitted during Range traversal and counter discrepancies. Add validation for deleted flags during list traversal, element search, and CAS insertion. Add concurrent regression test TestGetOrInsertConcurrentIssue81. Fixes #81 --- hashmap.go | 4 ++++ hashmap_test.go | 24 ++++++++++++++++++++++++ list.go | 15 +++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/hashmap.go b/hashmap.go index 3bf83c1..c0b8694 100644 --- a/hashmap.go +++ b/hashmap.go @@ -48,6 +48,10 @@ func (m *Map[Key, Value]) Get(key Key) (Value, bool) { hash := m.hasher(key) for element := m.store.Load().item(hash); element != nil; element = element.Next() { + if element.deleted.Load() != 0 { + continue + } + if element.keyHash == hash && element.key == key { return element.Value(), true } diff --git a/hashmap_test.go b/hashmap_test.go index f0eb327..e558813 100644 --- a/hashmap_test.go +++ b/hashmap_test.go @@ -482,3 +482,27 @@ func TestGetOrInsertHangIssue67(_ *testing.T) { wg.Wait() } + +func TestGetOrInsertConcurrentIssue81(t *testing.T) { + m := New[int, bool]() + var wg sync.WaitGroup + n := 1000 + + for i := 0; i < n; i++ { + wg.Add(1) + go func(val int) { + defer wg.Done() + m.GetOrInsert(val, true) + }(i) + } + wg.Wait() + + count := 0 + m.Range(func(key int, value bool) bool { + count++ + return true + }) + + assert.Equal(t, n, count) + assert.Equal(t, uintptr(n), m.Len()) +} diff --git a/list.go b/list.go index 596b2cf..e6e0626 100644 --- a/list.go +++ b/list.go @@ -76,7 +76,7 @@ func (l *List[Key, Value]) Delete(element *ListElement[Key, Value]) { } func (l *List[Key, Value]) search(searchStart *ListElement[Key, Value], hash uintptr, key Key) (left, found, right *ListElement[Key, Value]) { - if searchStart != nil && hash < searchStart.keyHash { // key would remain left from item? { + if searchStart != nil && (hash < searchStart.keyHash || searchStart.deleted.Load() != 0) { // key would remain left from item or item deleted? searchStart = nil // start search at head } @@ -92,7 +92,9 @@ func (l *List[Key, Value]) search(searchStart *ListElement[Key, Value], hash uin for { if hash == found.keyHash && key == found.key { // key hash already exists, compare keys - return nil, found, nil + if found.deleted.Load() == 0 { + return nil, found, nil + } } if hash < found.keyHash { // new item needs to be inserted before the found value @@ -116,12 +118,21 @@ func (l *List[Key, Value]) insertAt(element, left, right *ListElement[Key, Value left = l.head } + if left != l.head && left.deleted.Load() != 0 { + return false // left was deleted concurrently + } + element.next.Store(right) if !left.next.CompareAndSwap(right, element) { return false // item was modified concurrently } + if left != l.head && left.deleted.Load() != 0 { + element.deleted.Store(1) + return false // left was deleted concurrently + } + l.count.Add(1) return true } From 899b405cc9117699289409406fa8100d6a417c4e Mon Sep 17 00:00:00 2001 From: AdamMagued Date: Sat, 3 Oct 2026 03:09:27 +0300 Subject: [PATCH 2/2] test: resolve intrange and unused parameter linter warnings --- hashmap_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hashmap_test.go b/hashmap_test.go index e558813..5d356aa 100644 --- a/hashmap_test.go +++ b/hashmap_test.go @@ -488,7 +488,7 @@ func TestGetOrInsertConcurrentIssue81(t *testing.T) { var wg sync.WaitGroup n := 1000 - for i := 0; i < n; i++ { + for i := range n { wg.Add(1) go func(val int) { defer wg.Done() @@ -498,7 +498,7 @@ func TestGetOrInsertConcurrentIssue81(t *testing.T) { wg.Wait() count := 0 - m.Range(func(key int, value bool) bool { + m.Range(func(_ int, value bool) bool { count++ return true })