kvm2's NoCloud seed builder moves to internal/provider/nocloud (exported, with an ErrEmptyName sentinel instead of permanentf) and its keyed locks to internal/provider/keyedlock, tests moved under their existing names. kvm2 behaviour is unchanged. proxmox gains the per-name lock and the pure seed helpers (seedISOName, seedVolID, buildSeed); unlike kvm2 (review finding S5.1), a seed staging failure there is Transient, not Permanent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
91 lines
2.1 KiB
Go
91 lines
2.1 KiB
Go
package keyedlock
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func lockCount(k *Locks) int {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
return len(k.locks)
|
|
}
|
|
|
|
func TestKeyedLocks_entryRemovedWhenUncontended(t *testing.T) {
|
|
t.Parallel()
|
|
k := New()
|
|
release, err := k.Acquire(context.Background(), "a")
|
|
if err != nil {
|
|
t.Fatalf("acquire error = %v", err)
|
|
}
|
|
if lockCount(k) != 1 {
|
|
t.Errorf("entries while held = %d, want 1", lockCount(k))
|
|
}
|
|
release()
|
|
if lockCount(k) != 0 {
|
|
t.Errorf("entries after release = %d, want 0", lockCount(k))
|
|
}
|
|
if _, err := k.Acquire(context.Background(), "a"); err != nil {
|
|
t.Fatalf("acquire after release error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestKeyedLocks_waitersShareOneLock(t *testing.T) {
|
|
t.Parallel()
|
|
k := New()
|
|
const n = 20
|
|
var inside, overlaps atomic.Int32
|
|
var wg sync.WaitGroup
|
|
for range n {
|
|
wg.Go(func() {
|
|
release, err := k.Acquire(context.Background(), "shared")
|
|
if err != nil {
|
|
t.Errorf("acquire error = %v", err)
|
|
return
|
|
}
|
|
if inside.Add(1) > 1 {
|
|
overlaps.Add(1)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
inside.Add(-1)
|
|
release()
|
|
})
|
|
}
|
|
wg.Wait()
|
|
if overlaps.Load() != 0 {
|
|
t.Errorf("%d holders overlapped on one key", overlaps.Load())
|
|
}
|
|
if lockCount(k) != 0 {
|
|
t.Errorf("entries after every holder released = %d, want 0", lockCount(k))
|
|
}
|
|
}
|
|
|
|
func TestKeyedLocks_waiterGivesUpWithContext(t *testing.T) {
|
|
t.Parallel()
|
|
k := New()
|
|
release, err := k.Acquire(context.Background(), "a")
|
|
if err != nil {
|
|
t.Fatalf("acquire error = %v", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
start := time.Now()
|
|
if _, err := k.Acquire(ctx, "a"); !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("waiter error = %v, want DeadlineExceeded", err)
|
|
}
|
|
if time.Since(start) > time.Second {
|
|
t.Errorf("waiter took %v, want to give up with its ctx", time.Since(start))
|
|
}
|
|
if lockCount(k) != 1 {
|
|
t.Errorf("entries with the holder still in = %d, want 1: the waiter's exit must not drop the holder's entry", lockCount(k))
|
|
}
|
|
release()
|
|
if lockCount(k) != 0 {
|
|
t.Errorf("entries after the holder released = %d, want 0", lockCount(k))
|
|
}
|
|
}
|