Skip to content

Commit

Permalink
🩹 Fix: Memory leak removal in the idempotency middleware (#3263)
Browse files Browse the repository at this point in the history
* 🩹 Fix: Add key removal in MemoryLock

* Fixed concurrent deletion.

* Fix: idempotency middleware's MemoryLock

* Add MemoryLock benchmarks.

* Updated benchmarks: Add returning error handling

* Renamed benchmark: RepeatedKeys

---------

Co-authored-by: Juan Calderon-Perez <[email protected]>
  • Loading branch information
sunnyyssh and gaby authored Dec 28, 2024
1 parent 57744eb commit 775e0a7
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 9 deletions.
34 changes: 25 additions & 9 deletions middleware/idempotency/locker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,58 @@ type Locker interface {
Unlock(key string) error
}

type countedLock struct {
mu sync.Mutex
locked int
}

type MemoryLock struct {
keys map[string]*sync.Mutex
keys map[string]*countedLock
mu sync.Mutex
}

func (l *MemoryLock) Lock(key string) error {
l.mu.Lock()
mu, ok := l.keys[key]
lock, ok := l.keys[key]
if !ok {
mu = new(sync.Mutex)
l.keys[key] = mu
lock = new(countedLock)
l.keys[key] = lock
}
lock.locked++
l.mu.Unlock()

mu.Lock()
lock.mu.Lock()

return nil
}

func (l *MemoryLock) Unlock(key string) error {
l.mu.Lock()
mu, ok := l.keys[key]
l.mu.Unlock()
lock, ok := l.keys[key]
if !ok {
// This happens if we try to unlock an unknown key
l.mu.Unlock()
return nil
}
l.mu.Unlock()

mu.Unlock()
lock.mu.Unlock()

l.mu.Lock()
lock.locked--
if lock.locked <= 0 {
// This happens if countedLock is used to Lock and Unlock the same number of times
// So, we can delete the key to prevent memory leak
delete(l.keys, key)
}
l.mu.Unlock()

return nil
}

func NewMemoryLock() *MemoryLock {
return &MemoryLock{
keys: make(map[string]*sync.Mutex),
keys: make(map[string]*countedLock),
}
}

Expand Down
66 changes: 66 additions & 0 deletions middleware/idempotency/locker_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package idempotency_test

import (
"strconv"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -59,3 +61,67 @@ func Test_MemoryLock(t *testing.T) {
require.NoError(t, err)
}
}

func Benchmark_MemoryLock(b *testing.B) {
keys := make([]string, b.N)
for i := range keys {
keys[i] = strconv.Itoa(i)
}

lock := idempotency.NewMemoryLock()

b.ResetTimer()

for i := 0; i < b.N; i++ {
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
}

func Benchmark_MemoryLock_Parallel(b *testing.B) {
// In order to prevent using repeated keys I pre-allocate keys
keys := make([]string, 1_000_000)
for i := range keys {
keys[i] = strconv.Itoa(i)
}

b.Run("UniqueKeys", func(b *testing.B) {
lock := idempotency.NewMemoryLock()
var keyI atomic.Int32
b.RunParallel(func(p *testing.PB) {
for p.Next() {
i := int(keyI.Add(1)) % len(keys)
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
})
})

b.Run("RepeatedKeys", func(b *testing.B) {
lock := idempotency.NewMemoryLock()
var keyI atomic.Int32
b.RunParallel(func(p *testing.PB) {
for p.Next() {
// Division by 3 ensures that index will be repreated exactly 3 times
i := int(keyI.Add(1)) / 3 % len(keys)
key := keys[i]
if err := lock.Lock(key); err != nil {
b.Fatal(err)
}
if err := lock.Unlock(key); err != nil {
b.Fatal(err)
}
}
})
})
}

1 comment on commit 775e0a7

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 775e0a7 Previous: 47be681 Ratio
Benchmark_Ctx_AcceptsEncodings 268.1 ns/op 0 B/op 0 allocs/op 170.5 ns/op 0 B/op 0 allocs/op 1.57
Benchmark_Ctx_AcceptsEncodings - ns/op 268.1 ns/op 170.5 ns/op 1.57
Benchmark_Ctx_AcceptsLanguages 384.5 ns/op 0 B/op 0 allocs/op 249.2 ns/op 0 B/op 0 allocs/op 1.54
Benchmark_Ctx_AcceptsLanguages - ns/op 384.5 ns/op 249.2 ns/op 1.54
Benchmark_Utils_GetOffer/1_parameter 231.1 ns/op 0 B/op 0 allocs/op 131.7 ns/op 0 B/op 0 allocs/op 1.75
Benchmark_Utils_GetOffer/1_parameter - ns/op 231.1 ns/op 131.7 ns/op 1.75
Benchmark_Middleware_BasicAuth - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_Middleware_BasicAuth_Upper - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth_Upper - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_CORS_NewHandler - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandler - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflight - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflight - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_Middleware_CSRF_GenerateToken - B/op 519 B/op 332 B/op 1.56
Benchmark_Middleware_CSRF_GenerateToken - allocs/op 10 allocs/op 6 allocs/op 1.67

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.