Skip to content

Commit

Permalink
Fix too agressive Map/MapOf shrinking on deletion leading to out of r…
Browse files Browse the repository at this point in the history
…ange panic (#113)
  • Loading branch information
puzpuzpuz authored Nov 14, 2023
1 parent 42e3390 commit 1386eb4
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 2 deletions.
2 changes: 1 addition & 1 deletion map.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ func (m *Map) resize(knownTable *mapTable, hint mapResizeHint) {
newTable = newMapTable(tableLen << 1)
case mapShrinkHint:
shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction)
if table.sumSize() <= shrinkThreshold {
if tableLen > minMapTableLen && table.sumSize() <= shrinkThreshold {
// Shrink the table with factor of 2.
atomic.AddInt64(&m.totalShrinks, 1)
newTable = newMapTable(tableLen >> 1)
Expand Down
31 changes: 31 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,37 @@ func TestMapStoreThenLoadAndDelete(t *testing.T) {
}
}

func TestMapStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T) {
const numEntries = 1000
m := NewMap()
for i := 0; i < numEntries; i++ {
m.Store(strconv.Itoa(i), i)
}

cdone := make(chan bool)
go func() {
for i := 0; i < numEntries; i++ {
m.Delete(strconv.Itoa(int(i)))
}
cdone <- true
}()
go func() {
for i := 0; i < numEntries; i++ {
m.Delete(strconv.Itoa(int(i)))
}
cdone <- true
}()

// Wait for the goroutines to finish.
<-cdone
<-cdone

stats := CollectMapStats(m)
if stats.RootBuckets < MinMapTableLen {
t.Fatalf("table was too small: %d", stats.RootBuckets)
}
}

func sizeBasedOnRange(m *Map) int {
size := 0
m.Range(func(key string, value interface{}) bool {
Expand Down
2 changes: 1 addition & 1 deletion mapof.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ func (m *MapOf[K, V]) resize(knownTable *mapOfTable[K, V], hint mapResizeHint) {
newTable = newMapOfTable[K, V](tableLen << 1)
case mapShrinkHint:
shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction)
if table.sumSize() <= shrinkThreshold {
if tableLen > minMapTableLen && table.sumSize() <= shrinkThreshold {
// Shrink the table with factor of 2.
atomic.AddInt64(&m.totalShrinks, 1)
newTable = newMapOfTable[K, V](tableLen >> 1)
Expand Down
31 changes: 31 additions & 0 deletions mapof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,37 @@ func TestMapOfStructStoreThenLoadAndDelete(t *testing.T) {
}
}

func TestMapOfStoreThenParallelDelete_DoesNotShrinkBelowMinTableLen(t *testing.T) {
const numEntries = 1000
m := NewMapOf[int, int]()
for i := 0; i < numEntries; i++ {
m.Store(i, i)
}

cdone := make(chan bool)
go func() {
for i := 0; i < numEntries; i++ {
m.Delete(i)
}
cdone <- true
}()
go func() {
for i := 0; i < numEntries; i++ {
m.Delete(i)
}
cdone <- true
}()

// Wait for the goroutines to finish.
<-cdone
<-cdone

stats := CollectMapOfStats(m)
if stats.RootBuckets < MinMapTableLen {
t.Fatalf("table was too small: %d", stats.RootBuckets)
}
}

func sizeBasedOnTypedRange(m *MapOf[string, int]) int {
size := 0
m.Range(func(key string, value int) bool {
Expand Down

0 comments on commit 1386eb4

Please sign in to comment.