fix bug
This commit is contained in:
1 parent
b55ceb1fdd
commit
a951e4aad3
6 files changed
+279
-44
No files matched your search
@@ -22,6 +22,11 @@ var schemaSQL string
|
||||
|
||||
const schemaMigrationLockKey int64 = 7337741001
|
||||
|
||||
// maxOpenConns 是连接池上限,见 Open 里的说明。取值远低于 PostgreSQL 默认的
|
||||
// max_connections=100,同时远高于应用自身的嵌套取连接深度(启动期的 schema
|
||||
// advisory lock 会在持有一条连接的同时让 seedBuiltins 另取连接),不会自锁。
|
||||
const maxOpenConns = 32
|
||||
|
||||
var schemaDeadlockRetryDelays = [...]time.Duration{
|
||||
100 * time.Millisecond,
|
||||
250 * time.Millisecond,
|
||||
@@ -133,6 +138,16 @@ func Open(dsn string) (*DB, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// database/sql 默认不限制连接数:池里没有空闲连接时会无条件新建,一路顶到
|
||||
// PostgreSQL 的 max_connections(默认 100)才被拒,于是高峰期的查询拿到的是
|
||||
// `FATAL: sorry, too many clients already` 这种**错误**。封顶之后超额查询改为
|
||||
// 排队等待空闲连接——同样的负载下变成变慢而不是报错,调用方不必再去区分
|
||||
// "读不到"和"没有"。maxOpenConns 要留出余量给 psql / reset-password.sh 以及
|
||||
// 可能并存的其他实例;若 max_connections 调低过,这里也要跟着往下调。
|
||||
sqlDB.SetMaxOpenConns(maxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(maxOpenConns)
|
||||
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
sqlDB.Close()
|
||||
return nil, fmt.Errorf("ping postgres (%s): %w", config.Redact(dsn), err)
|
||||
|
||||
@@ -25,6 +25,24 @@ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, key
|
||||
return err
|
||||
}
|
||||
|
||||
// InsertSettingIfAbsent 只在 key 尚未存在时写入,已存在则原样保留并返回 inserted=false。
|
||||
// 给 auth.password_hash 这类"只允许首次设置"的键用:判定落在数据库的主键约束上,
|
||||
// 调用方先 GetSetting 再写的那种检查就只是快速失败路径——读出错或并发撞车时,
|
||||
// 已有的值也不会被 ON CONFLICT DO UPDATE 顺手覆盖掉。
|
||||
func (d *DB) InsertSettingIfAbsent(key, value string) (inserted bool, err error) {
|
||||
res, err := d.Exec(`
|
||||
INSERT INTO settings(key, value) VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO NOTHING`, key, value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// GetBool returns the boolean setting, or def when unset/unparseable.
|
||||
func (d *DB) GetBool(key string, def bool) bool {
|
||||
v, ok, err := d.GetSetting(key)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// InsertSettingIfAbsent 是 auth.password_hash 的兜底:调用方的 GetSetting 检查可能
|
||||
// 因为数据库报错而失效,也可能被并发请求插队(bcrypt 要跑几十毫秒),所以"仅首次
|
||||
// 可设"的保证必须落在主键约束上,而不是上层的 if 判断。
|
||||
func TestInsertSettingIfAbsentDoesNotOverwrite(t *testing.T) {
|
||||
d, err := Open(testDSN(t))
|
||||
if err != nil {
|
||||
t.Skipf("postgres unavailable (%v)", err)
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
// 用测试专属 key,绝不碰开发库里真实的 auth.password_hash。
|
||||
key := fmt.Sprintf("test.insert_if_absent.%d", time.Now().UnixNano())
|
||||
defer func() { _, _ = d.Exec(`DELETE FROM settings WHERE key=$1`, key) }()
|
||||
|
||||
inserted, err := d.InsertSettingIfAbsent(key, "first")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !inserted {
|
||||
t.Fatal("首次写入应返回 inserted=true")
|
||||
}
|
||||
|
||||
inserted, err = d.InsertSettingIfAbsent(key, "second")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inserted {
|
||||
t.Fatal("键已存在时应返回 inserted=false")
|
||||
}
|
||||
|
||||
got, ok, err := d.GetSetting(key)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("GetSetting: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if got != "first" {
|
||||
t.Fatalf("值被覆盖成 %q,应保持 %q", got, "first")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user