mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 16:30:25 +08:00 
			
		
		
		
	feat: 实现数据库备份和恢复并发调度 (#84)
This commit is contained in:
		@@ -93,3 +93,5 @@ require (
 | 
			
		||||
	modernc.org/sqlite v1.23.1 // indirect
 | 
			
		||||
	vitess.io/vitess v0.17.3 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require github.com/emirpasic/gods v1.18.1
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ func (d *DbBackup) GetPageList(rc *req.Ctx) {
 | 
			
		||||
	db, err := d.DbApp.GetById(new(entity.Db), dbId, "db_instance_id", "database")
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
 | 
			
		||||
 | 
			
		||||
	queryCond, page := ginx.BindQueryAndPage[*entity.DbBackupQuery](rc.GinCtx, new(entity.DbBackupQuery))
 | 
			
		||||
	queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
 | 
			
		||||
	queryCond.DbInstanceId = db.InstanceId
 | 
			
		||||
	queryCond.InDbNames = strings.Fields(db.Database)
 | 
			
		||||
	res, err := d.DbBackupApp.GetPageList(queryCond, page, new([]vo.DbBackup))
 | 
			
		||||
@@ -51,32 +51,30 @@ func (d *DbBackup) Create(rc *req.Ctx) {
 | 
			
		||||
	db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
 | 
			
		||||
 | 
			
		||||
	tasks := make([]*entity.DbBackup, 0, len(dbNames))
 | 
			
		||||
	jobs := make([]*entity.DbBackup, 0, len(dbNames))
 | 
			
		||||
	for _, dbName := range dbNames {
 | 
			
		||||
		task := &entity.DbBackup{
 | 
			
		||||
			DbTaskBase:   entity.NewDbBTaskBase(true, backupForm.Repeated, backupForm.StartTime, backupForm.Interval),
 | 
			
		||||
			DbName:       dbName,
 | 
			
		||||
			Name:         backupForm.Name,
 | 
			
		||||
			DbInstanceId: db.InstanceId,
 | 
			
		||||
		job := &entity.DbBackup{
 | 
			
		||||
			DbJobBaseImpl: entity.NewDbBJobBase(db.InstanceId, dbName, entity.DbJobTypeBackup, true, backupForm.Repeated, backupForm.StartTime, backupForm.Interval),
 | 
			
		||||
			Name:          backupForm.Name,
 | 
			
		||||
		}
 | 
			
		||||
		tasks = append(tasks, task)
 | 
			
		||||
		jobs = append(jobs, job)
 | 
			
		||||
	}
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, tasks...), "添加数据库备份任务失败: %v")
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbBackupApp.Create(rc.MetaCtx, jobs), "添加数据库备份任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save 保存数据库备份任务
 | 
			
		||||
// Update 保存数据库备份任务
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:backupId [PUT]
 | 
			
		||||
func (d *DbBackup) Save(rc *req.Ctx) {
 | 
			
		||||
func (d *DbBackup) Update(rc *req.Ctx) {
 | 
			
		||||
	backupForm := &form.DbBackupForm{}
 | 
			
		||||
	ginx.BindJsonAndValid(rc.GinCtx, backupForm)
 | 
			
		||||
	rc.ReqParam = backupForm
 | 
			
		||||
 | 
			
		||||
	task := &entity.DbBackup{}
 | 
			
		||||
	task.Id = backupForm.Id
 | 
			
		||||
	task.Name = backupForm.Name
 | 
			
		||||
	task.StartTime = backupForm.StartTime
 | 
			
		||||
	task.Interval = backupForm.Interval
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbBackupApp.Save(rc.MetaCtx, task), "保存数据库备份任务失败: %v")
 | 
			
		||||
	job := entity.NewDbJob(entity.DbJobTypeBackup).(*entity.DbBackup)
 | 
			
		||||
	job.Id = backupForm.Id
 | 
			
		||||
	job.Name = backupForm.Name
 | 
			
		||||
	job.StartTime = backupForm.StartTime
 | 
			
		||||
	job.Interval = backupForm.Interval
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbBackupApp.Update(rc.MetaCtx, job), "保存数据库备份任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint64) error) error {
 | 
			
		||||
@@ -89,8 +87,8 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		taskId := uint64(value)
 | 
			
		||||
		err = fn(rc.MetaCtx, taskId)
 | 
			
		||||
		backupId := uint64(value)
 | 
			
		||||
		err = fn(rc.MetaCtx, backupId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -99,28 +97,28 @@ func (d *DbBackup) walk(rc *req.Ctx, fn func(ctx context.Context, backupId uint6
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete 删除数据库备份任务
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:taskId [DELETE]
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:backupId [DELETE]
 | 
			
		||||
func (d *DbBackup) Delete(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbBackupApp.Delete)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "删除数据库备份任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Enable 启用数据库备份任务
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:taskId/enable [PUT]
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:backupId/enable [PUT]
 | 
			
		||||
func (d *DbBackup) Enable(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbBackupApp.Enable)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "启用数据库备份任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Disable 禁用数据库备份任务
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:taskId/disable [PUT]
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:backupId/disable [PUT]
 | 
			
		||||
func (d *DbBackup) Disable(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbBackupApp.Disable)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "禁用数据库备份任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Start 禁用数据库备份任务
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:taskId/start [PUT]
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:backupId/start [PUT]
 | 
			
		||||
func (d *DbBackup) Start(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbBackupApp.Start)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "运行数据库备份任务失败: %v")
 | 
			
		||||
@@ -138,7 +136,7 @@ func (d *DbBackup) GetDbNamesWithoutBackup(rc *req.Ctx) {
 | 
			
		||||
	rc.ResData = dbNamesWithoutBackup
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPageList 获取数据库备份历史
 | 
			
		||||
// GetHistoryPageList 获取数据库备份历史
 | 
			
		||||
// @router /api/dbs/:dbId/backups/:backupId/histories [GET]
 | 
			
		||||
func (d *DbBackup) GetHistoryPageList(rc *req.Ctx) {
 | 
			
		||||
	dbId := uint64(ginx.PathParamInt(rc.GinCtx, "dbId"))
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ func (d *DbRestore) GetPageList(rc *req.Ctx) {
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
 | 
			
		||||
 | 
			
		||||
	var restores []vo.DbRestore
 | 
			
		||||
	queryCond, page := ginx.BindQueryAndPage[*entity.DbRestoreQuery](rc.GinCtx, new(entity.DbRestoreQuery))
 | 
			
		||||
	queryCond, page := ginx.BindQueryAndPage[*entity.DbJobQuery](rc.GinCtx, new(entity.DbJobQuery))
 | 
			
		||||
	queryCond.DbInstanceId = db.InstanceId
 | 
			
		||||
	queryCond.InDbNames = strings.Fields(db.Database)
 | 
			
		||||
	res, err := d.DbRestoreApp.GetPageList(queryCond, page, &restores)
 | 
			
		||||
@@ -47,33 +47,31 @@ func (d *DbRestore) Create(rc *req.Ctx) {
 | 
			
		||||
	db, err := d.DbApp.GetById(new(entity.Db), dbId, "instanceId")
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "获取数据库信息失败: %v")
 | 
			
		||||
 | 
			
		||||
	task := &entity.DbRestore{
 | 
			
		||||
		DbTaskBase:          entity.NewDbBTaskBase(true, restoreForm.Repeated, restoreForm.StartTime, restoreForm.Interval),
 | 
			
		||||
		DbName:              restoreForm.DbName,
 | 
			
		||||
		DbInstanceId:        db.InstanceId,
 | 
			
		||||
	job := &entity.DbRestore{
 | 
			
		||||
		DbJobBaseImpl:       entity.NewDbBJobBase(db.InstanceId, restoreForm.DbName, entity.DbJobTypeRestore, true, restoreForm.Repeated, restoreForm.StartTime, restoreForm.Interval),
 | 
			
		||||
		PointInTime:         restoreForm.PointInTime,
 | 
			
		||||
		DbBackupId:          restoreForm.DbBackupId,
 | 
			
		||||
		DbBackupHistoryId:   restoreForm.DbBackupHistoryId,
 | 
			
		||||
		DbBackupHistoryName: restoreForm.DbBackupHistoryName,
 | 
			
		||||
	}
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, task), "添加数据库恢复任务失败: %v")
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbRestoreApp.Create(rc.MetaCtx, job), "添加数据库恢复任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Save 保存数据库恢复任务
 | 
			
		||||
// Update 保存数据库恢复任务
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:restoreId [PUT]
 | 
			
		||||
func (d *DbRestore) Save(rc *req.Ctx) {
 | 
			
		||||
func (d *DbRestore) Update(rc *req.Ctx) {
 | 
			
		||||
	restoreForm := &form.DbRestoreForm{}
 | 
			
		||||
	ginx.BindJsonAndValid(rc.GinCtx, restoreForm)
 | 
			
		||||
	rc.ReqParam = restoreForm
 | 
			
		||||
 | 
			
		||||
	task := &entity.DbRestore{}
 | 
			
		||||
	task.Id = restoreForm.Id
 | 
			
		||||
	task.StartTime = restoreForm.StartTime
 | 
			
		||||
	task.Interval = restoreForm.Interval
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbRestoreApp.Save(rc.MetaCtx, task), "保存数据库恢复任务失败: %v")
 | 
			
		||||
	job := &entity.DbRestore{}
 | 
			
		||||
	job.Id = restoreForm.Id
 | 
			
		||||
	job.StartTime = restoreForm.StartTime
 | 
			
		||||
	job.Interval = restoreForm.Interval
 | 
			
		||||
	biz.ErrIsNilAppendErr(d.DbRestoreApp.Update(rc.MetaCtx, job), "保存数据库恢复任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, taskId uint64) error) error {
 | 
			
		||||
func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, restoreId uint64) error) error {
 | 
			
		||||
	idsStr := ginx.PathParam(rc.GinCtx, "restoreId")
 | 
			
		||||
	biz.NotEmpty(idsStr, "restoreId 为空")
 | 
			
		||||
	rc.ReqParam = idsStr
 | 
			
		||||
@@ -83,8 +81,8 @@ func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, taskId uint64
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		taskId := uint64(value)
 | 
			
		||||
		err = fn(rc.MetaCtx, taskId)
 | 
			
		||||
		restoreId := uint64(value)
 | 
			
		||||
		err = fn(rc.MetaCtx, restoreId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -92,19 +90,22 @@ func (d *DbRestore) walk(rc *req.Ctx, fn func(ctx context.Context, taskId uint64
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:taskId [DELETE]
 | 
			
		||||
// Delete 删除数据库恢复任务
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:restoreId [DELETE]
 | 
			
		||||
func (d *DbRestore) Delete(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbRestoreApp.Delete)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "删除数据库恢复任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:taskId/enable [PUT]
 | 
			
		||||
// Enable 启用数据库恢复任务
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:restoreId/enable [PUT]
 | 
			
		||||
func (d *DbRestore) Enable(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbRestoreApp.Enable)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "启用数据库恢复任务失败: %v")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:taskId/disable [PUT]
 | 
			
		||||
// Disable 禁用数据库恢复任务
 | 
			
		||||
// @router /api/dbs/:dbId/restores/:restoreId/disable [PUT]
 | 
			
		||||
func (d *DbRestore) Disable(rc *req.Ctx) {
 | 
			
		||||
	err := d.walk(rc, d.DbRestoreApp.Disable)
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "禁用数据库恢复任务失败: %v")
 | 
			
		||||
 
 | 
			
		||||
@@ -19,11 +19,13 @@ var (
 | 
			
		||||
	dataSyncApp  DataSyncTask
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var repositories *repository.Repositories
 | 
			
		||||
//var repositories *repository.Repositories
 | 
			
		||||
//var scheduler *dbScheduler[*entity.DbBackup]
 | 
			
		||||
//var scheduler1 *dbScheduler[*entity.DbRestore]
 | 
			
		||||
 | 
			
		||||
func Init() {
 | 
			
		||||
	sync.OnceFunc(func() {
 | 
			
		||||
		repositories = &repository.Repositories{
 | 
			
		||||
		repositories := &repository.Repositories{
 | 
			
		||||
			Instance:       persistence.GetInstanceRepo(),
 | 
			
		||||
			Backup:         persistence.NewDbBackupRepo(),
 | 
			
		||||
			BackupHistory:  persistence.NewDbBackupHistoryRepo(),
 | 
			
		||||
@@ -40,15 +42,18 @@ func Init() {
 | 
			
		||||
		dbSqlApp = newDbSqlApp(persistence.GetDbSqlRepo())
 | 
			
		||||
		dataSyncApp = newDataSyncApp(persistence.GetDataSyncTaskRepo(), persistence.GetDataSyncLogRepo())
 | 
			
		||||
 | 
			
		||||
		dbBackupApp, err = newDbBackupApp(repositories, dbApp)
 | 
			
		||||
		scheduler, err := newDbScheduler(repositories)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(fmt.Sprintf("初始化 dbScheduler 失败: %v", err))
 | 
			
		||||
		}
 | 
			
		||||
		dbBackupApp, err = newDbBackupApp(repositories, dbApp, scheduler)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(fmt.Sprintf("初始化 dbBackupApp 失败: %v", err))
 | 
			
		||||
		}
 | 
			
		||||
		dbRestoreApp, err = newDbRestoreApp(repositories, dbApp)
 | 
			
		||||
		dbRestoreApp, err = newDbRestoreApp(repositories, dbApp, scheduler)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(fmt.Sprintf("初始化 dbRestoreApp 失败: %v", err))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		dbBinlogApp, err = newDbBinlogApp(repositories, dbApp)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(fmt.Sprintf("初始化 dbBinlogApp 失败: %v", err))
 | 
			
		||||
 
 | 
			
		||||
@@ -3,29 +3,27 @@ package application
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func newDbBackupApp(repositories *repository.Repositories, dbApp Db) (*DbBackupApp, error) {
 | 
			
		||||
func newDbBackupApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbBackupApp, error) {
 | 
			
		||||
	var jobs []*entity.DbBackup
 | 
			
		||||
	if err := repositories.Backup.ListToDo(&jobs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeBackup, jobs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	app := &DbBackupApp{
 | 
			
		||||
		backupRepo:        repositories.Backup,
 | 
			
		||||
		instanceRepo:      repositories.Instance,
 | 
			
		||||
		backupHistoryRepo: repositories.BackupHistory,
 | 
			
		||||
		dbApp:             dbApp,
 | 
			
		||||
		scheduler:         scheduler,
 | 
			
		||||
	}
 | 
			
		||||
	scheduler, err := newDbScheduler[*entity.DbBackup](
 | 
			
		||||
		repositories.Backup,
 | 
			
		||||
		withRunBackupTask(app))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	app.scheduler = scheduler
 | 
			
		||||
	return app, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -34,41 +32,41 @@ type DbBackupApp struct {
 | 
			
		||||
	instanceRepo      repository.Instance
 | 
			
		||||
	backupHistoryRepo repository.DbBackupHistory
 | 
			
		||||
	dbApp             Db
 | 
			
		||||
	scheduler         *dbScheduler[*entity.DbBackup]
 | 
			
		||||
	scheduler         *dbScheduler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Close() {
 | 
			
		||||
	app.scheduler.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Create(ctx context.Context, tasks ...*entity.DbBackup) error {
 | 
			
		||||
	return app.scheduler.AddTask(ctx, tasks...)
 | 
			
		||||
func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
 | 
			
		||||
	return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeBackup, jobs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Save(ctx context.Context, task *entity.DbBackup) error {
 | 
			
		||||
	return app.scheduler.UpdateTask(ctx, task)
 | 
			
		||||
func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
 | 
			
		||||
	return app.scheduler.UpdateJob(ctx, job)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Delete(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	// todo: 删除数据库备份历史文件
 | 
			
		||||
	return app.scheduler.DeleteTask(ctx, taskId)
 | 
			
		||||
	return app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Enable(ctx context.Context, taskId uint64) error {
 | 
			
		||||
	return app.scheduler.EnableTask(ctx, taskId)
 | 
			
		||||
func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	return app.scheduler.EnableJob(ctx, entity.DbJobTypeBackup, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Disable(ctx context.Context, taskId uint64) error {
 | 
			
		||||
	return app.scheduler.DisableTask(ctx, taskId)
 | 
			
		||||
func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	return app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) Start(ctx context.Context, taskId uint64) error {
 | 
			
		||||
	return app.scheduler.StartTask(ctx, taskId)
 | 
			
		||||
func (app *DbBackupApp) Start(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	return app.scheduler.StartJobNow(ctx, entity.DbJobTypeBackup, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPageList 分页获取数据库备份任务
 | 
			
		||||
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	return app.backupRepo.GetDbBackupList(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
func (app *DbBackupApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
 | 
			
		||||
@@ -76,54 +74,11 @@ func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []str
 | 
			
		||||
	return app.backupRepo.GetDbNamesWithoutBackup(instanceId, dbNames)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPageList 分页获取数据库备份历史
 | 
			
		||||
// GetHistoryPageList 分页获取数据库备份历史
 | 
			
		||||
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	return app.backupHistoryRepo.GetHistories(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func withRunBackupTask(app *DbBackupApp) dbSchedulerOption[*entity.DbBackup] {
 | 
			
		||||
	return func(scheduler *dbScheduler[*entity.DbBackup]) {
 | 
			
		||||
		scheduler.RunTask = app.runTask
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBackupApp) runTask(ctx context.Context, task *entity.DbBackup) error {
 | 
			
		||||
	id, err := NewIncUUID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	history := &entity.DbBackupHistory{
 | 
			
		||||
		Uuid:         id.String(),
 | 
			
		||||
		DbBackupId:   task.Id,
 | 
			
		||||
		DbInstanceId: task.DbInstanceId,
 | 
			
		||||
		DbName:       task.DbName,
 | 
			
		||||
	}
 | 
			
		||||
	conn, err := app.dbApp.GetDbConnByInstanceId(task.DbInstanceId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	dbProgram := conn.GetDialect().GetDbProgram()
 | 
			
		||||
	binlogInfo, err := dbProgram.Backup(ctx, history)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	name := task.Name
 | 
			
		||||
	if len(name) == 0 {
 | 
			
		||||
		name = task.DbName
 | 
			
		||||
	}
 | 
			
		||||
	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 := app.backupHistoryRepo.Insert(ctx, history); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewIncUUID() (uuid.UUID, error) {
 | 
			
		||||
	var uid uuid.UUID
 | 
			
		||||
	now, seq, err := uuid.GetTime()
 | 
			
		||||
@@ -144,19 +99,3 @@ func NewIncUUID() (uuid.UUID, error) {
 | 
			
		||||
 | 
			
		||||
	return uid, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// func newDbBackupHistoryApp(repositories *repository.Repositories) (*DbBackupHistoryApp, error) {
 | 
			
		||||
// 	app := &DbBackupHistoryApp{
 | 
			
		||||
// 		repo: repositories.BackupHistory,
 | 
			
		||||
// 	}
 | 
			
		||||
// 	return app, nil
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// type DbBackupHistoryApp struct {
 | 
			
		||||
// 	repo repository.DbBackupHistory
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// // GetPageList 分页获取数据库备份历史
 | 
			
		||||
// func (app *DbBackupHistoryApp) GetPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
// 	return app.repo.GetHistories(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
// }
 | 
			
		||||
 
 | 
			
		||||
@@ -28,12 +28,12 @@ type DbBinlogApp struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	binlogResult = map[entity.TaskStatus]string{
 | 
			
		||||
		entity.TaskDelay:    "等待备份BINLOG",
 | 
			
		||||
		entity.TaskReady:    "准备备份BINLOG",
 | 
			
		||||
		entity.TaskReserved: "BINLOG备份中",
 | 
			
		||||
		entity.TaskSuccess:  "BINLOG备份成功",
 | 
			
		||||
		entity.TaskFailed:   "BINLOG备份失败",
 | 
			
		||||
	binlogResult = map[entity.DbJobStatus]string{
 | 
			
		||||
		entity.DbJobDelay:   "等待备份BINLOG",
 | 
			
		||||
		entity.DbJobReady:   "准备备份BINLOG",
 | 
			
		||||
		entity.DbJobRunning: "BINLOG备份中",
 | 
			
		||||
		entity.DbJobSuccess: "BINLOG备份成功",
 | 
			
		||||
		entity.DbJobFailed:  "BINLOG备份失败",
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -53,8 +53,8 @@ func newDbBinlogApp(repositories *repository.Repositories, dbApp Db) (*DbBinlogA
 | 
			
		||||
	return svc, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBinlogApp) runTask(ctx context.Context, backup *entity.DbBackup) error {
 | 
			
		||||
	if err := app.AddTaskIfNotExists(ctx, entity.NewDbBinlog(backup.DbInstanceId)); err != nil {
 | 
			
		||||
func (app *DbBinlogApp) fetchBinlog(ctx context.Context, backup *entity.DbBackup) error {
 | 
			
		||||
	if err := app.AddJobIfNotExists(ctx, entity.NewDbBinlog(backup.DbInstanceId)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
 | 
			
		||||
@@ -80,13 +80,13 @@ func (app *DbBinlogApp) runTask(ctx context.Context, backup *entity.DbBackup) er
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		err = app.binlogHistoryRepo.InsertWithBinlogFiles(ctx, backup.DbInstanceId, binlogFiles)
 | 
			
		||||
	}
 | 
			
		||||
	taskStatus := entity.TaskSuccess
 | 
			
		||||
	jobStatus := entity.DbJobSuccess
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		taskStatus = entity.TaskFailed
 | 
			
		||||
		jobStatus = entity.DbJobFailed
 | 
			
		||||
	}
 | 
			
		||||
	task := &entity.DbBinlog{}
 | 
			
		||||
	task.Id = backup.DbInstanceId
 | 
			
		||||
	return app.updateCurTask(ctx, taskStatus, err, task)
 | 
			
		||||
	job := &entity.DbBinlog{}
 | 
			
		||||
	job.Id = backup.DbInstanceId
 | 
			
		||||
	return app.updateCurJob(ctx, jobStatus, err, job)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBinlogApp) run() {
 | 
			
		||||
@@ -99,16 +99,16 @@ func (app *DbBinlogApp) run() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBinlogApp) fetchFromAllInstances() {
 | 
			
		||||
	tasks, err := app.backupRepo.ListRepeating()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	var backups []*entity.DbBackup
 | 
			
		||||
	if err := app.backupRepo.ListRepeating(&backups); err != nil {
 | 
			
		||||
		logx.Errorf("DbBinlogApp: 获取数据库备份任务失败: %s", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, task := range tasks {
 | 
			
		||||
	for _, backup := range backups {
 | 
			
		||||
		if app.closed() {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if err := app.runTask(app.context, task); err != nil {
 | 
			
		||||
		if err := app.fetchBinlog(app.context, backup); err != nil {
 | 
			
		||||
			logx.Errorf("DbBinlogApp: 下载 binlog 文件失败: %s", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
@@ -124,31 +124,31 @@ func (app *DbBinlogApp) closed() bool {
 | 
			
		||||
	return app.context.Err() != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBinlogApp) AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error {
 | 
			
		||||
	if err := app.binlogRepo.AddTaskIfNotExists(ctx, task); err != nil {
 | 
			
		||||
func (app *DbBinlogApp) AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error {
 | 
			
		||||
	if err := app.binlogRepo.AddJobIfNotExists(ctx, job); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if task.Id == 0 {
 | 
			
		||||
	if job.Id == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBinlogApp) DeleteTask(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (app *DbBinlogApp) Delete(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	// todo: 删除 Binlog 历史文件
 | 
			
		||||
	if err := app.binlogRepo.DeleteById(ctx, taskId); err != nil {
 | 
			
		||||
	if err := app.binlogRepo.DeleteById(ctx, jobId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbBinlogApp) updateCurTask(ctx context.Context, status entity.TaskStatus, lastErr error, task *entity.DbBinlog) error {
 | 
			
		||||
	task.LastStatus = status
 | 
			
		||||
func (app *DbBinlogApp) updateCurJob(ctx context.Context, status entity.DbJobStatus, lastErr error, job *entity.DbBinlog) error {
 | 
			
		||||
	job.LastStatus = status
 | 
			
		||||
	var result = binlogResult[status]
 | 
			
		||||
	if lastErr != nil {
 | 
			
		||||
		result = fmt.Sprintf("%v: %v", binlogResult[status], lastErr)
 | 
			
		||||
	}
 | 
			
		||||
	task.LastResult = stringx.TruncateStr(result, entity.LastResultSize)
 | 
			
		||||
	task.LastTime = timex.NewNullTime(time.Now())
 | 
			
		||||
	return app.binlogRepo.UpdateById(ctx, task, "last_status", "last_result", "last_time")
 | 
			
		||||
	job.LastResult = stringx.TruncateStr(result, entity.LastResultSize)
 | 
			
		||||
	job.LastTime = timex.NewNullTime(time.Now())
 | 
			
		||||
	return app.binlogRepo.UpdateById(ctx, job, "last_status", "last_result", "last_time")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,19 @@ package application
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/db/dbm"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db) (*DbRestoreApp, error) {
 | 
			
		||||
func newDbRestoreApp(repositories *repository.Repositories, dbApp Db, scheduler *dbScheduler) (*DbRestoreApp, error) {
 | 
			
		||||
	var jobs []*entity.DbRestore
 | 
			
		||||
	if err := repositories.Restore.ListToDo(&jobs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := scheduler.AddJob(context.Background(), false, entity.DbJobTypeRestore, jobs); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	app := &DbRestoreApp{
 | 
			
		||||
		restoreRepo:        repositories.Restore,
 | 
			
		||||
		instanceRepo:       repositories.Instance,
 | 
			
		||||
@@ -17,14 +22,8 @@ func newDbRestoreApp(repositories *repository.Repositories, dbApp Db) (*DbRestor
 | 
			
		||||
		restoreHistoryRepo: repositories.RestoreHistory,
 | 
			
		||||
		binlogHistoryRepo:  repositories.BinlogHistory,
 | 
			
		||||
		dbApp:              dbApp,
 | 
			
		||||
		scheduler:          scheduler,
 | 
			
		||||
	}
 | 
			
		||||
	scheduler, err := newDbScheduler[*entity.DbRestore](
 | 
			
		||||
		repositories.Restore,
 | 
			
		||||
		withRunRestoreTask(app))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	app.scheduler = scheduler
 | 
			
		||||
	return app, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -35,37 +34,37 @@ type DbRestoreApp struct {
 | 
			
		||||
	restoreHistoryRepo repository.DbRestoreHistory
 | 
			
		||||
	binlogHistoryRepo  repository.DbBinlogHistory
 | 
			
		||||
	dbApp              Db
 | 
			
		||||
	scheduler          *dbScheduler[*entity.DbRestore]
 | 
			
		||||
	scheduler          *dbScheduler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) Close() {
 | 
			
		||||
	app.scheduler.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) Create(ctx context.Context, tasks ...*entity.DbRestore) error {
 | 
			
		||||
	return app.scheduler.AddTask(ctx, tasks...)
 | 
			
		||||
func (app *DbRestoreApp) Create(ctx context.Context, job *entity.DbRestore) error {
 | 
			
		||||
	return app.scheduler.AddJob(ctx, true /* 保存到数据库 */, entity.DbJobTypeRestore, job)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) Save(ctx context.Context, task *entity.DbRestore) error {
 | 
			
		||||
	return app.scheduler.UpdateTask(ctx, task)
 | 
			
		||||
func (app *DbRestoreApp) Update(ctx context.Context, job *entity.DbRestore) error {
 | 
			
		||||
	return app.scheduler.UpdateJob(ctx, job)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) Delete(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (app *DbRestoreApp) Delete(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	// todo: 删除数据库恢复历史文件
 | 
			
		||||
	return app.scheduler.DeleteTask(ctx, taskId)
 | 
			
		||||
	return app.scheduler.RemoveJob(ctx, entity.DbJobTypeRestore, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) Enable(ctx context.Context, taskId uint64) error {
 | 
			
		||||
	return app.scheduler.EnableTask(ctx, taskId)
 | 
			
		||||
func (app *DbRestoreApp) Enable(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	return app.scheduler.EnableJob(ctx, entity.DbJobTypeRestore, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) Disable(ctx context.Context, taskId uint64) error {
 | 
			
		||||
	return app.scheduler.DisableTask(ctx, taskId)
 | 
			
		||||
func (app *DbRestoreApp) Disable(ctx context.Context, jobId uint64) error {
 | 
			
		||||
	return app.scheduler.DisableJob(ctx, entity.DbJobTypeRestore, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPageList 分页获取数据库恢复任务
 | 
			
		||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	return app.restoreRepo.GetDbRestoreList(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
func (app *DbRestoreApp) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	return app.restoreRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDbNamesWithoutRestore 获取未配置定时恢复的数据库名称
 | 
			
		||||
@@ -73,108 +72,7 @@ func (app *DbRestoreApp) GetDbNamesWithoutRestore(instanceId uint64, dbNames []s
 | 
			
		||||
	return app.restoreRepo.GetDbNamesWithoutRestore(instanceId, dbNames)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 分页获取数据库备份历史
 | 
			
		||||
// GetHistoryPageList 分页获取数据库备份历史
 | 
			
		||||
func (app *DbRestoreApp) GetHistoryPageList(condition *entity.DbRestoreHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	return app.restoreHistoryRepo.GetDbRestoreHistories(condition, pageParam, toEntity, orderBy...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) runTask(ctx context.Context, task *entity.DbRestore) error {
 | 
			
		||||
	conn, err := app.dbApp.GetDbConnByInstanceId(task.DbInstanceId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	dbProgram := conn.GetDialect().GetDbProgram()
 | 
			
		||||
	if task.PointInTime.Valid {
 | 
			
		||||
		latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
 | 
			
		||||
		binlogHistory, ok, err := app.binlogHistoryRepo.GetLatestHistory(task.DbInstanceId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if ok {
 | 
			
		||||
			latestBinlogSequence = binlogHistory.Sequence
 | 
			
		||||
		} else {
 | 
			
		||||
			backupHistory, err := app.backupHistoryRepo.GetEarliestHistory(task.DbInstanceId)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			earliestBackupSequence = backupHistory.BinlogSequence
 | 
			
		||||
		}
 | 
			
		||||
		binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := app.binlogHistoryRepo.InsertWithBinlogFiles(ctx, task.DbInstanceId, binlogFiles); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := app.restorePointInTime(ctx, dbProgram, task); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if err := app.restoreBackupHistory(ctx, dbProgram, task); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	history := &entity.DbRestoreHistory{
 | 
			
		||||
		CreateTime:  time.Now(),
 | 
			
		||||
		DbRestoreId: task.Id,
 | 
			
		||||
	}
 | 
			
		||||
	if err := app.restoreHistoryRepo.Insert(ctx, history); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) restorePointInTime(ctx context.Context, program dbm.DbProgram, task *entity.DbRestore) error {
 | 
			
		||||
	binlogHistory, err := app.binlogHistoryRepo.GetHistoryByTime(task.DbInstanceId, task.PointInTime.Time)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	position, err := program.GetBinlogEventPositionAtOrAfterTime(ctx, binlogHistory.FileName, task.PointInTime.Time)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	target := &entity.BinlogInfo{
 | 
			
		||||
		FileName: binlogHistory.FileName,
 | 
			
		||||
		Sequence: binlogHistory.Sequence,
 | 
			
		||||
		Position: position,
 | 
			
		||||
	}
 | 
			
		||||
	backupHistory, err := app.backupHistoryRepo.GetLatestHistory(task.DbInstanceId, task.DbName, target)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	start := &entity.BinlogInfo{
 | 
			
		||||
		FileName: backupHistory.BinlogFileName,
 | 
			
		||||
		Sequence: backupHistory.BinlogSequence,
 | 
			
		||||
		Position: backupHistory.BinlogPosition,
 | 
			
		||||
	}
 | 
			
		||||
	binlogHistories, err := app.binlogHistoryRepo.GetHistories(task.DbInstanceId, start, target)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	restoreInfo := &dbm.RestoreInfo{
 | 
			
		||||
		BackupHistory:   backupHistory,
 | 
			
		||||
		BinlogHistories: binlogHistories,
 | 
			
		||||
		StartPosition:   backupHistory.BinlogPosition,
 | 
			
		||||
		TargetPosition:  target.Position,
 | 
			
		||||
		TargetTime:      task.PointInTime.Time,
 | 
			
		||||
	}
 | 
			
		||||
	if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return program.ReplayBinlog(ctx, task.DbName, task.DbName, restoreInfo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (app *DbRestoreApp) restoreBackupHistory(ctx context.Context, program dbm.DbProgram, task *entity.DbRestore) error {
 | 
			
		||||
	backupHistory := &entity.DbBackupHistory{}
 | 
			
		||||
	if err := app.backupHistoryRepo.GetById(backupHistory, task.DbBackupHistoryId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func withRunRestoreTask(app *DbRestoreApp) dbSchedulerOption[*entity.DbRestore] {
 | 
			
		||||
	return func(scheduler *dbScheduler[*entity.DbRestore]) {
 | 
			
		||||
		scheduler.RunTask = app.runTask
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,232 +4,360 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/db/dbm"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/queue"
 | 
			
		||||
	"mayfly-go/pkg/utils/anyx"
 | 
			
		||||
	"mayfly-go/pkg/utils/stringx"
 | 
			
		||||
	"mayfly-go/pkg/utils/timex"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/runner"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const sleepAfterError = time.Minute
 | 
			
		||||
const (
 | 
			
		||||
	maxRunning = 8
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type dbScheduler[T entity.DbTask] struct {
 | 
			
		||||
	mutex     sync.Mutex
 | 
			
		||||
	waitGroup sync.WaitGroup
 | 
			
		||||
	queue     *queue.DelayQueue[T]
 | 
			
		||||
	context   context.Context
 | 
			
		||||
	cancel    context.CancelFunc
 | 
			
		||||
	RunTask   func(ctx context.Context, task T) error
 | 
			
		||||
	taskRepo  repository.DbTask[T]
 | 
			
		||||
type dbScheduler struct {
 | 
			
		||||
	mutex              sync.Mutex
 | 
			
		||||
	runner             *runner.Runner[entity.DbJob]
 | 
			
		||||
	dbApp              Db
 | 
			
		||||
	backupRepo         repository.DbBackup
 | 
			
		||||
	backupHistoryRepo  repository.DbBackupHistory
 | 
			
		||||
	restoreRepo        repository.DbRestore
 | 
			
		||||
	restoreHistoryRepo repository.DbRestoreHistory
 | 
			
		||||
	binlogHistoryRepo  repository.DbBinlogHistory
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type dbSchedulerOption[T entity.DbTask] func(*dbScheduler[T])
 | 
			
		||||
 | 
			
		||||
func newDbScheduler[T entity.DbTask](taskRepo repository.DbTask[T], opts ...dbSchedulerOption[T]) (*dbScheduler[T], error) {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	scheduler := &dbScheduler[T]{
 | 
			
		||||
		taskRepo: taskRepo,
 | 
			
		||||
		queue:    queue.NewDelayQueue[T](0),
 | 
			
		||||
		context:  ctx,
 | 
			
		||||
		cancel:   cancel,
 | 
			
		||||
func newDbScheduler(repositories *repository.Repositories) (*dbScheduler, error) {
 | 
			
		||||
	scheduler := &dbScheduler{
 | 
			
		||||
		runner:             runner.NewRunner[entity.DbJob](maxRunning),
 | 
			
		||||
		dbApp:              dbApp,
 | 
			
		||||
		backupRepo:         repositories.Backup,
 | 
			
		||||
		backupHistoryRepo:  repositories.BackupHistory,
 | 
			
		||||
		restoreRepo:        repositories.Restore,
 | 
			
		||||
		restoreHistoryRepo: repositories.RestoreHistory,
 | 
			
		||||
		binlogHistoryRepo:  repositories.BinlogHistory,
 | 
			
		||||
	}
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt(scheduler)
 | 
			
		||||
	}
 | 
			
		||||
	if scheduler.RunTask == nil {
 | 
			
		||||
		return nil, errors.New("数据库任务调度器没有设置 RunTask")
 | 
			
		||||
	}
 | 
			
		||||
	if err := scheduler.loadTask(context.Background()); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	scheduler.waitGroup.Add(1)
 | 
			
		||||
	go scheduler.run()
 | 
			
		||||
	return scheduler, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) updateTaskStatus(ctx context.Context, status entity.TaskStatus, lastErr error, task T) error {
 | 
			
		||||
	base := task.GetTaskBase()
 | 
			
		||||
	base.LastStatus = status
 | 
			
		||||
	var result = task.MessageWithStatus(status)
 | 
			
		||||
	if lastErr != nil {
 | 
			
		||||
		result = fmt.Sprintf("%v: %v", result, lastErr)
 | 
			
		||||
func (s *dbScheduler) repo(typ entity.DbJobType) repository.DbJob {
 | 
			
		||||
	switch typ {
 | 
			
		||||
	case entity.DbJobTypeBackup:
 | 
			
		||||
		return s.backupRepo
 | 
			
		||||
	case entity.DbJobTypeRestore:
 | 
			
		||||
		return s.restoreRepo
 | 
			
		||||
	default:
 | 
			
		||||
		panic(errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ)))
 | 
			
		||||
	}
 | 
			
		||||
	base.LastResult = stringx.TruncateStr(result, entity.LastResultSize)
 | 
			
		||||
	base.LastTime = timex.NewNullTime(time.Now())
 | 
			
		||||
	return s.taskRepo.UpdateTaskStatus(ctx, task)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) UpdateTask(ctx context.Context, task T) error {
 | 
			
		||||
func (s *dbScheduler) UpdateJob(ctx context.Context, job entity.DbJob) error {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if err := s.taskRepo.UpdateById(ctx, task); err != nil {
 | 
			
		||||
	if err := s.repo(job.GetJobType()).UpdateById(ctx, job); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldTask, ok := s.queue.Remove(ctx, task.GetId())
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("任务不存在")
 | 
			
		||||
	}
 | 
			
		||||
	oldTask.Update(task)
 | 
			
		||||
	if !oldTask.Schedule() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if !s.queue.Enqueue(ctx, oldTask) {
 | 
			
		||||
		return errors.New("任务入队失败")
 | 
			
		||||
	}
 | 
			
		||||
	job.SetRun(s.run)
 | 
			
		||||
	job.SetRunnable(s.runnable)
 | 
			
		||||
	_ = s.runner.UpdateOrAdd(ctx, job)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) run() {
 | 
			
		||||
	defer s.waitGroup.Done()
 | 
			
		||||
 | 
			
		||||
	for !s.closed() {
 | 
			
		||||
		time.Sleep(time.Second)
 | 
			
		||||
 | 
			
		||||
		s.mutex.Lock()
 | 
			
		||||
		task, ok := s.queue.TryDequeue()
 | 
			
		||||
		if !ok {
 | 
			
		||||
			s.mutex.Unlock()
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if err := s.updateTaskStatus(s.context, entity.TaskReserved, nil, task); err != nil {
 | 
			
		||||
			s.mutex.Unlock()
 | 
			
		||||
			timex.SleepWithContext(s.context, sleepAfterError)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
		errRun := s.RunTask(s.context, task)
 | 
			
		||||
		taskStatus := entity.TaskSuccess
 | 
			
		||||
		if errRun != nil {
 | 
			
		||||
			taskStatus = entity.TaskFailed
 | 
			
		||||
		}
 | 
			
		||||
		s.mutex.Lock()
 | 
			
		||||
		if err := s.updateTaskStatus(s.context, taskStatus, errRun, task); err != nil {
 | 
			
		||||
			s.mutex.Unlock()
 | 
			
		||||
			timex.SleepWithContext(s.context, sleepAfterError)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		task.Schedule()
 | 
			
		||||
		if !task.IsFinished() {
 | 
			
		||||
			s.queue.Enqueue(s.context, task)
 | 
			
		||||
		}
 | 
			
		||||
		s.mutex.Unlock()
 | 
			
		||||
	}
 | 
			
		||||
func (s *dbScheduler) Close() {
 | 
			
		||||
	s.runner.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) Close() {
 | 
			
		||||
	s.cancel()
 | 
			
		||||
	s.waitGroup.Wait()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) closed() bool {
 | 
			
		||||
	return s.context.Err() != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) loadTask(ctx context.Context) error {
 | 
			
		||||
func (s *dbScheduler) AddJob(ctx context.Context, saving bool, jobType entity.DbJobType, jobs any) error {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	tasks, err := s.taskRepo.ListToDo()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, task := range tasks {
 | 
			
		||||
		if !task.Schedule() {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		s.queue.Enqueue(ctx, task)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) AddTask(ctx context.Context, tasks ...T) error {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	for _, task := range tasks {
 | 
			
		||||
		if err := s.taskRepo.AddTask(ctx, task); err != nil {
 | 
			
		||||
	if saving {
 | 
			
		||||
		if err := s.repo(jobType).AddJob(ctx, jobs); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if !task.Schedule() {
 | 
			
		||||
			continue
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
			job.SetJobType(jobType)
 | 
			
		||||
			if !job.Schedule() {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			job.SetRun(s.run)
 | 
			
		||||
			job.SetRunnable(s.runnable)
 | 
			
		||||
			_ = s.runner.Add(ctx, job)
 | 
			
		||||
		}
 | 
			
		||||
		s.queue.Enqueue(ctx, task)
 | 
			
		||||
	default:
 | 
			
		||||
		job := jobs.(entity.DbJob)
 | 
			
		||||
		job.SetJobType(jobType)
 | 
			
		||||
		if !job.Schedule() {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		job.SetRun(s.run)
 | 
			
		||||
		job.SetRunnable(s.runnable)
 | 
			
		||||
		_ = s.runner.Add(ctx, job)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) DeleteTask(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (s *dbScheduler) RemoveJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
 | 
			
		||||
	// todo: 删除数据库备份历史文件
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if err := s.taskRepo.DeleteById(ctx, taskId); err != nil {
 | 
			
		||||
	if err := s.repo(jobType).DeleteById(ctx, jobId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	s.queue.Remove(ctx, taskId)
 | 
			
		||||
	_ = s.runner.Remove(ctx, entity.FormatJobKey(jobType, jobId))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) EnableTask(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (s *dbScheduler) EnableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	task := anyx.DeepZero[T]()
 | 
			
		||||
	if err := s.taskRepo.GetById(task, taskId); err != nil {
 | 
			
		||||
	repo := s.repo(jobType)
 | 
			
		||||
	job := entity.NewDbJob(jobType)
 | 
			
		||||
	if err := repo.GetById(job, jobId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if task.IsEnabled() {
 | 
			
		||||
	if job.IsEnabled() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	task.GetTaskBase().Enabled = true
 | 
			
		||||
	if err := s.taskRepo.UpdateEnabled(ctx, taskId, true); err != nil {
 | 
			
		||||
	job.GetJobBase().Enabled = true
 | 
			
		||||
	if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	s.queue.Remove(ctx, taskId)
 | 
			
		||||
	if !task.Schedule() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	s.queue.Enqueue(ctx, task)
 | 
			
		||||
	job.SetRun(s.run)
 | 
			
		||||
	job.SetRunnable(s.runnable)
 | 
			
		||||
	_ = s.runner.Add(ctx, job)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) DisableTask(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (s *dbScheduler) DisableJob(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	task := anyx.DeepZero[T]()
 | 
			
		||||
	if err := s.taskRepo.GetById(task, taskId); err != nil {
 | 
			
		||||
	repo := s.repo(jobType)
 | 
			
		||||
	job := entity.NewDbJob(jobType)
 | 
			
		||||
	if err := repo.GetById(job, jobId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !task.IsEnabled() {
 | 
			
		||||
	if !job.IsEnabled() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := s.taskRepo.UpdateEnabled(ctx, taskId, false); err != nil {
 | 
			
		||||
	if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	s.queue.Remove(ctx, taskId)
 | 
			
		||||
	_ = s.runner.Remove(ctx, job.GetKey())
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler[T]) StartTask(ctx context.Context, taskId uint64) error {
 | 
			
		||||
func (s *dbScheduler) StartJobNow(ctx context.Context, jobType entity.DbJobType, jobId uint64) error {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	task := anyx.DeepZero[T]()
 | 
			
		||||
	if err := s.taskRepo.GetById(task, taskId); err != nil {
 | 
			
		||||
	job := entity.NewDbJob(jobType)
 | 
			
		||||
	if err := s.repo(jobType).GetById(job, jobId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !task.IsEnabled() {
 | 
			
		||||
	if !job.IsEnabled() {
 | 
			
		||||
		return errors.New("任务未启用")
 | 
			
		||||
	}
 | 
			
		||||
	s.queue.Remove(ctx, taskId)
 | 
			
		||||
	task.GetTaskBase().Deadline = time.Now()
 | 
			
		||||
	s.queue.Enqueue(ctx, task)
 | 
			
		||||
	job.GetJobBase().Deadline = time.Now()
 | 
			
		||||
	job.SetRun(s.run)
 | 
			
		||||
	job.SetRunnable(s.runnable)
 | 
			
		||||
	_ = s.runner.StartNow(ctx, job)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler) backupMysql(ctx context.Context, job entity.DbJob) error {
 | 
			
		||||
	id, err := NewIncUUID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	backup := job.(*entity.DbBackup)
 | 
			
		||||
	history := &entity.DbBackupHistory{
 | 
			
		||||
		Uuid:         id.String(),
 | 
			
		||||
		DbBackupId:   backup.Id,
 | 
			
		||||
		DbInstanceId: backup.DbInstanceId,
 | 
			
		||||
		DbName:       backup.DbName,
 | 
			
		||||
	}
 | 
			
		||||
	conn, err := s.dbApp.GetDbConnByInstanceId(backup.DbInstanceId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	dbProgram := conn.GetDialect().GetDbProgram()
 | 
			
		||||
	binlogInfo, err := dbProgram.Backup(ctx, history)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	name := backup.Name
 | 
			
		||||
	if len(name) == 0 {
 | 
			
		||||
		name = backup.DbName
 | 
			
		||||
	}
 | 
			
		||||
	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) restoreMysql(ctx context.Context, job entity.DbJob) error {
 | 
			
		||||
	restore := job.(*entity.DbRestore)
 | 
			
		||||
	conn, err := s.dbApp.GetDbConnByInstanceId(restore.DbInstanceId)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	dbProgram := conn.GetDialect().GetDbProgram()
 | 
			
		||||
	if restore.PointInTime.Valid {
 | 
			
		||||
		latestBinlogSequence, earliestBackupSequence := int64(-1), int64(-1)
 | 
			
		||||
		binlogHistory, ok, err := s.binlogHistoryRepo.GetLatestHistory(restore.DbInstanceId)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if ok {
 | 
			
		||||
			latestBinlogSequence = binlogHistory.Sequence
 | 
			
		||||
		} else {
 | 
			
		||||
			backupHistory, err := s.backupHistoryRepo.GetEarliestHistory(restore.DbInstanceId)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			earliestBackupSequence = backupHistory.BinlogSequence
 | 
			
		||||
		}
 | 
			
		||||
		binlogFiles, err := dbProgram.FetchBinlogs(ctx, true, earliestBackupSequence, latestBinlogSequence)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := s.binlogHistoryRepo.InsertWithBinlogFiles(ctx, restore.DbInstanceId, binlogFiles); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := s.restorePointInTime(ctx, dbProgram, restore); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if err := s.restoreBackupHistory(ctx, dbProgram, restore); 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) run(ctx context.Context, job entity.DbJob) {
 | 
			
		||||
	job.SetLastStatus(entity.DbJobRunning, nil)
 | 
			
		||||
	if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
 | 
			
		||||
		logx.Errorf("failed to update job status: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var errRun error
 | 
			
		||||
	switch typ := job.GetJobType(); typ {
 | 
			
		||||
	case entity.DbJobTypeBackup:
 | 
			
		||||
		errRun = s.backupMysql(ctx, job)
 | 
			
		||||
	case entity.DbJobTypeRestore:
 | 
			
		||||
		errRun = s.restoreMysql(ctx, job)
 | 
			
		||||
	default:
 | 
			
		||||
		errRun = errors.New(fmt.Sprintf("无效的数据库任务类型: %v", typ))
 | 
			
		||||
	}
 | 
			
		||||
	status := entity.DbJobSuccess
 | 
			
		||||
	if errRun != nil {
 | 
			
		||||
		status = entity.DbJobFailed
 | 
			
		||||
	}
 | 
			
		||||
	job.SetLastStatus(status, errRun)
 | 
			
		||||
	if err := s.repo(job.GetJobType()).UpdateLastStatus(ctx, job); err != nil {
 | 
			
		||||
		logx.Errorf("failed to update job status: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler) runnable(job entity.DbJob, next runner.NextFunc) bool {
 | 
			
		||||
	const maxCountByInstanceId = 4
 | 
			
		||||
	const maxCountByDbName = 1
 | 
			
		||||
	var countByInstanceId, countByDbName int
 | 
			
		||||
	jobBase := job.GetJobBase()
 | 
			
		||||
	for item, ok := next(); ok; item, ok = next() {
 | 
			
		||||
		itemBase := item.(entity.DbJob).GetJobBase()
 | 
			
		||||
		if jobBase.DbInstanceId == itemBase.DbInstanceId {
 | 
			
		||||
			countByInstanceId++
 | 
			
		||||
			if countByInstanceId > maxCountByInstanceId {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			if jobBase.DbName == itemBase.DbName {
 | 
			
		||||
				countByDbName++
 | 
			
		||||
				if countByDbName > maxCountByDbName {
 | 
			
		||||
					return false
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler) restorePointInTime(ctx context.Context, program dbm.DbProgram, job *entity.DbRestore) error {
 | 
			
		||||
	binlogHistory, err := s.binlogHistoryRepo.GetHistoryByTime(job.DbInstanceId, job.PointInTime.Time)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	position, err := program.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.GetLatestHistory(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 := &dbm.RestoreInfo{
 | 
			
		||||
		BackupHistory:   backupHistory,
 | 
			
		||||
		BinlogHistories: binlogHistories,
 | 
			
		||||
		StartPosition:   backupHistory.BinlogPosition,
 | 
			
		||||
		TargetPosition:  target.Position,
 | 
			
		||||
		TargetTime:      job.PointInTime.Time,
 | 
			
		||||
	}
 | 
			
		||||
	if err := program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return program.ReplayBinlog(ctx, job.DbName, job.DbName, restoreInfo)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *dbScheduler) restoreBackupHistory(ctx context.Context, program dbm.DbProgram, job *entity.DbRestore) error {
 | 
			
		||||
	backupHistory := &entity.DbBackupHistory{}
 | 
			
		||||
	if err := s.backupHistoryRepo.GetById(backupHistory, job.DbBackupHistoryId); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return program.RestoreBackupHistory(ctx, backupHistory.DbName, backupHistory.DbBackupId, backupHistory.Uuid)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -97,12 +97,12 @@ func (s *DbInstanceSuite) TearDownTest() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DbInstanceSuite) TestBackup() {
 | 
			
		||||
	task := &entity.DbBackupHistory{
 | 
			
		||||
	history := &entity.DbBackupHistory{
 | 
			
		||||
		DbName: dbNameBackupTest,
 | 
			
		||||
		Uuid:   dbNameBackupTest,
 | 
			
		||||
	}
 | 
			
		||||
	task.Id = backupIdTest
 | 
			
		||||
	s.testBackup(task)
 | 
			
		||||
	history.Id = backupIdTest
 | 
			
		||||
	s.testBackup(history)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DbInstanceSuite) testBackup(backupHistory *entity.DbBackupHistory) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,27 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
var _ DbTask = (*DbBackup)(nil)
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/pkg/runner"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ DbJob = (*DbBackup)(nil)
 | 
			
		||||
 | 
			
		||||
// DbBackup 数据库备份任务
 | 
			
		||||
type DbBackup struct {
 | 
			
		||||
	*DbTaskBase
 | 
			
		||||
	*DbJobBaseImpl
 | 
			
		||||
 | 
			
		||||
	Name         string `json:"name"`         // 备份任务名称
 | 
			
		||||
	DbName       string `json:"dbName"`       // 数据库名
 | 
			
		||||
	DbInstanceId uint64 `json:"dbInstanceId"` // 数据库实例ID
 | 
			
		||||
	Name string `json:"Name"` // 数据库备份名称
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*DbBackup) MessageWithStatus(status TaskStatus) string {
 | 
			
		||||
	var result string
 | 
			
		||||
	switch status {
 | 
			
		||||
	case TaskDelay:
 | 
			
		||||
		result = "等待备份数据库"
 | 
			
		||||
	case TaskReady:
 | 
			
		||||
		result = "准备备份数据库"
 | 
			
		||||
	case TaskReserved:
 | 
			
		||||
		result = "数据库备份中"
 | 
			
		||||
	case TaskSuccess:
 | 
			
		||||
		result = "数据库备份成功"
 | 
			
		||||
	case TaskFailed:
 | 
			
		||||
		result = "数据库备份失败"
 | 
			
		||||
func (d *DbBackup) SetRun(fn func(ctx context.Context, job DbJob)) {
 | 
			
		||||
	d.run = func(ctx context.Context) {
 | 
			
		||||
		fn(ctx, d)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbBackup) SetRunnable(fn func(job DbJob, next runner.NextFunc) bool) {
 | 
			
		||||
	d.runnable = func(next runner.NextFunc) bool {
 | 
			
		||||
		return fn(d, next)
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,17 +10,17 @@ import (
 | 
			
		||||
type DbBinlog struct {
 | 
			
		||||
	model.Model
 | 
			
		||||
 | 
			
		||||
	LastStatus   TaskStatus     // 最近一次执行状态
 | 
			
		||||
	LastStatus   DbJobStatus    // 最近一次执行状态
 | 
			
		||||
	LastResult   string         // 最近一次执行结果
 | 
			
		||||
	LastTime     timex.NullTime // 最近一次执行时间
 | 
			
		||||
	DbInstanceId uint64         `json:"dbInstanceId"` // 数据库实例ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDbBinlog(instanceId uint64) *DbBinlog {
 | 
			
		||||
	binlogTask := &DbBinlog{}
 | 
			
		||||
	binlogTask.Id = instanceId
 | 
			
		||||
	binlogTask.DbInstanceId = instanceId
 | 
			
		||||
	return binlogTask
 | 
			
		||||
	job := &DbBinlog{}
 | 
			
		||||
	job.Id = instanceId
 | 
			
		||||
	job.DbInstanceId = instanceId
 | 
			
		||||
	return job
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BinlogFile is the metadata of the MySQL binlog file.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										235
									
								
								server/internal/db/domain/entity/db_job.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								server/internal/db/domain/entity/db_job.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/runner"
 | 
			
		||||
	"mayfly-go/pkg/utils/stringx"
 | 
			
		||||
	"mayfly-go/pkg/utils/timex"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const LastResultSize = 256
 | 
			
		||||
 | 
			
		||||
type DbJobKey = runner.JobKey
 | 
			
		||||
 | 
			
		||||
type DbJobStatus = runner.JobStatus
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DbJobUnknown = runner.JobUnknown
 | 
			
		||||
	DbJobDelay   = runner.JobDelay
 | 
			
		||||
	DbJobReady   = runner.JobWaiting
 | 
			
		||||
	DbJobRunning = runner.JobRunning
 | 
			
		||||
	DbJobRemoved = runner.JobRemoved
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DbJobSuccess DbJobStatus = 0x20 + iota
 | 
			
		||||
	DbJobFailed
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DbJobType = string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DbJobTypeBackup  DbJobType = "db-backup"
 | 
			
		||||
	DbJobTypeRestore DbJobType = "db-restore"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	DbJobNameBackup  = "数据库备份"
 | 
			
		||||
	DbJobNameRestore = "数据库恢复"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ runner.Job = (DbJob)(nil)
 | 
			
		||||
 | 
			
		||||
type DbJobBase interface {
 | 
			
		||||
	model.ModelI
 | 
			
		||||
	runner.Job
 | 
			
		||||
 | 
			
		||||
	GetId() uint64
 | 
			
		||||
	GetJobType() DbJobType
 | 
			
		||||
	SetJobType(typ DbJobType)
 | 
			
		||||
	GetJobBase() *DbJobBaseImpl
 | 
			
		||||
	SetLastStatus(status DbJobStatus, err error)
 | 
			
		||||
	IsEnabled() bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DbJob interface {
 | 
			
		||||
	DbJobBase
 | 
			
		||||
 | 
			
		||||
	SetRun(fn func(ctx context.Context, job DbJob))
 | 
			
		||||
	SetRunnable(fn func(job DbJob, next runner.NextFunc) bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDbJob(typ DbJobType) DbJob {
 | 
			
		||||
	switch typ {
 | 
			
		||||
	case DbJobTypeBackup:
 | 
			
		||||
		return &DbBackup{
 | 
			
		||||
			DbJobBaseImpl: &DbJobBaseImpl{
 | 
			
		||||
				jobType: DbJobTypeBackup},
 | 
			
		||||
		}
 | 
			
		||||
	case DbJobTypeRestore:
 | 
			
		||||
		return &DbRestore{
 | 
			
		||||
			DbJobBaseImpl: &DbJobBaseImpl{
 | 
			
		||||
				jobType: DbJobTypeRestore},
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		panic(fmt.Sprintf("invalid DbJobType: %v", typ))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ DbJobBase = (*DbJobBaseImpl)(nil)
 | 
			
		||||
 | 
			
		||||
type DbJobBaseImpl struct {
 | 
			
		||||
	model.Model
 | 
			
		||||
 | 
			
		||||
	DbInstanceId uint64         // 数据库实例ID
 | 
			
		||||
	DbName       string         // 数据库名称
 | 
			
		||||
	Enabled      bool           // 是否启用
 | 
			
		||||
	StartTime    time.Time      // 开始时间
 | 
			
		||||
	Interval     time.Duration  // 间隔时间
 | 
			
		||||
	Repeated     bool           // 是否重复执行
 | 
			
		||||
	LastStatus   DbJobStatus    // 最近一次执行状态
 | 
			
		||||
	LastResult   string         // 最近一次执行结果
 | 
			
		||||
	LastTime     timex.NullTime // 最近一次执行时间
 | 
			
		||||
	Deadline     time.Time      `gorm:"-" json:"-"` // 计划执行时间
 | 
			
		||||
	run          runner.RunFunc
 | 
			
		||||
	runnable     runner.RunnableFunc
 | 
			
		||||
	jobType      DbJobType
 | 
			
		||||
	jobKey       runner.JobKey
 | 
			
		||||
	jobStatus    runner.JobStatus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDbBJobBase(instanceId uint64, dbName string, jobType DbJobType, enabled bool, repeated bool, startTime time.Time, interval time.Duration) *DbJobBaseImpl {
 | 
			
		||||
	return &DbJobBaseImpl{
 | 
			
		||||
		DbInstanceId: instanceId,
 | 
			
		||||
		DbName:       dbName,
 | 
			
		||||
		jobType:      jobType,
 | 
			
		||||
		Enabled:      enabled,
 | 
			
		||||
		Repeated:     repeated,
 | 
			
		||||
		StartTime:    startTime,
 | 
			
		||||
		Interval:     interval,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) GetJobType() DbJobType {
 | 
			
		||||
	return d.jobType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) SetJobType(typ DbJobType) {
 | 
			
		||||
	d.jobType = typ
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) SetLastStatus(status DbJobStatus, err error) {
 | 
			
		||||
	var statusName, jobName string
 | 
			
		||||
	switch status {
 | 
			
		||||
	case DbJobRunning:
 | 
			
		||||
		statusName = "运行中"
 | 
			
		||||
	case DbJobSuccess:
 | 
			
		||||
		statusName = "成功"
 | 
			
		||||
	case DbJobFailed:
 | 
			
		||||
		statusName = "失败"
 | 
			
		||||
	default:
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	switch d.jobType {
 | 
			
		||||
	case DbJobTypeBackup:
 | 
			
		||||
		jobName = DbJobNameBackup
 | 
			
		||||
	case DbJobTypeRestore:
 | 
			
		||||
		jobName = DbJobNameRestore
 | 
			
		||||
	default:
 | 
			
		||||
		jobName = d.jobType
 | 
			
		||||
	}
 | 
			
		||||
	d.LastStatus = status
 | 
			
		||||
	var result = jobName + statusName
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		result = fmt.Sprintf("%s: %v", result, err)
 | 
			
		||||
	}
 | 
			
		||||
	d.LastResult = stringx.TruncateStr(result, LastResultSize)
 | 
			
		||||
	d.LastTime = timex.NewNullTime(time.Now())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) GetId() uint64 {
 | 
			
		||||
	if d == nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return d.Id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) GetDeadline() time.Time {
 | 
			
		||||
	return d.Deadline
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) Schedule() bool {
 | 
			
		||||
	if d.IsFinished() || !d.Enabled {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	switch d.LastStatus {
 | 
			
		||||
	case DbJobSuccess:
 | 
			
		||||
		if d.Interval == 0 {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
		lastTime := d.LastTime.Time
 | 
			
		||||
		if lastTime.Sub(d.StartTime) < 0 {
 | 
			
		||||
			lastTime = d.StartTime.Add(-d.Interval)
 | 
			
		||||
		}
 | 
			
		||||
		d.Deadline = lastTime.Add(d.Interval - lastTime.Sub(d.StartTime)%d.Interval)
 | 
			
		||||
	case DbJobFailed:
 | 
			
		||||
		d.Deadline = time.Now().Add(time.Minute)
 | 
			
		||||
	default:
 | 
			
		||||
		d.Deadline = d.StartTime
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) IsFinished() bool {
 | 
			
		||||
	return !d.Repeated && d.LastStatus == DbJobSuccess
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) Renew(job runner.Job) {
 | 
			
		||||
	jobBase := job.(DbJob).GetJobBase()
 | 
			
		||||
	d.StartTime = jobBase.StartTime
 | 
			
		||||
	d.Interval = jobBase.Interval
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) GetJobBase() *DbJobBaseImpl {
 | 
			
		||||
	return d
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) IsEnabled() bool {
 | 
			
		||||
	return d.Enabled
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) Run(ctx context.Context) {
 | 
			
		||||
	if d.run == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	d.run(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) Runnable(next runner.NextFunc) bool {
 | 
			
		||||
	if d.runnable == nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return d.runnable(next)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FormatJobKey(typ DbJobType, jobId uint64) DbJobKey {
 | 
			
		||||
	return fmt.Sprintf("%v-%d", typ, jobId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) GetKey() DbJobKey {
 | 
			
		||||
	if len(d.jobKey) == 0 {
 | 
			
		||||
		d.jobKey = FormatJobKey(d.jobType, d.Id)
 | 
			
		||||
	}
 | 
			
		||||
	return d.jobKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) GetStatus() DbJobStatus {
 | 
			
		||||
	return d.jobStatus
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbJobBaseImpl) SetStatus(status DbJobStatus) {
 | 
			
		||||
	d.jobStatus = status
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +1,31 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/pkg/runner"
 | 
			
		||||
	"mayfly-go/pkg/utils/timex"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ DbTask = (*DbRestore)(nil)
 | 
			
		||||
var _ DbJob = (*DbRestore)(nil)
 | 
			
		||||
 | 
			
		||||
// DbRestore 数据库恢复任务
 | 
			
		||||
type DbRestore struct {
 | 
			
		||||
	*DbTaskBase
 | 
			
		||||
	*DbJobBaseImpl
 | 
			
		||||
 | 
			
		||||
	DbName              string         `json:"dbName"`              // 数据库名
 | 
			
		||||
	PointInTime         timex.NullTime `json:"pointInTime"`         // 指定数据库恢复的时间点
 | 
			
		||||
	DbBackupId          uint64         `json:"dbBackupId"`          // 用于恢复的数据库恢复任务ID
 | 
			
		||||
	DbBackupHistoryId   uint64         `json:"dbBackupHistoryId"`   // 用于恢复的数据库恢复历史ID
 | 
			
		||||
	DbBackupHistoryName string         `json:"dbBackupHistoryName"` // 数据库恢复历史名称
 | 
			
		||||
	DbInstanceId        uint64         `json:"dbInstanceId"`        // 数据库实例ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*DbRestore) MessageWithStatus(status TaskStatus) string {
 | 
			
		||||
	var result string
 | 
			
		||||
	switch status {
 | 
			
		||||
	case TaskDelay:
 | 
			
		||||
		result = "等待恢复数据库"
 | 
			
		||||
	case TaskReady:
 | 
			
		||||
		result = "准备恢复数据库"
 | 
			
		||||
	case TaskReserved:
 | 
			
		||||
		result = "数据库恢复中"
 | 
			
		||||
	case TaskSuccess:
 | 
			
		||||
		result = "数据库恢复成功"
 | 
			
		||||
	case TaskFailed:
 | 
			
		||||
		result = "数据库恢复失败"
 | 
			
		||||
func (d *DbRestore) SetRun(fn func(ctx context.Context, job DbJob)) {
 | 
			
		||||
	d.run = func(ctx context.Context) {
 | 
			
		||||
		fn(ctx, d)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbRestore) SetRunnable(fn func(job DbJob, next runner.NextFunc) bool) {
 | 
			
		||||
	d.runnable = func(next runner.NextFunc) bool {
 | 
			
		||||
		return fn(d, next)
 | 
			
		||||
	}
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,109 +0,0 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/utils/timex"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TaskStatus int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TaskDelay TaskStatus = iota
 | 
			
		||||
	TaskReady
 | 
			
		||||
	TaskReserved
 | 
			
		||||
	TaskSuccess
 | 
			
		||||
	TaskFailed
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const LastResultSize = 256
 | 
			
		||||
 | 
			
		||||
type DbTask interface {
 | 
			
		||||
	model.ModelI
 | 
			
		||||
 | 
			
		||||
	GetId() uint64
 | 
			
		||||
	GetDeadline() time.Time
 | 
			
		||||
	IsFinished() bool
 | 
			
		||||
	Schedule() bool
 | 
			
		||||
	Update(task DbTask)
 | 
			
		||||
	GetTaskBase() *DbTaskBase
 | 
			
		||||
	MessageWithStatus(status TaskStatus) string
 | 
			
		||||
	IsEnabled() bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDbBTaskBase(enabled bool, repeated bool, startTime time.Time, interval time.Duration) *DbTaskBase {
 | 
			
		||||
	return &DbTaskBase{
 | 
			
		||||
		Enabled:   enabled,
 | 
			
		||||
		Repeated:  repeated,
 | 
			
		||||
		StartTime: startTime,
 | 
			
		||||
		Interval:  interval,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DbTaskBase struct {
 | 
			
		||||
	model.Model
 | 
			
		||||
 | 
			
		||||
	Enabled    bool           // 是否启用
 | 
			
		||||
	StartTime  time.Time      // 开始时间
 | 
			
		||||
	Interval   time.Duration  // 间隔时间
 | 
			
		||||
	Repeated   bool           // 是否重复执行
 | 
			
		||||
	LastStatus TaskStatus     // 最近一次执行状态
 | 
			
		||||
	LastResult string         // 最近一次执行结果
 | 
			
		||||
	LastTime   timex.NullTime // 最近一次执行时间
 | 
			
		||||
	Deadline   time.Time      `gorm:"-" json:"-"` // 计划执行时间
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) GetId() uint64 {
 | 
			
		||||
	if d == nil {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return d.Id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) GetDeadline() time.Time {
 | 
			
		||||
	return d.Deadline
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) Schedule() bool {
 | 
			
		||||
	if d.IsFinished() || !d.Enabled {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	switch d.LastStatus {
 | 
			
		||||
	case TaskSuccess:
 | 
			
		||||
		if d.Interval == 0 {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
		lastTime := d.LastTime.Time
 | 
			
		||||
		if lastTime.Sub(d.StartTime) < 0 {
 | 
			
		||||
			lastTime = d.StartTime.Add(-d.Interval)
 | 
			
		||||
		}
 | 
			
		||||
		d.Deadline = lastTime.Add(d.Interval - lastTime.Sub(d.StartTime)%d.Interval)
 | 
			
		||||
	case TaskFailed:
 | 
			
		||||
		d.Deadline = time.Now().Add(time.Minute)
 | 
			
		||||
	default:
 | 
			
		||||
		d.Deadline = d.StartTime
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) IsFinished() bool {
 | 
			
		||||
	return !d.Repeated && d.LastStatus == TaskSuccess
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) Update(task DbTask) {
 | 
			
		||||
	t := task.GetTaskBase()
 | 
			
		||||
	d.StartTime = t.StartTime
 | 
			
		||||
	d.Interval = t.Interval
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) GetTaskBase() *DbTaskBase {
 | 
			
		||||
	return d
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*DbTaskBase) MessageWithStatus(_ TaskStatus) string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DbTaskBase) IsEnabled() bool {
 | 
			
		||||
	return d.Enabled
 | 
			
		||||
}
 | 
			
		||||
@@ -40,8 +40,8 @@ type DbSqlExecQuery struct {
 | 
			
		||||
	CreatorId uint64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DbBackupQuery 数据库备份任务查询
 | 
			
		||||
type DbBackupQuery struct {
 | 
			
		||||
// DbJobQuery 数据库备份任务查询
 | 
			
		||||
type DbJobQuery struct {
 | 
			
		||||
	Id           uint64   `json:"id" form:"id"`
 | 
			
		||||
	DbName       string   `json:"dbName" form:"dbName"`
 | 
			
		||||
	IntervalDay  int      `json:"intervalDay" form:"intervalDay"`
 | 
			
		||||
@@ -61,13 +61,13 @@ type DbBackupHistoryQuery struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DbRestoreQuery 数据库备份任务查询
 | 
			
		||||
type DbRestoreQuery struct {
 | 
			
		||||
	Id           uint64   `json:"id" form:"id"`
 | 
			
		||||
	DbName       string   `json:"dbName" form:"dbName"`
 | 
			
		||||
	InDbNames    []string `json:"-" form:"-"`
 | 
			
		||||
	DbInstanceId uint64   `json:"-" form:"-"`
 | 
			
		||||
	Repeated     bool     `json:"repeated" form:"repeated"` // 是否重复执行
 | 
			
		||||
}
 | 
			
		||||
//type DbRestoreQuery struct {
 | 
			
		||||
//	Id           uint64   `json:"id" form:"id"`
 | 
			
		||||
//	DbName       string   `json:"dbName" form:"dbName"`
 | 
			
		||||
//	InDbNames    []string `json:"-" form:"-"`
 | 
			
		||||
//	DbInstanceId uint64   `json:"-" form:"-"`
 | 
			
		||||
//	Repeated     bool     `json:"repeated" form:"repeated"` // 是否重复执行
 | 
			
		||||
//}
 | 
			
		||||
 | 
			
		||||
// DbRestoreHistoryQuery 数据库备份任务查询
 | 
			
		||||
type DbRestoreHistoryQuery struct {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,7 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DbBackup interface {
 | 
			
		||||
	DbTask[*entity.DbBackup]
 | 
			
		||||
	DbJob
 | 
			
		||||
 | 
			
		||||
	// GetDbBackupList 分页获取数据信息列表
 | 
			
		||||
	GetDbBackupList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
 | 
			
		||||
	AddTask(ctx context.Context, tasks ...*entity.DbBackup) error
 | 
			
		||||
	GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,5 +9,5 @@ import (
 | 
			
		||||
type DbBinlog interface {
 | 
			
		||||
	base.Repo[*entity.DbBinlog]
 | 
			
		||||
 | 
			
		||||
	AddTaskIfNotExists(ctx context.Context, task *entity.DbBinlog) error
 | 
			
		||||
	AddJobIfNotExists(ctx context.Context, job *entity.DbBinlog) error
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								server/internal/db/domain/repository/db_job.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								server/internal/db/domain/repository/db_job.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DbJob interface {
 | 
			
		||||
	// AddJob 添加数据库任务
 | 
			
		||||
	AddJob(ctx context.Context, jobs any) error
 | 
			
		||||
	// GetById 根据实体id查询
 | 
			
		||||
	GetById(e entity.DbJob, id uint64, cols ...string) error
 | 
			
		||||
	// GetPageList 分页获取数据库任务列表
 | 
			
		||||
	GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
 | 
			
		||||
	// UpdateById 根据实体id更新实体信息
 | 
			
		||||
	UpdateById(ctx context.Context, e entity.DbJob, columns ...string) error
 | 
			
		||||
	// BatchInsertWithDb 使用指定gorm db执行,主要用于事务执行
 | 
			
		||||
	BatchInsertWithDb(ctx context.Context, db *gorm.DB, es any) error
 | 
			
		||||
	// DeleteById 根据实体主键删除实体
 | 
			
		||||
	DeleteById(ctx context.Context, id uint64) error
 | 
			
		||||
 | 
			
		||||
	UpdateLastStatus(ctx context.Context, job entity.DbJob) error
 | 
			
		||||
	UpdateEnabled(ctx context.Context, jobId uint64, enabled bool) error
 | 
			
		||||
	ListToDo(jobs any) error
 | 
			
		||||
	ListRepeating(jobs any) error
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +1,7 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DbRestore interface {
 | 
			
		||||
	DbTask[*entity.DbRestore]
 | 
			
		||||
	DbJob
 | 
			
		||||
 | 
			
		||||
	// GetDbRestoreList 分页获取数据信息列表
 | 
			
		||||
	GetDbRestoreList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
 | 
			
		||||
	AddTask(ctx context.Context, tasks ...*entity.DbRestore) error
 | 
			
		||||
	GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DbTask[T model.ModelI] interface {
 | 
			
		||||
	base.Repo[T]
 | 
			
		||||
 | 
			
		||||
	UpdateTaskStatus(ctx context.Context, task T) error
 | 
			
		||||
	UpdateEnabled(ctx context.Context, taskId uint64, enabled bool) error
 | 
			
		||||
	ListToDo() ([]T, error)
 | 
			
		||||
	ListRepeating() ([]T, error)
 | 
			
		||||
	AddTask(ctx context.Context, tasks ...T) error
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +1,22 @@
 | 
			
		||||
package persistence
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/gormx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ repository.DbBackup = (*dbBackupRepoImpl)(nil)
 | 
			
		||||
 | 
			
		||||
type dbBackupRepoImpl struct {
 | 
			
		||||
	//base.RepoImpl[*entity.DbBackup]
 | 
			
		||||
	dbTaskBase[*entity.DbBackup]
 | 
			
		||||
	dbJobBase[*entity.DbBackup]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDbBackupRepo() repository.DbBackup {
 | 
			
		||||
	return &dbBackupRepoImpl{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDbBackupList 分页获取数据库备份任务列表
 | 
			
		||||
func (d *dbBackupRepoImpl) GetDbBackupList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	qd := gormx.NewQuery(d.GetModel()).
 | 
			
		||||
		Eq("id", condition.Id).
 | 
			
		||||
		Eq0("db_instance_id", condition.DbInstanceId).
 | 
			
		||||
		Eq0("repeated", condition.Repeated).
 | 
			
		||||
		In0("db_name", condition.InDbNames).
 | 
			
		||||
		Like("db_name", condition.DbName)
 | 
			
		||||
	return gormx.PageQuery(qd, pageParam, toEntity)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbBackupRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbBackup) error {
 | 
			
		||||
	return gormx.Tx(func(db *gorm.DB) error {
 | 
			
		||||
		var instanceId uint64
 | 
			
		||||
		dbNames := make([]string, 0, len(tasks))
 | 
			
		||||
		for _, task := range tasks {
 | 
			
		||||
			if instanceId == 0 {
 | 
			
		||||
				instanceId = task.DbInstanceId
 | 
			
		||||
			}
 | 
			
		||||
			if task.DbInstanceId != instanceId {
 | 
			
		||||
				return errors.New("不支持同时为多个数据库实例添加备份任务")
 | 
			
		||||
			}
 | 
			
		||||
			if task.Interval == 0 {
 | 
			
		||||
				// 单次执行的备份任务可重复创建
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			dbNames = append(dbNames, task.DbName)
 | 
			
		||||
		}
 | 
			
		||||
		var res []string
 | 
			
		||||
		err := db.Model(d.GetModel()).Select("db_name").
 | 
			
		||||
			Where("db_instance_id = ?", instanceId).
 | 
			
		||||
			Where("db_name in ?", dbNames).
 | 
			
		||||
			Where("repeated = true").
 | 
			
		||||
			Scopes(gormx.UndeleteScope).Find(&res).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if len(res) > 0 {
 | 
			
		||||
			return fmt.Errorf("数据库备份任务已存在: %v", res)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return d.BatchInsertWithDb(ctx, db, tasks)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbBackupRepoImpl) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
 | 
			
		||||
	var dbNamesWithBackup []string
 | 
			
		||||
	query := gormx.NewQuery(d.GetModel()).
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@ func NewDbBinlogRepo() repository.DbBinlog {
 | 
			
		||||
	return &dbBinlogRepoImpl{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbBinlogRepoImpl) AddTaskIfNotExists(_ context.Context, task *entity.DbBinlog) error {
 | 
			
		||||
	if err := global.Db.Clauses(clause.OnConflict{DoNothing: true}).Create(task).Error; err != nil {
 | 
			
		||||
func (d *dbBinlogRepoImpl) AddJobIfNotExists(_ context.Context, job *entity.DbBinlog) error {
 | 
			
		||||
	if err := global.Db.Clauses(clause.OnConflict{DoNothing: true}).Create(job).Error; err != nil {
 | 
			
		||||
		return fmt.Errorf("启动 binlog 下载失败: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,7 @@ func (repo *dbBinlogHistoryRepoImpl) InsertWithBinlogFiles(ctx context.Context,
 | 
			
		||||
	if len(binlogFiles) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	histories := make([]*entity.DbBinlogHistory, 0, len(binlogFiles))
 | 
			
		||||
	histories := make([]any, 0, len(binlogFiles))
 | 
			
		||||
	for _, fileOnServer := range binlogFiles {
 | 
			
		||||
		if !fileOnServer.Downloaded {
 | 
			
		||||
			break
 | 
			
		||||
@@ -115,7 +115,7 @@ func (repo *dbBinlogHistoryRepoImpl) InsertWithBinlogFiles(ctx context.Context,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(histories) > 0 {
 | 
			
		||||
		if err := repo.Upsert(ctx, histories[len(histories)-1]); err != nil {
 | 
			
		||||
		if err := repo.Upsert(ctx, histories[len(histories)-1].(*entity.DbBinlogHistory)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										127
									
								
								server/internal/db/infrastructure/persistence/db_job_base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								server/internal/db/infrastructure/persistence/db_job_base.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
			
		||||
package persistence
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/global"
 | 
			
		||||
	"mayfly-go/pkg/gormx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"reflect"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type dbJobBase[T entity.DbJob] struct {
 | 
			
		||||
	base.RepoImpl[T]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) GetById(e entity.DbJob, id uint64, cols ...string) error {
 | 
			
		||||
	return d.RepoImpl.GetById(e.(T), id, cols...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) UpdateById(ctx context.Context, e entity.DbJob, columns ...string) error {
 | 
			
		||||
	return d.RepoImpl.UpdateById(ctx, e.(T), columns...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) UpdateEnabled(_ context.Context, jobId uint64, enabled bool) error {
 | 
			
		||||
	cond := map[string]any{
 | 
			
		||||
		"id": jobId,
 | 
			
		||||
	}
 | 
			
		||||
	return d.Updates(cond, map[string]any{
 | 
			
		||||
		"enabled": enabled,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) UpdateLastStatus(ctx context.Context, job entity.DbJob) error {
 | 
			
		||||
	return d.UpdateById(ctx, job.(T), "last_status", "last_result", "last_time")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) ListToDo(jobs any) error {
 | 
			
		||||
	db := global.Db.Model(d.GetModel())
 | 
			
		||||
	err := db.Where("enabled = ?", true).
 | 
			
		||||
		Where(db.Where("repeated = ?", true).Or("last_status <> ?", entity.DbJobSuccess)).
 | 
			
		||||
		Scopes(gormx.UndeleteScope).
 | 
			
		||||
		Find(jobs).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) ListRepeating(jobs any) error {
 | 
			
		||||
	cond := map[string]any{
 | 
			
		||||
		"enabled":  true,
 | 
			
		||||
		"repeated": true,
 | 
			
		||||
	}
 | 
			
		||||
	if err := d.ListByCond(cond, jobs); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPageList 分页获取数据库备份任务列表
 | 
			
		||||
func (d *dbJobBase[T]) GetPageList(condition *entity.DbJobQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	d.GetModel()
 | 
			
		||||
	qd := gormx.NewQuery(d.GetModel()).
 | 
			
		||||
		Eq("id", condition.Id).
 | 
			
		||||
		Eq0("db_instance_id", condition.DbInstanceId).
 | 
			
		||||
		Eq0("repeated", condition.Repeated).
 | 
			
		||||
		In0("db_name", condition.InDbNames).
 | 
			
		||||
		Like("db_name", condition.DbName)
 | 
			
		||||
	return gormx.PageQuery(qd, pageParam, toEntity)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbJobBase[T]) AddJob(ctx context.Context, jobs any) error {
 | 
			
		||||
	return gormx.Tx(func(db *gorm.DB) error {
 | 
			
		||||
		var instanceId uint64
 | 
			
		||||
		var dbNames []string
 | 
			
		||||
		reflectValue := reflect.ValueOf(jobs)
 | 
			
		||||
		var plural bool
 | 
			
		||||
		switch reflectValue.Kind() {
 | 
			
		||||
		case reflect.Slice, reflect.Array:
 | 
			
		||||
			plural = true
 | 
			
		||||
			reflectLen := reflectValue.Len()
 | 
			
		||||
			dbNames = make([]string, 0, reflectLen)
 | 
			
		||||
			for i := 0; i < reflectLen; i++ {
 | 
			
		||||
				job := reflectValue.Index(i).Interface().(entity.DbJob)
 | 
			
		||||
				jobBase := job.GetJobBase()
 | 
			
		||||
				if instanceId == 0 {
 | 
			
		||||
					instanceId = jobBase.DbInstanceId
 | 
			
		||||
				}
 | 
			
		||||
				if jobBase.DbInstanceId != instanceId {
 | 
			
		||||
					return errors.New("不支持同时为多个数据库实例添加数据库任务")
 | 
			
		||||
				}
 | 
			
		||||
				if jobBase.Interval == 0 {
 | 
			
		||||
					// 单次执行的数据库任务可重复创建
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				dbNames = append(dbNames, jobBase.DbName)
 | 
			
		||||
			}
 | 
			
		||||
		default:
 | 
			
		||||
			jobBase := jobs.(entity.DbJob).GetJobBase()
 | 
			
		||||
			instanceId = jobBase.DbInstanceId
 | 
			
		||||
			if jobBase.Interval > 0 {
 | 
			
		||||
				dbNames = append(dbNames, jobBase.DbName)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var res []string
 | 
			
		||||
		err := db.Model(d.GetModel()).Select("db_name").
 | 
			
		||||
			Where("db_instance_id = ?", instanceId).
 | 
			
		||||
			Where("db_name in ?", dbNames).
 | 
			
		||||
			Where("repeated = true").
 | 
			
		||||
			Scopes(gormx.UndeleteScope).Find(&res).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if len(res) > 0 {
 | 
			
		||||
			return errors.New(fmt.Sprintf("数据库任务已存在: %v", res))
 | 
			
		||||
		}
 | 
			
		||||
		if plural {
 | 
			
		||||
			return d.BatchInsertWithDb(ctx, db, jobs)
 | 
			
		||||
		}
 | 
			
		||||
		return d.InsertWithDb(ctx, db, jobs.(T))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -1,73 +1,22 @@
 | 
			
		||||
package persistence
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/db/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/gormx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"slices"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ repository.DbRestore = (*dbRestoreRepoImpl)(nil)
 | 
			
		||||
 | 
			
		||||
type dbRestoreRepoImpl struct {
 | 
			
		||||
	dbTaskBase[*entity.DbRestore]
 | 
			
		||||
	dbJobBase[*entity.DbRestore]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDbRestoreRepo() repository.DbRestore {
 | 
			
		||||
	return &dbRestoreRepoImpl{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDbRestoreList 分页获取数据库备份任务列表
 | 
			
		||||
func (d *dbRestoreRepoImpl) GetDbRestoreList(condition *entity.DbRestoreQuery, pageParam *model.PageParam, toEntity any, _ ...string) (*model.PageResult[any], error) {
 | 
			
		||||
	qd := gormx.NewQuery(d.GetModel()).
 | 
			
		||||
		Eq("id", condition.Id).
 | 
			
		||||
		Eq0("db_instance_id", condition.DbInstanceId).
 | 
			
		||||
		Eq0("repeated", condition.Repeated).
 | 
			
		||||
		In0("db_name", condition.InDbNames).
 | 
			
		||||
		Like("db_name", condition.DbName)
 | 
			
		||||
	return gormx.PageQuery(qd, pageParam, toEntity)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbRestoreRepoImpl) AddTask(ctx context.Context, tasks ...*entity.DbRestore) error {
 | 
			
		||||
	return gormx.Tx(func(db *gorm.DB) error {
 | 
			
		||||
		var instanceId uint64
 | 
			
		||||
		dbNames := make([]string, 0, len(tasks))
 | 
			
		||||
		for _, task := range tasks {
 | 
			
		||||
			if instanceId == 0 {
 | 
			
		||||
				instanceId = task.DbInstanceId
 | 
			
		||||
			}
 | 
			
		||||
			if task.DbInstanceId != instanceId {
 | 
			
		||||
				return errors.New("不支持同时为多个数据库实例添加备份任务")
 | 
			
		||||
			}
 | 
			
		||||
			if task.Interval == 0 {
 | 
			
		||||
				// 单次执行的恢复任务可重复创建
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			dbNames = append(dbNames, task.DbName)
 | 
			
		||||
		}
 | 
			
		||||
		var res []string
 | 
			
		||||
		err := db.Model(new(entity.DbRestore)).Select("db_name").
 | 
			
		||||
			Where("db_instance_id = ?", instanceId).
 | 
			
		||||
			Where("db_name in ?", dbNames).
 | 
			
		||||
			Where("repeated = true").
 | 
			
		||||
			Scopes(gormx.UndeleteScope).Find(&res).Error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if len(res) > 0 {
 | 
			
		||||
			return fmt.Errorf("数据库备份任务已存在: %v", res)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return d.BatchInsertWithDb(ctx, db, tasks)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbRestoreRepoImpl) GetDbNamesWithoutRestore(instanceId uint64, dbNames []string) ([]string, error) {
 | 
			
		||||
	var dbNamesWithRestore []string
 | 
			
		||||
	query := gormx.NewQuery(d.GetModel()).
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
package persistence
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"mayfly-go/internal/db/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/global"
 | 
			
		||||
	"mayfly-go/pkg/gormx"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type dbTaskBase[T model.ModelI] struct {
 | 
			
		||||
	base.RepoImpl[T]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbTaskBase[T]) UpdateEnabled(_ context.Context, taskId uint64, enabled bool) error {
 | 
			
		||||
	cond := map[string]any{
 | 
			
		||||
		"id": taskId,
 | 
			
		||||
	}
 | 
			
		||||
	return d.Updates(cond, map[string]any{
 | 
			
		||||
		"enabled": enabled,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbTaskBase[T]) UpdateTaskStatus(ctx context.Context, task T) error {
 | 
			
		||||
	return d.UpdateById(ctx, task, "last_status", "last_result", "last_time")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbTaskBase[T]) ListToDo() ([]T, error) {
 | 
			
		||||
	var tasks []T
 | 
			
		||||
	db := global.Db.Model(d.GetModel())
 | 
			
		||||
	err := db.Where("enabled = ?", true).
 | 
			
		||||
		Where(db.Where("repeated = ?", true).Or("last_status <> ?", entity.TaskSuccess)).
 | 
			
		||||
		Scopes(gormx.UndeleteScope).
 | 
			
		||||
		Find(&tasks).Error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return tasks, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *dbTaskBase[T]) ListRepeating() ([]T, error) {
 | 
			
		||||
	cond := map[string]any{
 | 
			
		||||
		"enabled":  true,
 | 
			
		||||
		"repeated": true,
 | 
			
		||||
	}
 | 
			
		||||
	var tasks []T
 | 
			
		||||
	if err := d.ListByCond(cond, &tasks); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return tasks, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -22,7 +22,7 @@ func InitDbBackupRouter(router *gin.RouterGroup) {
 | 
			
		||||
		// 创建数据库备份任务
 | 
			
		||||
		req.NewPost(":dbId/backups", d.Create).Log(req.NewLogSave("db-创建数据库备份任务")),
 | 
			
		||||
		// 保存数据库备份任务
 | 
			
		||||
		req.NewPut(":dbId/backups/:backupId", d.Save).Log(req.NewLogSave("db-保存数据库备份任务")),
 | 
			
		||||
		req.NewPut(":dbId/backups/:backupId", d.Update).Log(req.NewLogSave("db-保存数据库备份任务")),
 | 
			
		||||
		// 启用数据库备份任务
 | 
			
		||||
		req.NewPut(":dbId/backups/:backupId/enable", d.Enable).Log(req.NewLogSave("db-启用数据库备份任务")),
 | 
			
		||||
		// 禁用数据库备份任务
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ func InitDbRestoreRouter(router *gin.RouterGroup) {
 | 
			
		||||
		// 创建数据库备份任务
 | 
			
		||||
		req.NewPost(":dbId/restores", d.Create).Log(req.NewLogSave("db-创建数据库恢复任务")),
 | 
			
		||||
		// 保存数据库备份任务
 | 
			
		||||
		req.NewPut(":dbId/restores/:restoreId", d.Save).Log(req.NewLogSave("db-保存数据库恢复任务")),
 | 
			
		||||
		req.NewPut(":dbId/restores/:restoreId", d.Update).Log(req.NewLogSave("db-保存数据库恢复任务")),
 | 
			
		||||
		// 启用数据库备份任务
 | 
			
		||||
		req.NewPut(":dbId/restores/:restoreId/enable", d.Enable).Log(req.NewLogSave("db-启用数据库恢复任务")),
 | 
			
		||||
		// 禁用数据库备份任务
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ func (m *roleRepoImpl) GetRoleResources(roleId uint64, toEntity any) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *roleRepoImpl) SaveRoleResource(rr []*entity.RoleResource) {
 | 
			
		||||
	gormx.BatchInsert(rr)
 | 
			
		||||
	gormx.BatchInsert[*entity.RoleResource](rr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *roleRepoImpl) DeleteRoleResource(roleId uint64, resourceId uint64) {
 | 
			
		||||
 
 | 
			
		||||
@@ -103,8 +103,8 @@ func (ai *AppImpl[T, R]) BatchInsert(ctx context.Context, es []T) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 使用指定gorm db执行,主要用于事务执行
 | 
			
		||||
func (ai *AppImpl[T, R]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []T) error {
 | 
			
		||||
	return ai.GetRepo().BatchInsertWithDb(ctx, db, es)
 | 
			
		||||
func (ai *AppImpl[T, R]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, models []T) error {
 | 
			
		||||
	return ai.GetRepo().BatchInsertWithDb(ctx, db, models)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据实体id更新实体信息 (单纯更新,不做其他业务逻辑处理)
 | 
			
		||||
 
 | 
			
		||||
@@ -22,10 +22,10 @@ type Repo[T model.ModelI] interface {
 | 
			
		||||
	InsertWithDb(ctx context.Context, db *gorm.DB, e T) error
 | 
			
		||||
 | 
			
		||||
	// 批量新增实体
 | 
			
		||||
	BatchInsert(ctx context.Context, models []T) error
 | 
			
		||||
	BatchInsert(ctx context.Context, models any) error
 | 
			
		||||
 | 
			
		||||
	// 使用指定gorm db执行,主要用于事务执行
 | 
			
		||||
	BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []T) error
 | 
			
		||||
	BatchInsertWithDb(ctx context.Context, db *gorm.DB, models any) error
 | 
			
		||||
 | 
			
		||||
	// 根据实体id更新实体信息
 | 
			
		||||
	UpdateById(ctx context.Context, e T, columns ...string) error
 | 
			
		||||
@@ -93,23 +93,22 @@ func (br *RepoImpl[T]) InsertWithDb(ctx context.Context, db *gorm.DB, e T) error
 | 
			
		||||
	return gormx.InsertWithDb(db, br.fillBaseInfo(ctx, e))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (br *RepoImpl[T]) BatchInsert(ctx context.Context, es []T) error {
 | 
			
		||||
func (br *RepoImpl[T]) BatchInsert(ctx context.Context, es any) error {
 | 
			
		||||
	if db := contextx.GetDb(ctx); db != nil {
 | 
			
		||||
		return br.BatchInsertWithDb(ctx, db, es)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, e := range es {
 | 
			
		||||
	for _, e := range es.([]T) {
 | 
			
		||||
		br.fillBaseInfo(ctx, e)
 | 
			
		||||
	}
 | 
			
		||||
	return gormx.BatchInsert(es)
 | 
			
		||||
	return gormx.BatchInsert[T](es)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 使用指定gorm db执行,主要用于事务执行
 | 
			
		||||
func (br *RepoImpl[T]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []T) error {
 | 
			
		||||
	for _, e := range es {
 | 
			
		||||
func (br *RepoImpl[T]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, es any) error {
 | 
			
		||||
	for _, e := range es.([]T) {
 | 
			
		||||
		br.fillBaseInfo(ctx, e)
 | 
			
		||||
	}
 | 
			
		||||
	return gormx.BatchInsertWithDb(db, es)
 | 
			
		||||
	return gormx.BatchInsertWithDb[T](db, es)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (br *RepoImpl[T]) UpdateById(ctx context.Context, e T, columns ...string) error {
 | 
			
		||||
 
 | 
			
		||||
@@ -135,13 +135,13 @@ func InsertWithDb(db *gorm.DB, model any) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 批量插入
 | 
			
		||||
func BatchInsert[T any](models []T) error {
 | 
			
		||||
func BatchInsert[T any](models any) error {
 | 
			
		||||
	return BatchInsertWithDb[T](global.Db, models)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 批量插入
 | 
			
		||||
func BatchInsertWithDb[T any](db *gorm.DB, models []T) error {
 | 
			
		||||
	return db.CreateInBatches(models, len(models)).Error
 | 
			
		||||
func BatchInsertWithDb[T any](db *gorm.DB, models any) error {
 | 
			
		||||
	return db.CreateInBatches(models, len(models.([]T))).Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据id更新model,更新字段为model中不为空的值,即int类型不为0,ptr类型不为nil这类字段值
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package queue
 | 
			
		||||
package runner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
@@ -7,7 +7,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const minTimerDelay = time.Millisecond
 | 
			
		||||
const minTimerDelay = time.Millisecond * 1
 | 
			
		||||
const maxTimerDelay = time.Nanosecond * math.MaxInt64
 | 
			
		||||
 | 
			
		||||
type DelayQueue[T Delayable] struct {
 | 
			
		||||
@@ -17,14 +17,12 @@ type DelayQueue[T Delayable] struct {
 | 
			
		||||
	singleDequeue  chan struct{}
 | 
			
		||||
	mutex          sync.Mutex
 | 
			
		||||
	priorityQueue  *PriorityQueue[T]
 | 
			
		||||
	elmMap         map[uint64]T
 | 
			
		||||
 | 
			
		||||
	zero T
 | 
			
		||||
	zero           T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Delayable interface {
 | 
			
		||||
	GetDeadline() time.Time
 | 
			
		||||
	GetId() uint64
 | 
			
		||||
	GetKey() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDelayQueue[T Delayable](cap int) *DelayQueue[T] {
 | 
			
		||||
@@ -35,7 +33,6 @@ func NewDelayQueue[T Delayable](cap int) *DelayQueue[T] {
 | 
			
		||||
		dequeuedSignal: make(chan struct{}),
 | 
			
		||||
		transferChan:   make(chan T),
 | 
			
		||||
		singleDequeue:  singleDequeue,
 | 
			
		||||
		elmMap:         make(map[uint64]T, 64),
 | 
			
		||||
		priorityQueue: NewPriorityQueue[T](cap, func(src T, dst T) bool {
 | 
			
		||||
			return src.GetDeadline().Before(dst.GetDeadline())
 | 
			
		||||
		}),
 | 
			
		||||
@@ -135,7 +132,6 @@ func (s *DelayQueue[T]) dequeue() (T, bool) {
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return s.zero, false
 | 
			
		||||
	}
 | 
			
		||||
	delete(s.elmMap, elm.GetId())
 | 
			
		||||
	select {
 | 
			
		||||
	case s.dequeuedSignal <- struct{}{}:
 | 
			
		||||
	default:
 | 
			
		||||
@@ -147,7 +143,6 @@ func (s *DelayQueue[T]) enqueue(val T) bool {
 | 
			
		||||
	if ok := s.priorityQueue.Enqueue(val); !ok {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	s.elmMap[val.GetId()] = val
 | 
			
		||||
	select {
 | 
			
		||||
	case s.enqueuedSignal <- struct{}{}:
 | 
			
		||||
	default:
 | 
			
		||||
@@ -169,10 +164,6 @@ func (s *DelayQueue[T]) Enqueue(ctx context.Context, val T) bool {
 | 
			
		||||
	for {
 | 
			
		||||
		// 全局锁:避免入队和出队信号的重置与激活出现并发问题
 | 
			
		||||
		s.mutex.Lock()
 | 
			
		||||
		if _, ok := s.elmMap[val.GetId()]; ok {
 | 
			
		||||
			s.mutex.Unlock()
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ctx.Err() != nil {
 | 
			
		||||
			s.mutex.Unlock()
 | 
			
		||||
@@ -220,24 +211,20 @@ func (s *DelayQueue[T]) Enqueue(ctx context.Context, val T) bool {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DelayQueue[T]) Remove(_ context.Context, elmId uint64) (T, bool) {
 | 
			
		||||
func (s *DelayQueue[T]) Remove(_ context.Context, key string) (T, bool) {
 | 
			
		||||
	s.mutex.Lock()
 | 
			
		||||
	defer s.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if _, ok := s.elmMap[elmId]; ok {
 | 
			
		||||
		delete(s.elmMap, elmId)
 | 
			
		||||
		return s.priorityQueue.Remove(s.index(elmId))
 | 
			
		||||
	}
 | 
			
		||||
	return s.zero, false
 | 
			
		||||
	return s.priorityQueue.Remove(s.index(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DelayQueue[T]) index(elmId uint64) int {
 | 
			
		||||
func (s *DelayQueue[T]) index(key string) int {
 | 
			
		||||
	for i := 0; i < s.priorityQueue.Len(); i++ {
 | 
			
		||||
		elm, ok := s.priorityQueue.Peek(i)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if elmId == elm.GetId() {
 | 
			
		||||
		if key == elm.GetKey() {
 | 
			
		||||
			return i
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package queue
 | 
			
		||||
package runner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
@@ -6,6 +6,7 @@ import (
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"testing"
 | 
			
		||||
@@ -28,6 +29,10 @@ func (elm *delayElement) GetId() uint64 {
 | 
			
		||||
	return elm.id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (elm *delayElement) GetKey() string {
 | 
			
		||||
	return strconv.FormatUint(elm.id, 16)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type testDelayQueue = DelayQueue[*delayElement]
 | 
			
		||||
 | 
			
		||||
func newTestDelayQueue(cap int) *testDelayQueue {
 | 
			
		||||
@@ -42,7 +47,6 @@ func mustEnqueue(val int, delay int64) func(t *testing.T, queue *testDelayQueue)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newTestElm(value int, delay int64) *delayElement {
 | 
			
		||||
 | 
			
		||||
	return &delayElement{
 | 
			
		||||
		id:       elmId.Add(1),
 | 
			
		||||
		value:    value,
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package queue
 | 
			
		||||
package runner
 | 
			
		||||
 | 
			
		||||
//var (
 | 
			
		||||
//	false  = errors.New("queue: 队列已满")
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package queue
 | 
			
		||||
package runner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
							
								
								
									
										337
									
								
								server/pkg/runner/runner.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								server/pkg/runner/runner.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,337 @@
 | 
			
		||||
package runner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/emirpasic/gods/maps/linkedhashmap"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type JobKey = string
 | 
			
		||||
type RunFunc func(ctx context.Context)
 | 
			
		||||
type NextFunc func() (Job, bool)
 | 
			
		||||
type RunnableFunc func(next NextFunc) bool
 | 
			
		||||
 | 
			
		||||
type JobStatus int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	JobUnknown JobStatus = iota
 | 
			
		||||
	JobDelay
 | 
			
		||||
	JobWaiting
 | 
			
		||||
	JobRunning
 | 
			
		||||
	JobRemoved
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Job interface {
 | 
			
		||||
	GetKey() JobKey
 | 
			
		||||
	GetStatus() JobStatus
 | 
			
		||||
	SetStatus(status JobStatus)
 | 
			
		||||
	Run(ctx context.Context)
 | 
			
		||||
	Runnable(next NextFunc) bool
 | 
			
		||||
	GetDeadline() time.Time
 | 
			
		||||
	Schedule() bool
 | 
			
		||||
	Renew(job Job)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type iterator[T Job] struct {
 | 
			
		||||
	index int
 | 
			
		||||
	data  []T
 | 
			
		||||
	zero  T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (iter *iterator[T]) Begin() {
 | 
			
		||||
	iter.index = -1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (iter *iterator[T]) Next() (T, bool) {
 | 
			
		||||
	if iter.index >= len(iter.data)-1 {
 | 
			
		||||
		return iter.zero, false
 | 
			
		||||
	}
 | 
			
		||||
	iter.index++
 | 
			
		||||
	return iter.data[iter.index], true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type array[T Job] struct {
 | 
			
		||||
	size int
 | 
			
		||||
	data []T
 | 
			
		||||
	zero T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newArray[T Job](size int) *array[T] {
 | 
			
		||||
	return &array[T]{
 | 
			
		||||
		size: size,
 | 
			
		||||
		data: make([]T, 0, size),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *array[T]) Iterator() *iterator[T] {
 | 
			
		||||
	return &iterator[T]{
 | 
			
		||||
		index: -1,
 | 
			
		||||
		data:  a.data,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *array[T]) Full() bool {
 | 
			
		||||
	return len(a.data) >= a.size
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *array[T]) Append(job T) bool {
 | 
			
		||||
	if len(a.data) >= a.size {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	a.data = append(a.data, job)
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *array[T]) Get(key JobKey) (T, bool) {
 | 
			
		||||
	for _, job := range a.data {
 | 
			
		||||
		if key == job.GetKey() {
 | 
			
		||||
			return job, true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return a.zero, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *array[T]) Remove(key JobKey) {
 | 
			
		||||
	length := len(a.data)
 | 
			
		||||
	for i, elm := range a.data {
 | 
			
		||||
		if key == elm.GetKey() {
 | 
			
		||||
			a.data[i], a.data[length-1] = a.data[length-1], a.zero
 | 
			
		||||
			a.data = a.data[:length-1]
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Runner[T Job] struct {
 | 
			
		||||
	maxRunning int
 | 
			
		||||
	waiting    *linkedhashmap.Map
 | 
			
		||||
	running    *array[T]
 | 
			
		||||
	runnable   func(job T, iterateRunning func() (T, bool)) bool
 | 
			
		||||
	mutex      sync.Mutex
 | 
			
		||||
	wg         sync.WaitGroup
 | 
			
		||||
	context    context.Context
 | 
			
		||||
	cancel     context.CancelFunc
 | 
			
		||||
	zero       T
 | 
			
		||||
	signal     chan struct{}
 | 
			
		||||
	all        map[string]T
 | 
			
		||||
	delayQueue *DelayQueue[T]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRunner[T Job](maxRunning int) *Runner[T] {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	runner := &Runner[T]{
 | 
			
		||||
		maxRunning: maxRunning,
 | 
			
		||||
		all:        make(map[string]T, maxRunning),
 | 
			
		||||
		waiting:    linkedhashmap.New(),
 | 
			
		||||
		running:    newArray[T](maxRunning),
 | 
			
		||||
		context:    ctx,
 | 
			
		||||
		cancel:     cancel,
 | 
			
		||||
		signal:     make(chan struct{}, 1),
 | 
			
		||||
		delayQueue: NewDelayQueue[T](0),
 | 
			
		||||
	}
 | 
			
		||||
	runner.wg.Add(maxRunning + 1)
 | 
			
		||||
	for i := 0; i < maxRunning; i++ {
 | 
			
		||||
		go runner.run()
 | 
			
		||||
	}
 | 
			
		||||
	go func() {
 | 
			
		||||
		defer runner.wg.Done()
 | 
			
		||||
		for runner.context.Err() == nil {
 | 
			
		||||
			job, ok := runner.delayQueue.Dequeue(ctx)
 | 
			
		||||
			if !ok {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			runner.mutex.Lock()
 | 
			
		||||
			runner.waiting.Put(job.GetKey(), job)
 | 
			
		||||
			job.SetStatus(JobWaiting)
 | 
			
		||||
			runner.trigger()
 | 
			
		||||
			runner.mutex.Unlock()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	return runner
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) Close() {
 | 
			
		||||
	r.cancel()
 | 
			
		||||
	r.wg.Wait()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) run() {
 | 
			
		||||
	defer r.wg.Done()
 | 
			
		||||
 | 
			
		||||
	for r.context.Err() == nil {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-r.signal:
 | 
			
		||||
			job, ok := r.pickRunnable()
 | 
			
		||||
			if !ok {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			r.doRun(job)
 | 
			
		||||
			r.afterRun(job)
 | 
			
		||||
		case <-r.context.Done():
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) doRun(job T) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := recover(); err != nil {
 | 
			
		||||
			logx.Error(fmt.Sprintf("failed to run job: %v", err))
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	job.Run(r.context)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) afterRun(job T) {
 | 
			
		||||
	r.mutex.Lock()
 | 
			
		||||
	defer r.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	key := job.GetKey()
 | 
			
		||||
	r.running.Remove(key)
 | 
			
		||||
	r.trigger()
 | 
			
		||||
	switch job.GetStatus() {
 | 
			
		||||
	case JobRunning:
 | 
			
		||||
		r.schedule(r.context, job)
 | 
			
		||||
	case JobRemoved:
 | 
			
		||||
		delete(r.all, key)
 | 
			
		||||
	default:
 | 
			
		||||
		panic(fmt.Sprintf("invalid job status %v occurred after run", job.GetStatus()))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) pickRunnable() (T, bool) {
 | 
			
		||||
	r.mutex.Lock()
 | 
			
		||||
	defer r.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	iter := r.running.Iterator()
 | 
			
		||||
	var runnable T
 | 
			
		||||
	ok := r.waiting.Any(func(key interface{}, value interface{}) bool {
 | 
			
		||||
		job := value.(T)
 | 
			
		||||
		iter.Begin()
 | 
			
		||||
		if job.Runnable(func() (Job, bool) { return iter.Next() }) {
 | 
			
		||||
			if r.running.Full() {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			r.waiting.Remove(key)
 | 
			
		||||
			r.running.Append(job)
 | 
			
		||||
			job.SetStatus(JobRunning)
 | 
			
		||||
			if !r.running.Full() && !r.waiting.Empty() {
 | 
			
		||||
				r.trigger()
 | 
			
		||||
			}
 | 
			
		||||
			runnable = job
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	})
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return r.zero, false
 | 
			
		||||
	}
 | 
			
		||||
	return runnable, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) schedule(ctx context.Context, job T) {
 | 
			
		||||
	if !job.Schedule() {
 | 
			
		||||
		delete(r.all, job.GetKey())
 | 
			
		||||
		job.SetStatus(JobRemoved)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	r.delayQueue.Enqueue(ctx, job)
 | 
			
		||||
	job.SetStatus(JobDelay)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//func (r *Runner[T]) Schedule(ctx context.Context, job T) {
 | 
			
		||||
//	r.mutex.Lock()
 | 
			
		||||
//	defer r.mutex.Unlock()
 | 
			
		||||
//
 | 
			
		||||
//	switch job.GetStatus() {
 | 
			
		||||
//	case JobUnknown:
 | 
			
		||||
//	case JobDelay:
 | 
			
		||||
//		r.delayQueue.Remove(ctx, job.GetKey())
 | 
			
		||||
//	case JobWaiting:
 | 
			
		||||
//		r.waiting.Remove(job)
 | 
			
		||||
//	case JobRunning:
 | 
			
		||||
//		// 标记为 removed, 任务执行完成后再删除
 | 
			
		||||
//		return
 | 
			
		||||
//	case JobRemoved:
 | 
			
		||||
//		return
 | 
			
		||||
//	}
 | 
			
		||||
//	r.schedule(ctx, job)
 | 
			
		||||
//}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) Add(ctx context.Context, job T) error {
 | 
			
		||||
	r.mutex.Lock()
 | 
			
		||||
	defer r.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if _, ok := r.all[job.GetKey()]; ok {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	r.schedule(ctx, job)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) UpdateOrAdd(ctx context.Context, job T) error {
 | 
			
		||||
	r.mutex.Lock()
 | 
			
		||||
	defer r.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if old, ok := r.all[job.GetKey()]; ok {
 | 
			
		||||
		old.Renew(job)
 | 
			
		||||
		job = old
 | 
			
		||||
	}
 | 
			
		||||
	r.schedule(ctx, job)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) StartNow(ctx context.Context, job T) error {
 | 
			
		||||
	r.mutex.Lock()
 | 
			
		||||
	defer r.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	key := job.GetKey()
 | 
			
		||||
	if old, ok := r.all[key]; ok {
 | 
			
		||||
		job = old
 | 
			
		||||
		if job.GetStatus() == JobDelay {
 | 
			
		||||
			r.delayQueue.Remove(ctx, key)
 | 
			
		||||
			r.waiting.Put(key, job)
 | 
			
		||||
			r.trigger()
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	r.all[key] = job
 | 
			
		||||
	r.waiting.Put(key, job)
 | 
			
		||||
	r.trigger()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) trigger() {
 | 
			
		||||
	select {
 | 
			
		||||
	case r.signal <- struct{}{}:
 | 
			
		||||
	default:
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *Runner[T]) Remove(ctx context.Context, key JobKey) error {
 | 
			
		||||
	r.mutex.Lock()
 | 
			
		||||
	defer r.mutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	job, ok := r.all[key]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	switch job.GetStatus() {
 | 
			
		||||
	case JobUnknown:
 | 
			
		||||
		panic(fmt.Sprintf("invalid job status %v occurred after added", job.GetStatus()))
 | 
			
		||||
	case JobDelay:
 | 
			
		||||
		r.delayQueue.Remove(ctx, key)
 | 
			
		||||
	case JobWaiting:
 | 
			
		||||
		r.waiting.Remove(key)
 | 
			
		||||
	case JobRunning:
 | 
			
		||||
		// 标记为 removed, 任务执行完成后再删除
 | 
			
		||||
	case JobRemoved:
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	delete(r.all, key)
 | 
			
		||||
	job.SetStatus(JobRemoved)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										148
									
								
								server/pkg/runner/runner_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								server/pkg/runner/runner_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
			
		||||
package runner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
	"mayfly-go/pkg/utils/timex"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ Job = &testJob{}
 | 
			
		||||
 | 
			
		||||
func newTestJob(key string, runTime time.Duration) *testJob {
 | 
			
		||||
	return &testJob{
 | 
			
		||||
		deadline: time.Now(),
 | 
			
		||||
		Key:      key,
 | 
			
		||||
		run: func(ctx context.Context) {
 | 
			
		||||
			timex.SleepWithContext(ctx, runTime)
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type testJob struct {
 | 
			
		||||
	run      RunFunc
 | 
			
		||||
	Key      JobKey
 | 
			
		||||
	status   JobStatus
 | 
			
		||||
	ran      bool
 | 
			
		||||
	deadline time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) Renew(job Job) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) GetDeadline() time.Time {
 | 
			
		||||
	return t.deadline
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) Schedule() bool {
 | 
			
		||||
	return !t.ran
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) Run(ctx context.Context) {
 | 
			
		||||
	if t.run == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	t.run(ctx)
 | 
			
		||||
	t.ran = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) Runnable(_ NextFunc) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) GetKey() JobKey {
 | 
			
		||||
	return t.Key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) GetStatus() JobStatus {
 | 
			
		||||
	return t.status
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *testJob) SetStatus(status JobStatus) {
 | 
			
		||||
	t.status = status
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRunner_Close(t *testing.T) {
 | 
			
		||||
	runner := NewRunner[*testJob](1)
 | 
			
		||||
	signal := make(chan struct{}, 1)
 | 
			
		||||
	waiting := sync.WaitGroup{}
 | 
			
		||||
	waiting.Add(1)
 | 
			
		||||
	go func() {
 | 
			
		||||
		job := &testJob{
 | 
			
		||||
			Key: "close",
 | 
			
		||||
			run: func(ctx context.Context) {
 | 
			
		||||
				waiting.Done()
 | 
			
		||||
				timex.SleepWithContext(ctx, time.Hour)
 | 
			
		||||
				signal <- struct{}{}
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		_ = runner.Add(context.Background(), job)
 | 
			
		||||
	}()
 | 
			
		||||
	waiting.Wait()
 | 
			
		||||
	timer := time.NewTimer(time.Microsecond * 10)
 | 
			
		||||
	runner.Close()
 | 
			
		||||
	select {
 | 
			
		||||
	case <-timer.C:
 | 
			
		||||
		require.FailNow(t, "runner 未能及时退出")
 | 
			
		||||
	case <-signal:
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRunner_AddJob(t *testing.T) {
 | 
			
		||||
	type testCase struct {
 | 
			
		||||
		name string
 | 
			
		||||
		job  *testJob
 | 
			
		||||
		want bool
 | 
			
		||||
	}
 | 
			
		||||
	testCases := []testCase{
 | 
			
		||||
		{
 | 
			
		||||
			name: "first job",
 | 
			
		||||
			job:  newTestJob("single", time.Hour),
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "second job",
 | 
			
		||||
			job:  newTestJob("dual", time.Hour),
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "non repetitive job",
 | 
			
		||||
			job:  newTestJob("single", time.Hour),
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "repetitive job",
 | 
			
		||||
			job:  newTestJob("dual", time.Hour),
 | 
			
		||||
			want: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	runner := NewRunner[*testJob](1)
 | 
			
		||||
	defer runner.Close()
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		err := runner.Add(context.Background(), tc.job)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestJob_UpdateStatus(t *testing.T) {
 | 
			
		||||
	const d = time.Millisecond * 20
 | 
			
		||||
	runner := NewRunner[*testJob](1)
 | 
			
		||||
	running := newTestJob("running", d*2)
 | 
			
		||||
	waiting := newTestJob("waiting", d*2)
 | 
			
		||||
	_ = runner.Add(context.Background(), running)
 | 
			
		||||
	_ = runner.Add(context.Background(), waiting)
 | 
			
		||||
 | 
			
		||||
	time.Sleep(d)
 | 
			
		||||
	require.Equal(t, JobRunning, running.status)
 | 
			
		||||
	require.Equal(t, JobWaiting, waiting.status)
 | 
			
		||||
 | 
			
		||||
	time.Sleep(d * 2)
 | 
			
		||||
	require.Equal(t, JobRemoved, running.status)
 | 
			
		||||
	require.Equal(t, JobRunning, waiting.status)
 | 
			
		||||
 | 
			
		||||
	time.Sleep(d * 2)
 | 
			
		||||
	require.Equal(t, JobRemoved, running.status)
 | 
			
		||||
	require.Equal(t, JobRemoved, waiting.status)
 | 
			
		||||
}
 | 
			
		||||
@@ -41,6 +41,7 @@ func runWebServer(ctx context.Context) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logx.Errorf("Failed to Shutdown HTTP Server: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		initialize.Terminate()
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -272,7 +272,6 @@ func pkcs7UnPadding(data []byte) ([]byte, error) {
 | 
			
		||||
	}
 | 
			
		||||
	//获取填充的个数
 | 
			
		||||
	unPadding := int(data[length-1])
 | 
			
		||||
	// todo fix: slice bounds out of range
 | 
			
		||||
	if unPadding > length {
 | 
			
		||||
		return nil, errors.New("解密字符串时去除填充个数超出字符串长度")
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user