fix: stop prepared statement cache cleanup goroutine on Close
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
This commit is contained in:
1 parent
6182f42ca4
commit
3461e6bce4
4 files changed
+74
-14
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -13,6 +14,49 @@ import (
|
||||
"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)
|
||||
|
||||
Reference in new issue
Block a user