148 lines
5.6 KiB
Go
148 lines
5.6 KiB
Go
package cli
|
||
|
||
import (
|
||
"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 TestMySQLRealEventRestoreSafety(t *testing.T) {
|
||
server := mysqltest.New(t)
|
||
for _, scenario := range []string{"incoming_enabled", "incoming_versioned", "incoming_default", "target_enabled", "target_hidden", "disabled_allowed", "scheduler_off", "missing_process", "script_changes_scheduler"} {
|
||
t.Run(scenario, func(t *testing.T) {
|
||
cfg := server.Database(t)
|
||
parsed, err := mysql.ParseDSN(cfg.DSN)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
account := mysqlStringLiteral(parsed.User) + "@'127.0.0.1'"
|
||
db, err := database.Open(t.Context(), cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
if _, err := db.DB.ExecContext(t.Context(), "CREATE TABLE restore_marker (id INT PRIMARY KEY) ENGINE=InnoDB"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
event := "CREATE EVENT scheduled ON SCHEDULE AT '2036-01-01 00:00:00' ON COMPLETION PRESERVE ENABLE DO SELECT 'DISABLE'"
|
||
script := "INSERT INTO restore_marker VALUES (1);\n" + event + ";"
|
||
state, expected := "ON", "事件调度器仍为 ON"
|
||
switch scenario {
|
||
case "incoming_versioned":
|
||
script = "INSERT INTO restore_marker VALUES (1);\n/*!50106 " + event + " */;"
|
||
case "incoming_default":
|
||
script = "INSERT INTO restore_marker VALUES (1);\nCREATE EVENT disable ON SCHEDULE EVERY 1 DAY DO SELECT 1;"
|
||
case "target_enabled", "target_hidden":
|
||
if _, err := db.DB.ExecContext(t.Context(), event); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
script = "INSERT INTO restore_marker VALUES (1);"
|
||
if scenario == "target_hidden" {
|
||
if _, err := server.Admin().ExecContext(t.Context(), "REVOKE EVENT ON "+quoteMySQLIdentifier(parsed.DBName)+".* FROM "+account); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
expected = "EVENT"
|
||
}
|
||
case "disabled_allowed":
|
||
script = "INSERT INTO restore_marker VALUES (1);\nCREATE EVENT scheduled ON SCHEDULE EVERY 1 DAY DISABLE DO SELECT 1;"
|
||
expected = ""
|
||
case "scheduler_off":
|
||
state, expected = "OFF", ""
|
||
case "missing_process":
|
||
if _, err := server.Admin().ExecContext(t.Context(), "REVOKE PROCESS ON *.* FROM "+account); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
expected = "PROCESS"
|
||
case "script_changes_scheduler":
|
||
script = "INSERT INTO restore_marker VALUES (1);\nSET GLOBAL event_scheduler = ON;"
|
||
expected = "恢复文件不能修改"
|
||
}
|
||
if _, err := server.Admin().ExecContext(t.Context(), "SET GLOBAL event_scheduler = "+state); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
input := filepath.Join(server.Root(), scenario+"-restore.sql")
|
||
if err := os.WriteFile(input, []byte(script), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
err = restoreMySQL(t.Context(), cfg, input)
|
||
count := "0"
|
||
if expected == "" {
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
count = "1"
|
||
} else if err == nil || !strings.Contains(err.Error(), expected) {
|
||
t.Fatalf("恢复前应拒绝且不执行首条语句,期望 %s,得到 %v", expected, err)
|
||
}
|
||
checkMySQLInteropValue(t, cfg, "SELECT CAST(COUNT(*) AS CHAR) FROM restore_marker", count)
|
||
checkMySQLInteropValue(t, cfg, "SELECT @@GLOBAL.event_scheduler", state)
|
||
})
|
||
}
|
||
if _, err := server.Admin().ExecContext(t.Context(), "SET GLOBAL event_scheduler = OFF"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// OFF 不代表已经执行的事件被终止;实库确认工作线程存在时必须拒绝恢复。
|
||
t.Run("running_worker_after_scheduler_off", func(t *testing.T) {
|
||
cfg := server.Database(t)
|
||
db, err := database.Open(t.Context(), cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Close()
|
||
if _, err := db.DB.ExecContext(t.Context(), "CREATE EVENT waiting_event ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND ON COMPLETION PRESERVE ENABLE DO SELECT SLEEP(4)"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := server.Admin().ExecContext(t.Context(), "SET GLOBAL event_scheduler = ON"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
deadline := time.Now().Add(8 * time.Second)
|
||
var workers int
|
||
for time.Now().Before(deadline) {
|
||
if err := server.Admin().QueryRowContext(t.Context(), "SELECT COUNT(*) FROM information_schema.PROCESSLIST WHERE COMMAND = 'Connect' AND STATE = 'User sleep'").Scan(&workers); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if workers > 0 {
|
||
break
|
||
}
|
||
time.Sleep(25 * time.Millisecond)
|
||
}
|
||
if workers == 0 {
|
||
t.Fatal("没有观测到事件工作线程,不能把未覆盖的场景算作通过")
|
||
}
|
||
if _, err := server.Admin().ExecContext(t.Context(), "SET GLOBAL event_scheduler = OFF"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
input := filepath.Join(server.Root(), "running-worker.sql")
|
||
if err := os.WriteFile(input, []byte("CREATE TABLE must_not_exist (id INT);"), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := restoreMySQL(t.Context(), cfg, input); err == nil || !strings.Contains(err.Error(), "工作线程") {
|
||
t.Fatalf("事件尚未退出时应拒绝:%v", err)
|
||
}
|
||
checkMySQLInteropValue(t, cfg, "SELECT CAST(COUNT(*) AS CHAR) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()", "0")
|
||
deadline = time.Now().Add(8 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
if err := server.Admin().QueryRowContext(t.Context(), "SELECT COUNT(*) FROM information_schema.PROCESSLIST WHERE COMMAND = 'Connect'").Scan(&workers); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if workers == 0 {
|
||
break
|
||
}
|
||
time.Sleep(25 * time.Millisecond)
|
||
}
|
||
if workers != 0 {
|
||
t.Fatal("事件线程未在预期时间自然退出")
|
||
}
|
||
if err := restoreMySQL(t.Context(), cfg, input); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
}
|