mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-04 00:10:25 +08:00
382 lines
11 KiB
Go
382 lines
11 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"mayfly-go/internal/db/dbm/dbi"
|
|
"mayfly-go/internal/db/domain/entity"
|
|
"mayfly-go/internal/db/domain/repository"
|
|
"mayfly-go/pkg/runner"
|
|
"reflect"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/sync/singleflight"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
maxRunning = 8
|
|
)
|
|
|
|
type dbScheduler struct {
|
|
mutex sync.Mutex
|
|
runner *runner.Runner[entity.DbJob]
|
|
dbApp Db `inject:"DbApp"`
|
|
backupRepo repository.DbBackup `inject:"DbBackupRepo"`
|
|
backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
|
|
restoreRepo repository.DbRestore `inject:"DbRestoreRepo"`
|
|
restoreHistoryRepo repository.DbRestoreHistory `inject:"DbRestoreHistoryRepo"`
|
|
binlogRepo repository.DbBinlog `inject:"DbBinlogRepo"`
|
|
binlogHistoryRepo repository.DbBinlogHistory `inject:"DbBinlogHistoryRepo"`
|
|
sfGroup singleflight.Group
|
|
}
|
|
|
|
func newDbScheduler() *dbScheduler {
|
|
scheduler := &dbScheduler{}
|
|
scheduler.runner = runner.NewRunner[entity.DbJob](maxRunning, scheduler.runJob,
|
|
runner.WithScheduleJob[entity.DbJob](scheduler.scheduleJob),
|
|
runner.WithRunnableJob[entity.DbJob](scheduler.runnableJob),
|
|
runner.WithUpdateJob[entity.DbJob](scheduler.updateJob),
|
|
)
|
|
return scheduler
|
|
}
|
|
|
|
func (s *dbScheduler) scheduleJob(job entity.DbJob) (time.Time, error) {
|
|
return job.Schedule()
|
|
}
|
|
|
|
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
_ = s.runner.Update(ctx, job)
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) Close() {
|
|
s.runner.Close()
|
|
}
|
|
|
|
func (s *dbScheduler) AddJob(ctx context.Context, jobs any) error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
reflectValue := reflect.ValueOf(jobs)
|
|
switch reflectValue.Kind() {
|
|
case reflect.Array, reflect.Slice:
|
|
reflectLen := reflectValue.Len()
|
|
for i := 0; i < reflectLen; i++ {
|
|
job := reflectValue.Index(i).Interface().(entity.DbJob)
|
|
_ = s.runner.Add(ctx, job)
|
|
}
|
|
default:
|
|
job := jobs.(entity.DbJob)
|
|
_ = s.runner.Add(ctx, job)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
if err := s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) EnableJob(ctx context.Context, job entity.DbJob) error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
_ = s.runner.Add(ctx, job)
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) StartJobNow(ctx context.Context, job entity.DbJob) error {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
_ = s.runner.StartNow(ctx, job)
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) backup(ctx context.Context, dbProgram dbi.DbProgram, backup *entity.DbBackup) error {
|
|
id, err := NewIncUUID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
history := &entity.DbBackupHistory{
|
|
Uuid: id.String(),
|
|
DbBackupId: backup.Id,
|
|
DbInstanceId: backup.DbInstanceId,
|
|
DbName: backup.DbName,
|
|
}
|
|
binlogInfo, err := dbProgram.Backup(ctx, history)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
name := backup.DbName
|
|
if len(backup.Name) > 0 {
|
|
name = fmt.Sprintf("%s-%s", backup.DbName, backup.Name)
|
|
}
|
|
history.Name = fmt.Sprintf("%s[%s]", name, now.Format(time.DateTime))
|
|
history.CreateTime = now
|
|
history.BinlogFileName = binlogInfo.FileName
|
|
history.BinlogSequence = binlogInfo.Sequence
|
|
history.BinlogPosition = binlogInfo.Position
|
|
|
|
if err := s.backupHistoryRepo.Insert(ctx, history); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) singleFlightFetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, targetTime time.Time) error {
|
|
key := strconv.FormatUint(instanceId, 10)
|
|
for ctx.Err() == nil {
|
|
c := s.sfGroup.DoChan(key, func() (interface{}, error) {
|
|
if err := s.fetchBinlog(ctx, dbProgram, instanceId, true, targetTime); err != nil {
|
|
return targetTime, err
|
|
}
|
|
return targetTime, nil
|
|
})
|
|
select {
|
|
case res := <-c:
|
|
if targetTime.Compare(res.Val.(time.Time)) <= 0 {
|
|
return res.Err
|
|
}
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
func (s *dbScheduler) restore(ctx context.Context, dbProgram dbi.DbProgram, restore *entity.DbRestore) error {
|
|
if restore.PointInTime.Valid {
|
|
if err := s.fetchBinlog(ctx, dbProgram, restore.DbInstanceId, true, restore.PointInTime.Time); err != nil {
|
|
return err
|
|
}
|
|
if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
backupHistory, err := s.backupHistoryRepo.GetById(restore.DbBackupHistoryId)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
err = errors.New("备份历史已删除")
|
|
}
|
|
return err
|
|
}
|
|
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
history := &entity.DbRestoreHistory{
|
|
CreateTime: time.Now(),
|
|
DbRestoreId: restore.Id,
|
|
}
|
|
if err := s.restoreHistoryRepo.Insert(ctx, history); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) updateJob(ctx context.Context, job entity.DbJob) error {
|
|
switch t := job.(type) {
|
|
case *entity.DbBackup:
|
|
return s.backupRepo.UpdateById(ctx, t)
|
|
case *entity.DbRestore:
|
|
return s.restoreRepo.UpdateById(ctx, t)
|
|
case *entity.DbBinlog:
|
|
return s.binlogRepo.UpdateById(ctx, t)
|
|
default:
|
|
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
|
}
|
|
}
|
|
|
|
func (s *dbScheduler) runJob(ctx context.Context, job entity.DbJob) error {
|
|
conn, err := s.dbApp.GetDbConnByInstanceId(job.GetInstanceId())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbProgram, err := conn.GetDialect().GetDbProgram()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch t := job.(type) {
|
|
case *entity.DbBackup:
|
|
return s.backup(ctx, dbProgram, t)
|
|
case *entity.DbRestore:
|
|
return s.restore(ctx, dbProgram, t)
|
|
case *entity.DbBinlog:
|
|
return s.fetchBinlog(ctx, dbProgram, t.DbInstanceId, false, time.Now())
|
|
default:
|
|
return fmt.Errorf("无效的数据库任务类型: %T", t)
|
|
}
|
|
}
|
|
|
|
func (s *dbScheduler) runnableJob(job entity.DbJob, nextRunning runner.NextJobFunc[entity.DbJob]) (bool, error) {
|
|
if job.IsExpired() {
|
|
return false, runner.ErrJobExpired
|
|
}
|
|
const maxCountByInstanceId = 4
|
|
const maxCountByDbName = 1
|
|
var countByInstanceId, countByDbName int
|
|
for item, ok := nextRunning(); ok; item, ok = nextRunning() {
|
|
if job.GetInstanceId() == item.GetInstanceId() {
|
|
countByInstanceId++
|
|
if countByInstanceId >= maxCountByInstanceId {
|
|
return false, nil
|
|
}
|
|
if job.GetDbName() == item.GetDbName() {
|
|
countByDbName++
|
|
if countByDbName >= maxCountByDbName {
|
|
return false, nil
|
|
}
|
|
}
|
|
if (job.GetJobType() == entity.DbJobTypeBinlog && item.GetJobType() == entity.DbJobTypeRestore) ||
|
|
(job.GetJobType() == entity.DbJobTypeRestore && item.GetJobType() == entity.DbJobTypeBinlog) {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *dbScheduler) restorePointInTime(ctx context.Context, dbProgram dbi.DbProgram, job *entity.DbRestore) error {
|
|
binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
position, err := dbProgram.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, job.PointInTime.Time)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target := &entity.BinlogInfo{
|
|
FileName: binlogHistory.FileName,
|
|
Sequence: binlogHistory.Sequence,
|
|
Position: position,
|
|
}
|
|
backupHistory, err := s.backupHistoryRepo.GetLatestHistoryForBinlog(job.DbInstanceId, job.DbName, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
start := &entity.BinlogInfo{
|
|
FileName: backupHistory.BinlogFileName,
|
|
Sequence: backupHistory.BinlogSequence,
|
|
Position: backupHistory.BinlogPosition,
|
|
}
|
|
binlogHistories, err := s.binlogHistoryRepo.GetHistories(job.DbInstanceId, start, target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
restoreInfo := &dbi.RestoreInfo{
|
|
BackupHistory: backupHistory,
|
|
BinlogHistories: binlogHistories,
|
|
StartPosition: backupHistory.BinlogPosition,
|
|
TargetPosition: target.Position,
|
|
TargetTime: job.PointInTime.Time,
|
|
}
|
|
if err := dbProgram.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo); err != nil {
|
|
return err
|
|
}
|
|
if err := s.restoreBackupHistory(ctx, dbProgram, backupHistory); err != nil {
|
|
return err
|
|
}
|
|
// 由于 ReplayBinlog 未记录 BINLOG 事件,系统自动备份,避免数据丢失
|
|
backup := &entity.DbBackup{
|
|
DbInstanceId: backupHistory.DbInstanceId,
|
|
DbName: backupHistory.DbName,
|
|
Enabled: true,
|
|
Repeated: false,
|
|
StartTime: time.Now(),
|
|
Interval: 0,
|
|
Name: "系统备份",
|
|
}
|
|
backup.Id = backupHistory.DbBackupId
|
|
if err := s.backup(ctx, dbProgram, backup); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbi.DbProgram, backupHistory *entity.DbBackupHistory) (retErr error) {
|
|
if _, err := s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id); err != nil {
|
|
return err
|
|
}
|
|
ok, err := s.backupHistoryRepo.UpdateRestoring(true, backupHistory.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_, err = s.backupHistoryRepo.UpdateRestoring(false, backupHistory.Id)
|
|
if err == nil {
|
|
return
|
|
}
|
|
if retErr == nil {
|
|
retErr = err
|
|
return
|
|
}
|
|
retErr = fmt.Errorf("%w, %w", retErr, err)
|
|
}()
|
|
if !ok {
|
|
return errors.New("关联的数据库备份历史已删除")
|
|
}
|
|
return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
|
|
}
|
|
|
|
func (s *dbScheduler) fetchBinlog(ctx context.Context, dbProgram dbi.DbProgram, instanceId uint64, downloadLatestBinlogFile bool, targetTime time.Time) error {
|
|
if enabled, err := dbProgram.CheckBinlogEnabled(ctx); err != nil {
|
|
return err
|
|
} else if !enabled {
|
|
return errors.New("数据库未启用 BINLOG")
|
|
}
|
|
if enabled, err := dbProgram.CheckBinlogRowFormat(ctx); err != nil {
|
|
return err
|
|
} else if !enabled {
|
|
return errors.New("数据库未启用 BINLOG 行模式")
|
|
}
|
|
|
|
earliestBackupSequence := int64(-1)
|
|
binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(instanceId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if downloadLatestBinlogFile && targetTime.Before(binlogHistory.LastEventTime) {
|
|
return nil
|
|
}
|
|
|
|
if !ok {
|
|
backupHistory, ok, err := s.backupHistoryRepo.GetEarliestHistoryForBinlog(instanceId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return nil
|
|
}
|
|
earliestBackupSequence = backupHistory.BinlogSequence
|
|
}
|
|
|
|
// todo: 将循环从 dbProgram.FetchBinlogs 中提取出来,实现 BINLOG 同步成功后逐一保存 binlogHistory
|
|
binlogFiles, err := dbProgram.FetchBinlogs(ctx, downloadLatestBinlogFile, earliestBackupSequence, binlogHistory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, instanceId, binlogFiles)
|
|
}
|