Files
xlgo-core/app_hdb1_test.go
2026-07-14 10:24:10 +08:00

117 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package xlgo_test
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"sync"
"testing"
"time"
xlgo "github.com/EthanCodeCraft/xlgo-core"
"github.com/EthanCodeCraft/xlgo-core/config"
"github.com/EthanCodeCraft/xlgo-core/database"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
)
// appHungDriver 是测试用 database/sql 驱动,Conn.Ping 阻塞到 ctx 取消,模拟挂起 DB。
// 与 database 包内部的 hungDriver 同构(app_test 为外部包,需自带一份)。
type appHungDriver struct{}
func (appHungDriver) Open(string) (driver.Conn, error) { return appHungConn{}, nil }
type appHungConn struct{}
func (appHungConn) Prepare(string) (driver.Stmt, error) { return nil, errors.New("hung: not implemented") }
func (appHungConn) Close() error { return nil }
func (appHungConn) Begin() (driver.Tx, error) { return nil, errors.New("hung: not implemented") }
// Ping 阻塞到 ctx 取消(实现 driver.Pinger,方法名 Ping 而非 PingContext)。
func (appHungConn) Ping(ctx context.Context) error {
<-ctx.Done()
return ctx.Err()
}
var appHungRegisterOnce sync.Once
func registerAppHungDialect() {
appHungRegisterOnce.Do(func() {
sql.Register("xlgo_app_hung_hdb1", appHungDriver{})
database.RegisterDialect(database.DialectSpec{
Name: "app_hung_hdb1",
Dialector: func(string) gorm.Dialector {
db, _ := sql.Open("xlgo_app_hung_hdb1", "")
return appHungDialector{sqlDB: db}
},
DSN: func(*config.DatabaseConfig) string { return "app_hung://" },
})
})
}
// appHungDialector 让 gorm.Open 成功并注入挂起 *sql.DB,使 initDB 的 pingWithTimeout 触发。
type appHungDialector struct{ sqlDB *sql.DB }
func (d appHungDialector) Name() string { return "app_hung_hdb1" }
func (d appHungDialector) Initialize(db *gorm.DB) error { db.Config.ConnPool = d.sqlDB; return nil }
func (d appHungDialector) Migrator(*gorm.DB) gorm.Migrator { return nil }
func (d appHungDialector) DataTypeOf(*schema.Field) string { return "" }
func (d appHungDialector) DefaultValueOf(*schema.Field) clause.Expression { return nil }
func (d appHungDialector) BindVarTo(clause.Writer, *gorm.Statement, any) {}
func (d appHungDialector) QuoteTo(w clause.Writer, s string) { _, _ = w.WriteString(s) }
func (d appHungDialector) Explain(s string, _ ...any) string { return s }
// TestAppInitHungDBBounded_Hdb1 端到端回归 H-db-1App 级闭环):
// App.Init 启用 MySQL 且 DB 挂起时,initDB 的 pingWithTimeout 3s 失败 -> InitDB 返回错误
// -> App.Init 失败(failAfterInit 回滚)。全程有界,不因挂起 DB 无限卡死 App.Init。
// 修复前 gorm.Open 的 automatic Ping() 在 pingWithTimeout 之前就无限阻塞 -> App.Init 永久 hang。
// 同时验证 App.Init 失败后无 goroutine 泄漏(closeResources 正常)。
func TestAppInitHungDBBounded_Hdb1(t *testing.T) {
registerAppHungDialect()
cfg := &config.Config{
App: config.AppConfig{Env: "test"},
Server: config.ServerConfig{Port: 18091},
Database: config.DatabaseConfig{
Driver: "app_hung_hdb1",
Host: "localhost", Port: 3306, User: "u", Password: "p", Name: "n",
},
}
app := xlgo.New(xlgo.WithConfig(cfg), xlgo.WithMySQL())
done := make(chan error, 1)
start := time.Now()
go func() { done <- app.Init() }()
select {
case err := <-done:
elapsed := time.Since(start)
if err == nil {
t.Fatalf("挂起 DB 下 App.Init 应失败,got nil")
}
// initDB pingWithTimeout 3s 失败后重试 5 次(每次 ping 3s + retryDelay 1s 起步翻倍),
// 全跑约 5*4s=20s+。放宽到 35s 上限。关键是不触发下方 45s watchdog 的"无限阻塞"。
if elapsed > 35*time.Second {
t.Fatalf("App.Init 耗时 %v 超过 35s 上限(H-db-1initDB ping 应被 3s 约束,重试 5 次应 ~20s", elapsed)
}
case <-time.After(45 * time.Second):
t.Fatalf("App.Init 在挂起 DB 上无限阻塞(H-db-1gorm.Open automatic ping 或 initDB ping 未被超时约束)")
}
// App.Init 失败后应能正常 ShutdownfailAfterInit 已回滚资源,Shutdown 幂等)。
shutdownDone := make(chan error, 1)
go func() { shutdownDone <- app.Shutdown() }()
select {
case err := <-shutdownDone:
if err != nil {
t.Logf("App.Shutdown after failed Init returned err: %v(可接受,资源已回滚)", err)
}
case <-time.After(10 * time.Second):
t.Fatalf("App.Shutdown 在 Init 失败后无限阻塞(closeResources 未正常收口)")
}
// rootCtx 已被 failAfterInit cancel,无残留 goroutineStartProbing 未启动因 Init 失败)。
// 此处不直接断言 goroutine 数(受测试 runtime 噪声影响),靠 Shutdown 有界退出间接证明。
_ = context.Background()
}