BUG 原因
开启预编译后,gorm
(db *PreparedStmtDB) prepare会拿预编译语句先到 stmts 查找是否存在预编译 (stmt) 对象,有则直接返回 stmt 对象,无则缓存预编译对象,再缓存预编译语句,(db *PreparedStmtDB) ExecContext有报错则delete(db.Stmts, query)删除预编译对象,但未删除缓存预编译语句,会造成 gorm 发送预编译通过但 SQL 执行时错误(如不可重复) PreparedSQL 一直增长未释放数据
版本
- go:1.22
- gorm:1.25.8
复现
- gorm 配置
DB, _= gorm.Open(mysql.New(conf), &gorm.Config{
PrepareStmt: true,
})
- 调用创建方法
database.DB.Create(&m)
- 报错
Duplicate entry
Gorm 预编译源码
type Stmt struct {
*sql.Stmt
Transaction bool
prepared chan struct{}
prepareErr error
}
type PreparedStmtDB struct {
Stmts map[string]*Stmt
PreparedSQL []string
Mux *sync.RWMutex
ConnPool
}
func NewPreparedStmtDB(connPool ConnPool) *PreparedStmtDB {
return &PreparedStmtDB{
ConnPool: connPool,
Stmts: make(map[string]*Stmt),
Mux: &sync.RWMutex{},
PreparedSQL: make([]string, 0, 100),
}
}
func (db *PreparedStmtDB) GetDBConn() (*sql.DB, error) {
if sqldb, ok := db.ConnPool.(*sql.DB); ok {
return sqldb, nil
}
if dbConnector, ok := db.ConnPool.(GetDBConnector); ok && dbConnector != nil {
return dbConnector.GetDBConn()
}
return nil, ErrInvalidDB
}
func (db *PreparedStmtDB) Close() {
db.Mux.Lock()
defer db.Mux.Unlock()
for _, query := range db.PreparedSQL {
if stmt, ok := db.Stmts[query]; ok {
delete(db.Stmts, query)
go stmt.Close()
}
}
}
func (sdb *PreparedStmtDB) Reset() {
sdb.Mux.Lock()
defer sdb.Mux.Unlock()
for _, stmt := range sdb.Stmts {
go stmt.Close()
}
sdb.PreparedSQL = make([]string, 0, 100)
sdb.Stmts = make(map[string]*Stmt)
}
func (db *PreparedStmtDB) prepare(ctx context.Context, conn ConnPool, isTransaction bool, query string) (Stmt, error) {
db.Mux.RLock()
if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {
db.Mux.RUnlock()
// wait for other goroutines prepared
<-stmt.prepared
if stmt.prepareErr != nil {
return Stmt{}, stmt.prepareErr
}
return *stmt, nil
}
db.Mux.RUnlock()
db.Mux.Lock()
// double check
if stmt, ok := db.Stmts[query]; ok && (!stmt.Transaction || isTransaction) {
db.Mux.Unlock()
// wait for other goroutines prepared
<-stmt.prepared
if stmt.prepareErr != nil {
return Stmt{}, stmt.prepareErr
}
return *stmt, nil
}
_, ok := db.Stmts[query]
// cache preparing stmt first
cacheStmt := Stmt{Transaction: isTransaction, prepared: make(chan struct{})}
// 设置缓存
db.Stmts[query] = &cacheStmt
db.Mux.Unlock()
_, ok = db.Stmts[query]
// prepare completed
defer close(cacheStmt.prepared)
// Reason why cannot lock conn.PrepareContext
// suppose the maxopen is 1, g1 is creating record and g2 is querying record.
// 1. g1 begin tx, g1 is requeue because of waiting for the system call, now `db.ConnPool` db.numOpen == 1.
// 2. g2 select lock `conn.PrepareContext(ctx, query)`, now db.numOpen == db.maxOpen , wait for release.
// 3. g1 tx exec insert, wait for unlock `conn.PrepareContext(ctx, query)` to finish tx and release.
stmt, err := conn.PrepareContext(ctx, query)
if err != nil {
cacheStmt.prepareErr = err
db.Mux.Lock()
delete(db.Stmts, query)
db.Mux.Unlock()
return Stmt{}, err
}
db.Mux.Lock()
cacheStmt.Stmt = stmt
// 设置缓存 SQL
db.PreparedSQL = append(db.PreparedSQL, query)
db.Mux.Unlock()
return cacheStmt, nil
}
func (db *PreparedStmtDB) ExecContext(ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) {
stmt, err := db.prepare(ctx, db.ConnPool, false, query)
if err == nil {
result, err = stmt.ExecContext(ctx, args...)
if err != nil {
db.Mux.Lock()
defer db.Mux.Unlock()
go stmt.Close()
// 删除缓存
delete(db.Stmts, query)
}
}
return result, err
}
Comment From: github-actions[bot]
The issue has been automatically marked as stale as it missing playground pull request link, which is important to help others understand your issue effectively and make sure the issue hasn't been fixed on latest master, checkout https://github.com/go-gorm/playground for details. it will be closed in 30 days if no further activity occurs. if you are asking question, please use the Question template, most likely your question already answered https://github.com/go-gorm/gorm/issues or described in the document https://gorm.io ✨ Search Before Asking ✨
Comment From: a631807682
PreparedSQL stores the key of Stmts, which gorm uses only when PreparedStmtDB Close.
It is an exported field, is it used elsewhere? Can we remove it? @jinzhu
https://github.com/go-gorm/gorm/commit/c7667e9299134799da6f16e19eaf50cb8419736f
Comment From: ivila
PreparedSQLstores the key ofStmts, which gorm uses only whenPreparedStmtDB Close. It is an exported field, is it used elsewhere? Can we remove it? @jinzhu
I think he uses this just trying to avoid deleting keys while iterating, in my opinion, removing the PreparedSQL field and using two iteration is better, just like the following:
// and PreparedStmtDB.PreparedSQL should be removed
func (db *PreparedStmtDB) Close() {
db.mux.Lock()
keys := make([]string, 0, len(db.Stmts))
for key := range db.Stmts {
keys = append(keys, key)
}
for _, key := range keys {
if stmt, ok := db.Stmts[key]; ok {
delete(db.Stmts, key)
stmt.Close()
}
}
db.mux.Unlock()
}
Should we? @jinzhu @a631807682
Comment From: a631807682
PreparedSQLstores the key ofStmts, which gorm uses only whenPreparedStmtDB Close. It is an exported field, is it used elsewhere? Can we remove it? @jinzhu c7667e9I think he uses this just trying to avoid deleting keys while iterating, in my opinion, removing the PreparedSQL field and using two iteration is better, just like the following:
``` // and PreparedStmtDB.PreparedSQL should be removed func (db *PreparedStmtDB) Close() { db.mux.Lock()
keys := make([]string, 0, len(db.Stmts)) for key := range db.Stmts { keys = append(keys, key) } for _, key := range keys { if stmt, ok := db.Stmts[key]; ok { delete(db.Stmts, key) stmt.Close() } }
db.mux.Unlock() } ```
Should we? @jinzhu @a631807682
Are you interested in creating a PR for it?
Comment From: ivila
Sure, just wait for hours, I will write a PR for it.
Comment From: ivila
@a631807682 during my coding, I see another potential bug in the Reset function: https://github.com/go-gorm/gorm/blob/4a50b36f638c6899089e6e3457425528ce693933/prepare_stmt.go#L59-L69 while we prepare a new statement, the new Stmt register itself to the db.Stmt map (with sql.Stmt* field value nil), and unlock the mutex: https://github.com/go-gorm/gorm/blob/4a50b36f638c6899089e6e3457425528ce693933/prepare_stmt.go#L96-L104
which result in a nil pointer error at Reset function call:
func (sdb *PreparedStmtDB) Reset() {
sdb.Mux.Lock()
defer sdb.Mux.Unlock()
for _, stmt := range sdb.Stmts {
// stmt could be nil here
go stmt.Close()
}
sdb.PreparedSQL = make([]string, 0, 100)
sdb.Stmts = make(map[string]*Stmt)
}
but as what I said, it is just a potential bug(people not likely call Reset and exec SQL concurrently).