重构数据库备份与恢复模块 (#80)

* fix: 保存 LastResult 时截断字符串过长部分,以避免数据库报错

* refactor: 新增 entity.DbTaskBase 和 persistence.dbTaskBase, 用于实现数据库备份和恢复任务处理相关部分

* fix: aeskey变更后,解密密码出现数组越界访问错误

* fix: 时间属性为零值时,保存到 mysql 数据库报错

* refactor db.infrastructure.service.scheduler

* feat: 实现立即备份功能

* refactor db.infrastructure.service.db_instance

* refactor: 从数据库中获取数据库备份目录、mysql文件路径等配置信息

* fix: 数据库备份和恢复问题

* fix: 修改 .gitignore 文件,忽略数据库备份目录和数据库程序目录
This commit is contained in:
kanzihuang
2024-01-05 08:55:34 +08:00
committed by GitHub
parent 76fd6675b5
commit ae3d2659aa
83 changed files with 1819 additions and 1688 deletions

View File

@@ -0,0 +1,276 @@
//go:build e2e
package dbm
import (
"context"
"errors"
"fmt"
"github.com/stretchr/testify/suite"
"mayfly-go/internal/db/config"
"mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository"
"mayfly-go/internal/db/infrastructure/persistence"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
const (
instanceIdTest = 0
backupIdTest = 0
dbNameBackupTest = "test-backup-01"
tableNameBackupTest = "test-backup"
tableNameRestorePITTest = "test-restore-pit"
tableNameNoBackupTest = "test-not-backup"
)
type DbInstanceSuite struct {
suite.Suite
repositories *repository.Repositories
instanceSvc *DbProgramMysql
dbConn *DbConn
}
func (s *DbInstanceSuite) SetupSuite() {
if err := chdir("mayfly-go", "server"); err != nil {
panic(err)
}
dbInfo := DbInfo{
Type: DbTypeMysql,
Host: "localhost",
Port: 3306,
Username: "test",
Password: "test",
}
dbConn, err := dbInfo.Conn()
s.Require().NoError(err)
s.dbConn = dbConn
s.repositories = &repository.Repositories{
Instance: persistence.GetInstanceRepo(),
Backup: persistence.NewDbBackupRepo(),
BackupHistory: persistence.NewDbBackupHistoryRepo(),
Restore: persistence.NewDbRestoreRepo(),
RestoreHistory: persistence.NewDbRestoreHistoryRepo(),
Binlog: persistence.NewDbBinlogRepo(),
BinlogHistory: persistence.NewDbBinlogHistoryRepo(),
}
s.instanceSvc = NewDbProgramMysql(s.dbConn)
var extName string
if runtime.GOOS == "windows" {
extName = ".exe"
}
path := "db/mysql/bin"
s.instanceSvc.mysqlBin = &config.MysqlBin{
Path: filepath.Join(path),
MysqlPath: filepath.Join(path, "mysql"+extName),
MysqldumpPath: filepath.Join(path, "mysqldump"+extName),
MysqlbinlogPath: filepath.Join(path, "mysqlbinlog"+extName),
}
s.instanceSvc.backupPath = "db/backup"
}
func (s *DbInstanceSuite) TearDownSuite() {
if s.dbConn != nil {
s.dbConn.Close()
s.dbConn = nil
}
}
func (s *DbInstanceSuite) SetupTest() {
sql := strings.Builder{}
require := s.Require()
sql.WriteString(fmt.Sprintf("drop database if exists `%s`;", dbNameBackupTest))
sql.WriteString(fmt.Sprintf("create database `%s`;", dbNameBackupTest))
require.NoError(s.instanceSvc.execute("", sql.String()))
}
func (s *DbInstanceSuite) TearDownTest() {
require := s.Require()
sql := fmt.Sprintf("drop database if exists `%s`", dbNameBackupTest)
require.NoError(s.instanceSvc.execute("", sql))
_ = os.RemoveAll(s.instanceSvc.getDbInstanceBackupRoot(instanceIdTest))
}
func (s *DbInstanceSuite) TestBackup() {
task := &entity.DbBackupHistory{
DbName: dbNameBackupTest,
Uuid: dbNameBackupTest,
}
task.Id = backupIdTest
s.testBackup(task)
}
func (s *DbInstanceSuite) testBackup(backupHistory *entity.DbBackupHistory) {
require := s.Require()
binlogInfo, err := s.instanceSvc.Backup(context.Background(), backupHistory)
require.NoError(err)
fileName := filepath.Join(s.instanceSvc.getDbBackupDir(s.dbConn.Info.InstanceId, backupHistory.Id), dbNameBackupTest+".sql")
_, err = os.Stat(fileName)
require.NoError(err)
backupHistory.BinlogFileName = binlogInfo.FileName
backupHistory.BinlogSequence = binlogInfo.Sequence
backupHistory.BinlogPosition = binlogInfo.Position
}
func TestDbInstance(t *testing.T) {
suite.Run(t, &DbInstanceSuite{})
}
func (s *DbInstanceSuite) TestRestoreDatabase() {
backupHistory := &entity.DbBackupHistory{
DbName: dbNameBackupTest,
Uuid: dbNameBackupTest,
}
s.createTable(dbNameBackupTest, tableNameBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameBackupTest, "")
s.testBackup(backupHistory)
s.createTable(dbNameBackupTest, tableNameNoBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameNoBackupTest, "")
s.testRestore(backupHistory)
s.selectTable(dbNameBackupTest, tableNameBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameNoBackupTest, "运行 mysql 程序失败")
}
func (s *DbInstanceSuite) TestRestorePontInTime() {
backupHistory := &entity.DbBackupHistory{
DbName: dbNameBackupTest,
Uuid: dbNameBackupTest,
}
s.createTable(dbNameBackupTest, tableNameBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameBackupTest, "")
s.testBackup(backupHistory)
s.createTable(dbNameBackupTest, tableNameRestorePITTest, "")
s.selectTable(dbNameBackupTest, tableNameRestorePITTest, "")
time.Sleep(time.Second)
targetTime := time.Now()
s.dropTable(dbNameBackupTest, tableNameBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameBackupTest, "运行 mysql 程序失败")
s.createTable(dbNameBackupTest, tableNameNoBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameNoBackupTest, "")
s.testRestore(backupHistory)
s.selectTable(dbNameBackupTest, tableNameBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameRestorePITTest, "运行 mysql 程序失败")
s.selectTable(dbNameBackupTest, tableNameNoBackupTest, "运行 mysql 程序失败")
s.testReplayBinlog(backupHistory, targetTime)
s.selectTable(dbNameBackupTest, tableNameBackupTest, "")
s.selectTable(dbNameBackupTest, tableNameRestorePITTest, "")
s.selectTable(dbNameBackupTest, tableNameNoBackupTest, "运行 mysql 程序失败")
}
func (s *DbInstanceSuite) testReplayBinlog(backupHistory *entity.DbBackupHistory, targetTime time.Time) {
require := s.Require()
binlogFilesOnServerSorted, err := s.instanceSvc.GetSortedBinlogFilesOnServer(context.Background())
require.NoError(err)
require.True(len(binlogFilesOnServerSorted) > 0, "binlog 文件不存在")
for i, bf := range binlogFilesOnServerSorted {
if bf.Name == backupHistory.BinlogFileName {
binlogFilesOnServerSorted = binlogFilesOnServerSorted[i:]
break
}
require.Less(i, len(binlogFilesOnServerSorted), "binlog 文件没找到")
}
err = s.instanceSvc.downloadBinlogFilesOnServer(context.Background(), binlogFilesOnServerSorted, true)
require.NoError(err)
binlogFileLast := binlogFilesOnServerSorted[len(binlogFilesOnServerSorted)-1]
position, err := s.instanceSvc.GetBinlogEventPositionAtOrAfterTime(context.Background(), binlogFileLast.Name, targetTime)
require.NoError(err)
binlogHistories := make([]*entity.DbBinlogHistory, 0, 2)
binlogHistoryBackup := &entity.DbBinlogHistory{
FileName: backupHistory.BinlogFileName,
Sequence: backupHistory.BinlogSequence,
}
binlogHistories = append(binlogHistories, binlogHistoryBackup)
if binlogHistoryBackup.Sequence != binlogFileLast.Sequence {
require.Equal(binlogFilesOnServerSorted[0].Sequence, binlogHistoryBackup.Sequence)
binlogHistoryLast := &entity.DbBinlogHistory{
FileName: binlogFileLast.Name,
Sequence: binlogFileLast.Sequence,
}
binlogHistories = append(binlogHistories, binlogHistoryLast)
}
restoreInfo := &RestoreInfo{
BackupHistory: backupHistory,
BinlogHistories: binlogHistories,
StartPosition: backupHistory.BinlogPosition,
TargetPosition: position,
TargetTime: targetTime,
}
err = s.instanceSvc.ReplayBinlog(context.Background(), dbNameBackupTest, dbNameBackupTest, restoreInfo)
require.NoError(err)
}
func (s *DbInstanceSuite) testRestore(backupHistory *entity.DbBackupHistory) {
require := s.Require()
err := s.instanceSvc.RestoreBackupHistory(context.Background(), backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
require.NoError(err)
}
func (s *DbInstanceSuite) selectTable(database, tableName, wantErr string) {
require := s.Require()
sql := fmt.Sprintf("select * from`%s`;", tableName)
err := s.instanceSvc.execute(database, sql)
if len(wantErr) > 0 {
require.ErrorContains(err, wantErr)
return
}
require.NoError(err)
}
func (s *DbInstanceSuite) createTable(database, tableName, wantErr string) {
require := s.Require()
sql := fmt.Sprintf("create table `%s`(id int);", tableName)
err := s.instanceSvc.execute(database, sql)
if len(wantErr) > 0 {
require.ErrorContains(err, wantErr)
return
}
require.NoError(err)
}
func (s *DbInstanceSuite) dropTable(database, tableName, wantErr string) {
require := s.Require()
sql := fmt.Sprintf("drop table `%s`;", tableName)
err := s.instanceSvc.execute(database, sql)
if len(wantErr) > 0 {
require.ErrorContains(err, wantErr)
return
}
require.NoError(err)
}
func chdir(projectName string, subdir ...string) error {
subdir = append([]string{"/", projectName}, subdir...)
suffix := filepath.Join(subdir...)
wd, err := os.Getwd()
if err != nil {
return err
}
for {
if strings.HasSuffix(wd, suffix) {
if err := os.Chdir(wd); err != nil {
return err
}
return nil
}
upper := filepath.Join(wd, "..")
if upper == wd {
return errors.New(fmt.Sprintf("not found directory: %s", suffix[1:]))
}
wd = upper
}
}