The expirable LRU cache backing the prepared statement store starts a background deleteExpired goroutine when it is created with a non-zero TTL. Its Close method was commented out, so the goroutine kept running for the lifetime of the process and leaked every time a PreparedStmtDB was discarded (for example when a *sql.DB is closed and reopened), causing steady goroutine and memory growth in long-running services. Expose LRU.Close, add Close to the stmt_store.Store interface and its lruStore implementation, and call it from PreparedStmtDB.Close so the background cleanup goroutine is stopped together with the cached statements. Close is idempotent and safe for concurrent use. Refs #7788
575 lines
12 KiB
Go
575 lines
12 KiB
Go
package tests_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"reflect"
|
|
"runtime"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm/internal/lru"
|
|
)
|
|
|
|
// TestLRU_Close_StopsBackgroundGoroutine verifies that Close terminates the
|
|
// background deleteExpired goroutine started for a cache with a non-zero TTL.
|
|
// Without Close, every such cache leaks one goroutine for the lifetime of the
|
|
// process (see go-gorm/gorm issue #7788).
|
|
func TestLRU_Close_StopsBackgroundGoroutine(t *testing.T) {
|
|
const n = 100
|
|
|
|
runtime.GC()
|
|
time.Sleep(50 * time.Millisecond)
|
|
baseline := runtime.NumGoroutine()
|
|
|
|
caches := make([]*lru.LRU[string, int], 0, n)
|
|
for i := 0; i < n; i++ {
|
|
// A non-zero TTL starts a background deleteExpired goroutine per cache.
|
|
caches = append(caches, lru.NewLRU[string, int](16, nil, time.Minute))
|
|
}
|
|
|
|
// Sanity check: the background goroutines are running.
|
|
if got := runtime.NumGoroutine(); got < baseline+n/2 {
|
|
t.Fatalf("expected background goroutines to start: got %d, baseline %d", got, baseline)
|
|
}
|
|
|
|
for _, c := range caches {
|
|
c.Close()
|
|
}
|
|
caches = nil
|
|
|
|
// After Close the background goroutines must terminate and the goroutine
|
|
// count must return close to the baseline.
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
runtime.GC()
|
|
got := runtime.NumGoroutine()
|
|
if got <= baseline+n/10 {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("background goroutines leaked after Close: got %d, baseline %d", got, baseline)
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestLRU_Add_ExistingKey_UpdatesValueAndExpiresAt(t *testing.T) {
|
|
lru := lru.NewLRU[string, int](10, nil, time.Hour)
|
|
lru.Add("key1", 1)
|
|
lru.Add("key1", 2)
|
|
|
|
if value, ok := lru.Get("key1"); !ok || value != 2 {
|
|
t.Errorf("Expected value to be updated to 2, got %v", value)
|
|
}
|
|
}
|
|
|
|
func TestLRU_Add_NewKey_AddsEntry(t *testing.T) {
|
|
lru := lru.NewLRU[string, int](10, nil, time.Hour)
|
|
lru.Add("key1", 1)
|
|
|
|
if value, ok := lru.Get("key1"); !ok || value != 1 {
|
|
t.Errorf("Expected key1 to be added with value 1, got %v", value)
|
|
}
|
|
}
|
|
|
|
func TestLRU_Add_ExceedsSize_RemovesOldest(t *testing.T) {
|
|
lru := lru.NewLRU[string, int](2, nil, time.Hour)
|
|
lru.Add("key1", 1)
|
|
lru.Add("key2", 2)
|
|
lru.Add("key3", 3)
|
|
|
|
if _, ok := lru.Get("key1"); ok {
|
|
t.Errorf("Expected key1 to be removed, but it still exists")
|
|
}
|
|
}
|
|
|
|
func TestLRU_Add_UnlimitedSize_NoEviction(t *testing.T) {
|
|
lru := lru.NewLRU[string, int](0, nil, time.Hour)
|
|
lru.Add("key1", 1)
|
|
lru.Add("key2", 2)
|
|
lru.Add("key3", 3)
|
|
|
|
if _, ok := lru.Get("key1"); !ok {
|
|
t.Errorf("Expected key1 to exist, but it was evicted")
|
|
}
|
|
}
|
|
|
|
func TestLRU_Add_Eviction(t *testing.T) {
|
|
lru := lru.NewLRU[string, int](0, nil, time.Second*2)
|
|
lru.Add("key1", 1)
|
|
lru.Add("key2", 2)
|
|
lru.Add("key3", 3)
|
|
time.Sleep(time.Second * 3)
|
|
if lru.Cap() != 0 {
|
|
t.Errorf("Expected lru to be empty, but it was not")
|
|
}
|
|
|
|
}
|
|
|
|
func BenchmarkLRU_Rand_NoExpire(b *testing.B) {
|
|
l := lru.NewLRU[int64, int64](8192, nil, 0)
|
|
|
|
trace := make([]int64, b.N*2)
|
|
for i := 0; i < b.N*2; i++ {
|
|
trace[i] = getRand(b) % 32768
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
var hit, miss int
|
|
for i := 0; i < 2*b.N; i++ {
|
|
if i%2 == 0 {
|
|
l.Add(trace[i], trace[i])
|
|
} else {
|
|
if _, ok := l.Get(trace[i]); ok {
|
|
hit++
|
|
} else {
|
|
miss++
|
|
}
|
|
}
|
|
}
|
|
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
}
|
|
|
|
func BenchmarkLRU_Freq_NoExpire(b *testing.B) {
|
|
l := lru.NewLRU[int64, int64](8192, nil, 0)
|
|
|
|
trace := make([]int64, b.N*2)
|
|
for i := 0; i < b.N*2; i++ {
|
|
if i%2 == 0 {
|
|
trace[i] = getRand(b) % 16384
|
|
} else {
|
|
trace[i] = getRand(b) % 32768
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
l.Add(trace[i], trace[i])
|
|
}
|
|
var hit, miss int
|
|
for i := 0; i < b.N; i++ {
|
|
if _, ok := l.Get(trace[i]); ok {
|
|
hit++
|
|
} else {
|
|
miss++
|
|
}
|
|
}
|
|
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
}
|
|
|
|
func BenchmarkLRU_Rand_WithExpire(b *testing.B) {
|
|
l := lru.NewLRU[int64, int64](8192, nil, time.Millisecond*10)
|
|
|
|
trace := make([]int64, b.N*2)
|
|
for i := 0; i < b.N*2; i++ {
|
|
trace[i] = getRand(b) % 32768
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
var hit, miss int
|
|
for i := 0; i < 2*b.N; i++ {
|
|
if i%2 == 0 {
|
|
l.Add(trace[i], trace[i])
|
|
} else {
|
|
if _, ok := l.Get(trace[i]); ok {
|
|
hit++
|
|
} else {
|
|
miss++
|
|
}
|
|
}
|
|
}
|
|
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
}
|
|
|
|
func BenchmarkLRU_Freq_WithExpire(b *testing.B) {
|
|
l := lru.NewLRU[int64, int64](8192, nil, time.Millisecond*10)
|
|
|
|
trace := make([]int64, b.N*2)
|
|
for i := 0; i < b.N*2; i++ {
|
|
if i%2 == 0 {
|
|
trace[i] = getRand(b) % 16384
|
|
} else {
|
|
trace[i] = getRand(b) % 32768
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
l.Add(trace[i], trace[i])
|
|
}
|
|
var hit, miss int
|
|
for i := 0; i < b.N; i++ {
|
|
if _, ok := l.Get(trace[i]); ok {
|
|
hit++
|
|
} else {
|
|
miss++
|
|
}
|
|
}
|
|
b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss))
|
|
}
|
|
|
|
func TestLRUNoPurge(t *testing.T) {
|
|
lc := lru.NewLRU[string, string](10, nil, 0)
|
|
|
|
lc.Add("key1", "val1")
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
v, ok := lc.Peek("key1")
|
|
if v != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
|
|
if !lc.Contains("key1") {
|
|
t.Fatalf("should contain key1")
|
|
}
|
|
if lc.Contains("key2") {
|
|
t.Fatalf("should not contain key2")
|
|
}
|
|
|
|
v, ok = lc.Peek("key2")
|
|
if v != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
|
|
if !reflect.DeepEqual(lc.Keys(), []string{"key1"}) {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
|
|
if lc.Resize(0) != 0 {
|
|
t.Fatalf("evicted count differs from expected")
|
|
}
|
|
if lc.Resize(2) != 0 {
|
|
t.Fatalf("evicted count differs from expected")
|
|
}
|
|
lc.Add("key2", "val2")
|
|
if lc.Resize(1) != 1 {
|
|
t.Fatalf("evicted count differs from expected")
|
|
}
|
|
}
|
|
|
|
func TestLRUEdgeCases(t *testing.T) {
|
|
lc := lru.NewLRU[string, *string](2, nil, 0)
|
|
|
|
// Adding a nil value
|
|
lc.Add("key1", nil)
|
|
|
|
value, exists := lc.Get("key1")
|
|
if value != nil || !exists {
|
|
t.Fatalf("unexpected value or existence flag for key1: value=%v, exists=%v", value, exists)
|
|
}
|
|
|
|
// Adding an entry with the same key but different value
|
|
newVal := "val1"
|
|
lc.Add("key1", &newVal)
|
|
|
|
value, exists = lc.Get("key1")
|
|
if value != &newVal || !exists {
|
|
t.Fatalf("unexpected value or existence flag for key1: value=%v, exists=%v", value, exists)
|
|
}
|
|
}
|
|
|
|
func TestLRU_Values(t *testing.T) {
|
|
lc := lru.NewLRU[string, string](3, nil, 0)
|
|
|
|
lc.Add("key1", "val1")
|
|
lc.Add("key2", "val2")
|
|
lc.Add("key3", "val3")
|
|
|
|
values := lc.Values()
|
|
if !reflect.DeepEqual(values, []string{"val1", "val2", "val3"}) {
|
|
t.Fatalf("values differs from expected")
|
|
}
|
|
}
|
|
|
|
// func TestExpirableMultipleClose(_ *testing.T) {
|
|
// lc :=lru.NewLRU[string, string](10, nil, 0)
|
|
// lc.Close()
|
|
// // should not panic
|
|
// lc.Close()
|
|
// }
|
|
|
|
func TestLRUWithPurge(t *testing.T) {
|
|
var evicted []string
|
|
lc := lru.NewLRU(10, func(key string, value string) { evicted = append(evicted, key, value) }, 150*time.Millisecond)
|
|
|
|
k, v, ok := lc.GetOldest()
|
|
if k != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if v != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
|
|
lc.Add("key1", "val1")
|
|
|
|
time.Sleep(100 * time.Millisecond) // not enough to expire
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
v, ok = lc.Get("key1")
|
|
if v != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
|
|
time.Sleep(200 * time.Millisecond) // expire
|
|
v, ok = lc.Get("key1")
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
if v != "" {
|
|
t.Fatalf("should be nil")
|
|
}
|
|
|
|
if lc.Len() != 0 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
if !reflect.DeepEqual(evicted, []string{"key1", "val1"}) {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
|
|
// add new entry
|
|
lc.Add("key2", "val2")
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
k, v, ok = lc.GetOldest()
|
|
if k != "key2" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if v != "val2" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
|
|
}
|
|
|
|
func TestLRUWithPurgeEnforcedBySize(t *testing.T) {
|
|
lc := lru.NewLRU[string, string](10, nil, time.Hour)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
i := i
|
|
lc.Add(fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i))
|
|
v, ok := lc.Get(fmt.Sprintf("key%d", i))
|
|
if v != fmt.Sprintf("val%d", i) {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
if lc.Len() > 20 {
|
|
t.Fatalf("length should be less than 20")
|
|
}
|
|
}
|
|
|
|
if lc.Len() != 10 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
}
|
|
|
|
func TestLRUConcurrency(t *testing.T) {
|
|
lc := lru.NewLRU[string, string](0, nil, 0)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1000)
|
|
for i := 0; i < 1000; i++ {
|
|
go func(i int) {
|
|
lc.Add(fmt.Sprintf("key-%d", i/10), fmt.Sprintf("val-%d", i/10))
|
|
wg.Done()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if lc.Len() != 100 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
}
|
|
|
|
func TestLRUInvalidateAndEvict(t *testing.T) {
|
|
var evicted int
|
|
lc := lru.NewLRU(-1, func(_, _ string) { evicted++ }, 0)
|
|
|
|
lc.Add("key1", "val1")
|
|
lc.Add("key2", "val2")
|
|
|
|
val, ok := lc.Get("key1")
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
if val != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if evicted != 0 {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
|
|
lc.Remove("key1")
|
|
if evicted != 1 {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
val, ok = lc.Get("key1")
|
|
if val != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
}
|
|
|
|
func TestLoadingExpired(t *testing.T) {
|
|
lc := lru.NewLRU[string, string](0, nil, time.Millisecond*5)
|
|
|
|
lc.Add("key1", "val1")
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
v, ok := lc.Peek("key1")
|
|
if v != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
|
|
v, ok = lc.Get("key1")
|
|
if v != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
|
|
for {
|
|
result, ok := lc.Get("key1")
|
|
if ok && result == "" {
|
|
t.Fatalf("ok should return a result")
|
|
}
|
|
if !ok {
|
|
break
|
|
}
|
|
}
|
|
|
|
time.Sleep(time.Millisecond * 100) // wait for expiration reaper
|
|
if lc.Len() != 0 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
v, ok = lc.Peek("key1")
|
|
if v != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
|
|
v, ok = lc.Get("key1")
|
|
if v != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
}
|
|
|
|
func TestLRURemoveOldest(t *testing.T) {
|
|
lc := lru.NewLRU[string, string](2, nil, 0)
|
|
|
|
if lc.Cap() != 2 {
|
|
t.Fatalf("expect cap is 2")
|
|
}
|
|
|
|
k, v, ok := lc.RemoveOldest()
|
|
if k != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if v != "" {
|
|
t.Fatalf("should be empty")
|
|
}
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
|
|
ok = lc.Remove("non_existent")
|
|
if ok {
|
|
t.Fatalf("should be false")
|
|
}
|
|
|
|
lc.Add("key1", "val1")
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
v, ok = lc.Get("key1")
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
if v != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
|
|
if !reflect.DeepEqual(lc.Keys(), []string{"key1"}) {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
lc.Add("key2", "val2")
|
|
if !reflect.DeepEqual(lc.Keys(), []string{"key1", "key2"}) {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if lc.Len() != 2 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
|
|
k, v, ok = lc.RemoveOldest()
|
|
if k != "key1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if v != "val1" {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if !ok {
|
|
t.Fatalf("should be true")
|
|
}
|
|
|
|
if !reflect.DeepEqual(lc.Keys(), []string{"key2"}) {
|
|
t.Fatalf("value differs from expected")
|
|
}
|
|
if lc.Len() != 1 {
|
|
t.Fatalf("length differs from expected")
|
|
}
|
|
}
|
|
|
|
func getRand(tb testing.TB) int64 {
|
|
out, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
|
if err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
return out.Int64()
|
|
}
|