Files
headscale/hscontrol/state/auth_cache_test.go
Kristoffer Dalby 0d4f2293ff state: replace zcache with bounded LRU for auth cache
Replace zcache with golang-lru/v2/expirable for both the state auth
cache and the OIDC state cache. Add tuning.register_cache_max_entries
(default 1024) to cap the number of pending registration entries.

Introduce types.RegistrationData to replace caching a full *Node;
only the fields the registration callback path reads are retained.
Remove the dead HSDatabase.regCache field. Drop zgo.at/zcache/v2
from go.mod.
2026-04-10 14:09:57 +01:00

65 lines
2.0 KiB
Go

package state
import (
"testing"
"time"
"github.com/hashicorp/golang-lru/v2/expirable"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAuthCacheBoundedLRU verifies that the registration auth cache is
// bounded by a maximum entry count, that exceeding the maxEntries evicts the
// oldest entry, and that the eviction callback resolves the parked
// AuthRequest with ErrRegistrationExpired so any waiting goroutine wakes.
func TestAuthCacheBoundedLRU(t *testing.T) {
const maxEntries = 4
cache := expirable.NewLRU[types.AuthID, *types.AuthRequest](
maxEntries,
func(_ types.AuthID, rn *types.AuthRequest) {
rn.FinishAuth(types.AuthVerdict{Err: ErrRegistrationExpired})
},
time.Hour, // long TTL — we test eviction by size, not by time
)
entries := make([]*types.AuthRequest, 0, maxEntries+1)
ids := make([]types.AuthID, 0, maxEntries+1)
for range maxEntries + 1 {
id := types.MustAuthID()
entry := types.NewAuthRequest()
cache.Add(id, entry)
ids = append(ids, id)
entries = append(entries, entry)
}
// Cap should be respected.
assert.Equal(t, maxEntries, cache.Len(), "cache must not exceed the configured maxEntries")
// The oldest entry must have been evicted.
_, ok := cache.Get(ids[0])
assert.False(t, ok, "oldest entry must be evicted when maxEntries is exceeded")
// The eviction callback must have woken the parked AuthRequest.
select {
case verdict := <-entries[0].WaitForAuth():
require.False(t, verdict.Accept(), "evicted entry must not signal Accept")
require.ErrorIs(t,
verdict.Err, ErrRegistrationExpired,
"evicted entry must surface ErrRegistrationExpired, got: %v",
verdict.Err,
)
case <-time.After(time.Second):
t.Fatal("eviction callback did not wake the parked AuthRequest")
}
// All non-evicted entries must still be retrievable.
for i := 1; i <= maxEntries; i++ {
_, ok := cache.Get(ids[i])
assert.True(t, ok, "non-evicted entry %d should still be in the cache", i)
}
}