mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-12 23:30:35 +00:00
fix(model): disable PostgreSQL prepared statements for pooler compatibility
GORM v1.25.2 closes cached prepared statements asynchronously on any SQL error and immediately re-Parses the same deterministic name (pgx's stmt_<sha256>) on the same client connection. Transaction-pooling proxies (PgBouncer >=1.21 with max_prepared_statements, Neon, Supabase) respond with FATAL "prepared statement name is already in use" (SQLSTATE 08P01) and drop the connection. PreferSimpleProtocol only disables pgx's implicit prepare and never covered GORM's explicit PrepareStmt cache. - PostgreSQL now runs with PrepareStmt disabled entirely; named prepared statements are fundamentally session state and cannot be made safe under transaction pooling. Parse/plan cost is noise for this workload. - Upgrade gorm to v1.25.12 so MySQL/SQLite statement caches (still enabled) no longer churn close/re-prepare on ordinary SQL errors; v1.25.9+ restricts eviction to driver.ErrBadConn. Deliberately not v1.26+, whose LRU eviction has an open use-after-close race (#7831). - sanitizeDBError now attaches a remediation hint on 08P01/42P05 so affected deployments can self-diagnose from the log line.
This commit is contained in:
@@ -58,7 +58,7 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.4.3
|
gorm.io/driver/mysql v1.4.3
|
||||||
gorm.io/driver/postgres v1.5.2
|
gorm.io/driver/postgres v1.5.2
|
||||||
gorm.io/gorm v1.25.2
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -3030,6 +3030,8 @@ gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
|||||||
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
||||||
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ func sanitizeDBError(err error) error {
|
|||||||
}
|
}
|
||||||
var pgErr *pgconn.PgError
|
var pgErr *pgconn.PgError
|
||||||
if errors.As(err, &pgErr) {
|
if errors.As(err, &pgErr) {
|
||||||
|
// 08P01 是 PgBouncer 对同连接重名 Parse 的 FATAL,42P05 是原生 PostgreSQL 的
|
||||||
|
// duplicate_prepared_statement;都指向预处理语句与事务池代理不兼容。
|
||||||
|
if pgErr.Code == "08P01" || pgErr.Code == "42P05" {
|
||||||
|
return fmt.Errorf("postgres error SQLSTATE %s: prepared statement conflict with a transaction-pooling proxy (PgBouncer/Neon/Supabase); other clients sharing this database must disable prepared statements, or upgrade PgBouncer to >=1.21 with max_prepared_statements enabled", pgErr.Code)
|
||||||
|
}
|
||||||
return fmt.Errorf("postgres error SQLSTATE %s", pgErr.Code)
|
return fmt.Errorf("postgres error SQLSTATE %s", pgErr.Code)
|
||||||
}
|
}
|
||||||
var chErr *proto.Exception
|
var chErr *proto.Exception
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ func TestSanitizeDBErrorStripsDriverMessage(t *testing.T) {
|
|||||||
want: "postgres error SQLSTATE 23505",
|
want: "postgres error SQLSTATE 23505",
|
||||||
leaked: "secret-value",
|
leaked: "secret-value",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "postgres pooler prepared statement conflict gets remediation hint",
|
||||||
|
err: &pgconn.PgError{Severity: "FATAL", Code: "08P01", Message: "prepared statement name is already in use: stmt_secret-value"},
|
||||||
|
want: "postgres error SQLSTATE 08P01: prepared statement conflict with a transaction-pooling proxy (PgBouncer/Neon/Supabase); other clients sharing this database must disable prepared statements, or upgrade PgBouncer to >=1.21 with max_prepared_statements enabled",
|
||||||
|
leaked: "secret-value",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "clickhouse exception",
|
name: "clickhouse exception",
|
||||||
err: &proto.Exception{Code: 241, Message: "Memory limit exceeded while processing 'secret-value'"},
|
err: &proto.Exception{Code: 241, Message: "Memory limit exceeded while processing 'secret-value'"},
|
||||||
|
|||||||
+4
-2
@@ -138,10 +138,12 @@ func chooseDB(envName string, isLog bool) (*gorm.DB, common.DatabaseType, error)
|
|||||||
if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
|
if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
|
||||||
// Use PostgreSQL
|
// Use PostgreSQL
|
||||||
common.SysLog("using PostgreSQL as database")
|
common.SysLog("using PostgreSQL as database")
|
||||||
|
// 同时关闭 pgx 隐式与 GORM 显式预处理语句:命名 prepared statement 与
|
||||||
|
// 事务池代理(PgBouncer/Neon/Supabase)不兼容,会触发 FATAL 08P01/42P05。
|
||||||
db, err := gorm.Open(postgres.New(postgres.Config{
|
db, err := gorm.Open(postgres.New(postgres.Config{
|
||||||
DSN: dsn,
|
DSN: dsn,
|
||||||
PreferSimpleProtocol: true, // disables implicit prepared statement usage
|
PreferSimpleProtocol: true,
|
||||||
}), newGormConfig(true))
|
}), newGormConfig(false))
|
||||||
return db, common.DatabaseTypePostgreSQL, err
|
return db, common.DatabaseTypePostgreSQL, err
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(dsn, "local") {
|
if strings.HasPrefix(dsn, "local") {
|
||||||
|
|||||||
Reference in New Issue
Block a user