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
195 lines
6.4 KiB
Go
195 lines
6.4 KiB
Go
// Package stmt_store provides an implementation of a statement cache for SQL statements.
|
|
package stmt_store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"gorm.io/gorm/internal/lru"
|
|
)
|
|
|
|
type Stmt struct {
|
|
*sql.Stmt
|
|
Transaction bool
|
|
prepared chan struct{}
|
|
prepareErr error
|
|
}
|
|
|
|
func (stmt *Stmt) Error() error {
|
|
return stmt.prepareErr
|
|
}
|
|
|
|
func (stmt *Stmt) Close() error {
|
|
<-stmt.prepared
|
|
|
|
if stmt.Stmt != nil {
|
|
return stmt.Stmt.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Store defines an interface for managing the caching operations of SQL statements (Stmt).
|
|
// This interface provides methods for creating new statements, retrieving all cache keys,
|
|
// getting cached statements, setting cached statements, and deleting cached statements.
|
|
type Store interface {
|
|
// New creates a new Stmt object and caches it.
|
|
// Parameters:
|
|
// ctx: The context for the request, which can carry deadlines, cancellation signals, etc.
|
|
// key: The key representing the SQL query, used for caching and preparing the statement.
|
|
// isTransaction: Indicates whether this operation is part of a transaction, which may affect the caching strategy.
|
|
// connPool: A connection pool that provides database connections.
|
|
// locker: A synchronization lock that is unlocked after initialization to avoid deadlocks.
|
|
// Returns:
|
|
// *Stmt: A newly created statement object for executing SQL operations.
|
|
// error: An error if the statement preparation fails.
|
|
New(ctx context.Context, key string, isTransaction bool, connPool ConnPool, locker sync.Locker) (*Stmt, error)
|
|
|
|
// Keys returns a slice of all cache keys in the store.
|
|
Keys() []string
|
|
|
|
// Get retrieves a Stmt object from the store based on the given key.
|
|
// Parameters:
|
|
// key: The key used to look up the Stmt object.
|
|
// Returns:
|
|
// *Stmt: The found Stmt object, or nil if not found.
|
|
// bool: Indicates whether the corresponding Stmt object was successfully found.
|
|
Get(key string) (*Stmt, bool)
|
|
|
|
// Set stores the given Stmt object in the store and associates it with the specified key.
|
|
// Parameters:
|
|
// key: The key used to associate the Stmt object.
|
|
// value: The Stmt object to be stored.
|
|
Set(key string, value *Stmt)
|
|
|
|
// Delete removes the Stmt object corresponding to the specified key from the store.
|
|
// Parameters:
|
|
// key: The key associated with the Stmt object to be deleted.
|
|
Delete(key string)
|
|
|
|
// Close releases resources held by the store, stopping any background
|
|
// maintenance goroutines started for cache expiration. It is safe to call
|
|
// Close multiple times. The store should not be used after Close is called.
|
|
Close()
|
|
}
|
|
|
|
// defaultMaxSize defines the default maximum capacity of the cache.
|
|
// Its value is the maximum value of the int64 type, which means that when the cache size is not specified,
|
|
// the cache can theoretically store as many elements as possible.
|
|
// (1 << 63) - 1 is the maximum value that an int64 type can represent.
|
|
const (
|
|
defaultMaxSize = math.MaxInt
|
|
// defaultTTL defines the default time-to-live (TTL) for each cache entry.
|
|
// When the TTL for cache entries is not specified, each cache entry will expire after 24 hours.
|
|
defaultTTL = time.Hour * 24
|
|
)
|
|
|
|
// New creates and returns a new Store instance.
|
|
//
|
|
// Parameters:
|
|
// - size: The maximum capacity of the cache. If the provided size is less than or equal to 0,
|
|
// it defaults to defaultMaxSize.
|
|
// - ttl: The time-to-live duration for each cache entry. If the provided ttl is less than or equal to 0,
|
|
// it defaults to defaultTTL.
|
|
//
|
|
// This function defines an onEvicted callback that is invoked when a cache entry is evicted.
|
|
// The callback ensures that if the evicted value (v) is not nil, its Close method is called asynchronously
|
|
// to release associated resources.
|
|
//
|
|
// Returns:
|
|
// - A Store instance implemented by lruStore, which internally uses an LRU cache with the specified size,
|
|
// eviction callback, and TTL.
|
|
func New(size int, ttl time.Duration) Store {
|
|
if size <= 0 {
|
|
size = defaultMaxSize
|
|
}
|
|
|
|
if ttl <= 0 {
|
|
ttl = defaultTTL
|
|
}
|
|
|
|
onEvicted := func(k string, v *Stmt) {
|
|
if v != nil {
|
|
go v.Close()
|
|
}
|
|
}
|
|
return &lruStore{lru: lru.NewLRU[string, *Stmt](size, onEvicted, ttl)}
|
|
}
|
|
|
|
type lruStore struct {
|
|
lru *lru.LRU[string, *Stmt]
|
|
}
|
|
|
|
func (s *lruStore) Keys() []string {
|
|
return s.lru.Keys()
|
|
}
|
|
|
|
func (s *lruStore) Get(key string) (*Stmt, bool) {
|
|
stmt, ok := s.lru.Get(key)
|
|
if ok && stmt != nil {
|
|
<-stmt.prepared
|
|
}
|
|
return stmt, ok
|
|
}
|
|
|
|
func (s *lruStore) Set(key string, value *Stmt) {
|
|
s.lru.Add(key, value)
|
|
}
|
|
|
|
func (s *lruStore) Delete(key string) {
|
|
s.lru.Remove(key)
|
|
}
|
|
|
|
// Close stops the underlying LRU cache's background expiration goroutine.
|
|
func (s *lruStore) Close() {
|
|
s.lru.Close()
|
|
}
|
|
|
|
type ConnPool interface {
|
|
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
|
}
|
|
|
|
// New creates a new Stmt object for executing SQL queries.
|
|
// It caches the Stmt object for future use and handles preparation and error states.
|
|
// Parameters:
|
|
//
|
|
// ctx: Context for the request, used to carry deadlines, cancellation signals, etc.
|
|
// key: The key representing the SQL query, used for caching and preparing the statement.
|
|
// isTransaction: Indicates whether this operation is part of a transaction, affecting cache strategy.
|
|
// conn: A connection pool that provides database connections.
|
|
// locker: A synchronization lock that is unlocked after initialization to avoid deadlocks.
|
|
//
|
|
// Returns:
|
|
//
|
|
// *Stmt: A newly created statement object for executing SQL operations.
|
|
// error: An error if the statement preparation fails.
|
|
func (s *lruStore) New(ctx context.Context, key string, isTransaction bool, conn ConnPool, locker sync.Locker) (_ *Stmt, err error) {
|
|
// Create a Stmt object and set its Transaction property.
|
|
// The prepared channel is used to synchronize the statement preparation state.
|
|
cacheStmt := &Stmt{
|
|
Transaction: isTransaction,
|
|
prepared: make(chan struct{}),
|
|
}
|
|
// Cache the Stmt object with the associated key.
|
|
s.Set(key, cacheStmt)
|
|
// Unlock after completing initialization to prevent deadlocks.
|
|
locker.Unlock()
|
|
|
|
// Ensure the prepared channel is closed after the function execution completes.
|
|
defer close(cacheStmt.prepared)
|
|
|
|
// Prepare the SQL statement using the provided connection.
|
|
cacheStmt.Stmt, err = conn.PrepareContext(ctx, key)
|
|
if err != nil {
|
|
// If statement preparation fails, record the error and remove the invalid Stmt object from the cache.
|
|
cacheStmt.prepareErr = err
|
|
s.Delete(key)
|
|
return &Stmt{}, err
|
|
}
|
|
|
|
// Return the successfully prepared Stmt object.
|
|
return cacheStmt, nil
|
|
}
|