Files
CLIProxyAPI/sdk/cliproxy/auth/session_cache_test.go
sususu 5679bbf330 fix(auth): optimize session cache LRU capacity eviction
- replace O(N log N) sorting under global write lock with container/list LRU eviction
- track primary alias groups for O(1) group lookup and clean group eviction
- capture timestamps inside lock and add nil receiver guards
2026-09-01 18:00:09 +08:00

171 lines
4.2 KiB
Go

package auth
import (
"fmt"
"sync"
"testing"
"time"
)
func TestSessionCache_CapacityEvictionOrder(t *testing.T) {
t.Parallel()
maxEntries := 5
cache := NewSessionCacheWithCapacity(time.Hour, maxEntries)
defer cache.Stop()
// Insert 5 entries (fill cache to capacity)
for i := 1; i <= 5; i++ {
cache.Set(fmt.Sprintf("sess-%d", i), fmt.Sprintf("auth-%d", i))
}
if cache.Len() != 5 {
t.Fatalf("cache.Len() = %d, want 5", cache.Len())
}
// Insert 6th entry: oldest (sess-1) should be evicted
cache.Set("sess-6", "auth-6")
if cache.Len() > maxEntries {
t.Fatalf("cache.Len() = %d, want <= %d", cache.Len(), maxEntries)
}
if _, ok := cache.Get("sess-1"); ok {
t.Fatal("expected oldest entry sess-1 to be evicted")
}
for i := 2; i <= 6; i++ {
if got, ok := cache.Get(fmt.Sprintf("sess-%d", i)); !ok || got != fmt.Sprintf("auth-%d", i) {
t.Fatalf("Get(sess-%d) = %q, %v; want auth-%d, true", i, got, ok, i)
}
}
}
func TestSessionCache_MultiAliasGroupEviction(t *testing.T) {
t.Parallel()
maxEntries := 4
cache := NewSessionCacheWithCapacity(time.Hour, maxEntries)
defer cache.Stop()
// Group 1: 2 aliases (s1-a, s1-b)
cache.SetAliases("auth-1", "s1-a", "s1-b")
// Group 2: 2 aliases (s2-a, s2-b)
cache.SetAliases("auth-2", "s2-a", "s2-b")
if cache.Len() != 4 {
t.Fatalf("cache.Len() = %d, want 4", cache.Len())
}
// Group 3: 1 alias (s3)
// Inserting s3 should evict Group 1 completely (both s1-a and s1-b)
cache.Set("s3", "auth-3")
if cache.Len() > maxEntries {
t.Fatalf("cache.Len() = %d, want <= %d", cache.Len(), maxEntries)
}
if _, ok := cache.Get("s1-a"); ok {
t.Fatal("expected s1-a to be evicted")
}
if _, ok := cache.Get("s1-b"); ok {
t.Fatal("expected s1-b to be evicted")
}
if got, ok := cache.Get("s2-a"); !ok || got != "auth-2" {
t.Fatalf("Get(s2-a) = %q, %v", got, ok)
}
if got, ok := cache.Get("s3"); !ok || got != "auth-3" {
t.Fatalf("Get(s3) = %q, %v", got, ok)
}
}
func TestSessionCache_HighThroughputCapacitySaturated(t *testing.T) {
t.Parallel()
maxEntries := 1000
cache := NewSessionCacheWithCapacity(time.Hour, maxEntries)
defer cache.Stop()
// Rapidly write 10,000 entries through a small capacity cache to ensure O(1) eviction
for i := 0; i < 10000; i++ {
cache.Set(fmt.Sprintf("sess-%d", i), fmt.Sprintf("auth-%d", i%10))
}
if cache.Len() > maxEntries {
t.Fatalf("cache.Len() = %d, want <= %d", cache.Len(), maxEntries)
}
// Latest entries must be present
for i := 9900; i < 10000; i++ {
if got, ok := cache.Get(fmt.Sprintf("sess-%d", i)); !ok || got != fmt.Sprintf("auth-%d", i%10) {
t.Fatalf("Get(sess-%d) = %q, %v", i, got, ok)
}
}
}
func TestSessionCache_ConcurrentSaturatedAccess(t *testing.T) {
t.Parallel()
maxEntries := 50
cache := NewSessionCacheWithCapacity(time.Hour, maxEntries)
defer cache.Stop()
var wg sync.WaitGroup
for worker := 0; worker < 8; worker++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
for i := 0; i < 500; i++ {
key := fmt.Sprintf("worker-%d-sess-%d", w, i)
cache.Set(key, fmt.Sprintf("auth-%d", w))
cache.Get(key)
if i%5 == 0 {
cache.Touch(key, fmt.Sprintf("auth-%d", w))
}
if i%7 == 0 {
cache.CompareAndDelete(key, fmt.Sprintf("auth-%d", w))
}
}
}(worker)
}
wg.Wait()
if cache.Len() > maxEntries {
t.Fatalf("cache.Len() = %d, want <= %d", cache.Len(), maxEntries)
}
}
func TestSessionCache_NilReceiverSafety(t *testing.T) {
t.Parallel()
var cache *SessionCache
if got, ok := cache.Get("session-1"); ok || got != "" {
t.Fatalf("nil.Get() = %q, %v; want '', false", got, ok)
}
if got, ok := cache.GetAndRefresh("session-1"); ok || got != "" {
t.Fatalf("nil.GetAndRefresh() = %q, %v; want '', false", got, ok)
}
cache.Set("session-1", "auth-1")
cache.SetAliases("auth-1", "s1", "s2")
if ok := cache.Touch("session-1", "auth-1"); ok {
t.Fatal("nil.Touch() unexpectedly succeeded")
}
if ok := cache.CompareAndDelete("session-1", "auth-1"); ok {
t.Fatal("nil.CompareAndDelete() unexpectedly succeeded")
}
cache.Invalidate("session-1")
cache.InvalidateAuth("auth-1")
if n := cache.Len(); n != 0 {
t.Fatalf("nil.Len() = %d, want 0", n)
}
cache.Stop()
}
func TestSessionCache_StopNilChannelNoPanic(t *testing.T) {
t.Parallel()
zeroCache := &SessionCache{}
zeroCache.Stop()
}