Files
forgejo/modules/cache/mutex_map_test.go
2025-11-16 08:53:27 -07:00

132 lines
2.7 KiB
Go

// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package cache
import (
"math/rand"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestMutexMap_BasicLockUnlock(t *testing.T) {
mm := &MutexMap{}
unlock := mm.Lock("test-key")
unlock()
// Should be able to lock again
unlock2 := mm.Lock("test-key")
unlock2()
}
func TestMutexMap_ConcurrentSameKey(t *testing.T) {
mm := &MutexMap{}
var anotherLockActive atomic.Bool
var firstError atomic.Value
var wg sync.WaitGroup
for range 10 {
wg.Go(func() {
unlock := mm.Lock("shared-key")
defer unlock()
// should *not* find that another goroutine has put `true` into here.
swapped := anotherLockActive.CompareAndSwap(false, true)
if !swapped {
firstError.CompareAndSwap(nil, "anotherLockActive was true!")
}
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond) // jitter the goroutines to ensure no serial execution
anotherLockActive.Store(false)
})
}
wg.Wait()
if err := firstError.Load(); err != nil {
t.Fatal(err)
}
}
func TestMutexMap_DifferentKeys(t *testing.T) {
mm := &MutexMap{}
done := make(chan bool, 1)
go func() {
// If these somehow refered to the same underlying `sync.Mutex`, because `sync.Mutex` is not re-entrant this would
// never complete.
unlock1 := mm.Lock("test-key-1")
unlock2 := mm.Lock("test-key-2")
unlock3 := mm.Lock("test-key-3")
unlock1()
unlock2()
unlock3()
done <- true
}()
select {
case <-done:
// Success
case <-time.After(1 * time.Second): // early timeout so that we don't wait for t.Deadline()
t.Fatal("test incomplete after timeout, indicating a locking bug")
}
}
func TestMutexMap_SimpleCleanup(t *testing.T) {
mm := &MutexMap{}
unlock1 := mm.Lock("test-key-1")
mm.mu.Lock()
assert.Len(t, mm.mutexMap, 1)
mm.mu.Unlock()
unlock1()
mm.mu.Lock()
assert.Empty(t, mm.mutexMap)
mm.mu.Unlock()
}
func TestMutexMap_ConcurrentCleanup(t *testing.T) {
mm := &MutexMap{}
var foundRefGreaterThanOne atomic.Bool
var wg sync.WaitGroup
for range 10 {
wg.Go(func() {
unlock := mm.Lock("shared-key")
defer unlock()
time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond) // jitter the goroutines to ensure no serial execution
mm.mu.Lock()
rcMutex := mm.mutexMap["shared-key"]
if rcMutex.refCount > 1 {
foundRefGreaterThanOne.Store(true)
}
mm.mu.Unlock()
})
}
wg.Wait()
assert.True(t, foundRefGreaterThanOne.Load(), "expected to find a refCount > 1")
mm.mu.Lock()
assert.Empty(t, mm.mutexMap)
mm.mu.Unlock()
}
func TestMutexMap_UnlockTwice(t *testing.T) {
mm := &MutexMap{}
assert.Panics(t, func() {
unlock := mm.Lock("test")
unlock()
unlock()
})
}