mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-02-13 03:53:23 +02:00
132 lines
2.7 KiB
Go
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()
|
|
})
|
|
}
|