保留并验证四个待提交源码文件:显式导出可写列以保留不可见列,跳过生成列并保留仅生成列表的行数;修复扫描越界,以实际结构元数据校验恢复结果。MySQL 5.7.44与8.0.45专项回归、后端常规测试、静态检查及模块校验通过;不代表完整生产验收。
143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
package cli
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/go-sql-driver/mysql"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
|
||
)
|
||
|
||
func TestMySQLRealBackupSnapshot(t *testing.T) {
|
||
server := mysqltest.New(t)
|
||
for _, scenario := range []struct {
|
||
name string
|
||
idle int
|
||
}{
|
||
{name: "without_idle_connections", idle: 0},
|
||
{name: "with_idle_connections", idle: 1},
|
||
} {
|
||
t.Run(scenario.name, func(t *testing.T) {
|
||
cfg := server.Database(t)
|
||
cfg.MaxIdleConns = scenario.idle
|
||
db, err := database.Open(t.Context(), cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
for _, table := range []string{"a_snapshot", "b_snapshot"} {
|
||
if _, err := db.DB.ExecContext(t.Context(), "CREATE TABLE "+table+" (id INT PRIMARY KEY, epoch INT NOT NULL) ENGINE=InnoDB"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.DB.ExecContext(t.Context(), "INSERT INTO "+table+" VALUES (1, 0)"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
parsed, err := mysql.ParseDSN(cfg.DSN)
|
||
if err != nil {
|
||
t.Fatal("解析隔离数据库连接失败")
|
||
}
|
||
locker, err := server.Admin().Conn(t.Context())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer locker.Close()
|
||
qualified := quoteMySQLIdentifier(parsed.DBName) + ".`b_snapshot`"
|
||
if _, err := locker.ExecContext(t.Context(), "LOCK TABLES "+qualified+" WRITE"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer func() {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
_, _ = locker.ExecContext(ctx, "UNLOCK TABLES")
|
||
}()
|
||
output := filepath.Join(server.Root(), scenario.name+".sql")
|
||
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
|
||
defer cancel()
|
||
done := make(chan struct{})
|
||
var backupErr error
|
||
go func() {
|
||
defer close(done)
|
||
backupErr = backupMySQLDatabase(ctx, cfg, output)
|
||
}()
|
||
t.Cleanup(func() {
|
||
cancel()
|
||
select {
|
||
case <-done:
|
||
case <-time.After(5 * time.Second):
|
||
t.Error("备份协程未按期结束")
|
||
}
|
||
})
|
||
waitForMySQLBackupLock(t, ctx, server.Admin(), parsed.DBName)
|
||
content, err := os.ReadFile(output + ".tmp")
|
||
if err != nil || !strings.Contains(string(content), "INSERT INTO `a_snapshot`") {
|
||
t.Fatal("未到达首表已导出、第二张表等待锁的测试边界")
|
||
}
|
||
// 真实状态依次为 (0,0)、(1,0)、(1,1),从未出现 (0,1)。
|
||
// 更新第一张表后,再在持锁连接更新第二张表,避免依赖调度概率。
|
||
if _, err := db.DB.ExecContext(ctx, "UPDATE a_snapshot SET epoch = 1"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := locker.ExecContext(ctx, "UPDATE "+qualified+" SET epoch = 1"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := locker.ExecContext(ctx, "UNLOCK TABLES"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
select {
|
||
case <-done:
|
||
if backupErr != nil {
|
||
t.Fatal(backupErr)
|
||
}
|
||
case <-ctx.Done():
|
||
t.Fatal("真实备份未按期完成")
|
||
}
|
||
restored := server.Database(t)
|
||
if err := restoreMySQL(t.Context(), restored, output); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
verify, err := database.Open(t.Context(), restored)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer verify.Close()
|
||
var first, second int
|
||
if err := verify.DB.QueryRowContext(t.Context(), "SELECT a.epoch, b.epoch FROM a_snapshot a JOIN b_snapshot b ON a.id = b.id").Scan(&first, &second); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if first != 0 || second != 0 {
|
||
t.Fatalf("跨表备份不是同一读取快照:首表=%d,次表=%d", first, second)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func waitForMySQLBackupLock(t *testing.T, ctx context.Context, admin *sql.DB, name string) uint64 {
|
||
t.Helper()
|
||
ticker := time.NewTicker(10 * time.Millisecond)
|
||
defer ticker.Stop()
|
||
for {
|
||
var connectionID uint64
|
||
err := admin.QueryRowContext(ctx, "SELECT ID FROM information_schema.PROCESSLIST WHERE DB = ? AND STATE LIKE '%lock%' AND (INFO = 'SHOW CREATE TABLE `b_snapshot`' OR INFO LIKE 'SELECT % FROM `b_snapshot`')", name).Scan(&connectionID)
|
||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||
t.Fatal(err)
|
||
}
|
||
if err == nil {
|
||
return connectionID
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
t.Fatal("未观察到备份在第二张表等待真实数据库锁")
|
||
case <-ticker.C:
|
||
}
|
||
}
|
||
}
|