mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 00:10:25 +08:00 
			
		
		
		
	* feat: 优化数据库 BINLOG 同步机制 * feat: 删除数据库实例前需删除关联的数据库备份与恢复任务 * refactor: 重构数据库备份与恢复模块 * feat: 定时清理数据库备份历史和本地 Binlog 文件 * feat: 压缩数据库备份文件
		
			
				
	
	
		
			426 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			426 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package runner
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"github.com/emirpasic/gods/maps/linkedhashmap"
 | 
						|
	"mayfly-go/pkg/logx"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	ErrJobNotFound = errors.New("任务未找到")
 | 
						|
	ErrJobExist    = errors.New("任务已存在")
 | 
						|
	ErrJobFinished = errors.New("任务已完成")
 | 
						|
	ErrJobDisabled = errors.New("任务已禁用")
 | 
						|
	ErrJobExpired  = errors.New("任务已过期")
 | 
						|
	ErrJobRunning  = errors.New("任务执行中")
 | 
						|
)
 | 
						|
 | 
						|
type JobKey = string
 | 
						|
type RunJobFunc[T Job] func(ctx context.Context, job T) error
 | 
						|
type NextJobFunc[T Job] func() (T, bool)
 | 
						|
type RunnableJobFunc[T Job] func(job T, nextRunning NextJobFunc[T]) (bool, error)
 | 
						|
type ScheduleJobFunc[T Job] func(job T) (deadline time.Time, err error)
 | 
						|
type UpdateJobFunc[T Job] func(ctx context.Context, job T) error
 | 
						|
 | 
						|
type JobStatus int
 | 
						|
 | 
						|
const (
 | 
						|
	JobUnknown JobStatus = iota
 | 
						|
	JobDelaying
 | 
						|
	JobWaiting
 | 
						|
	JobRunning
 | 
						|
	JobSuccess
 | 
						|
	JobFailed
 | 
						|
)
 | 
						|
 | 
						|
type Job interface {
 | 
						|
	GetKey() JobKey
 | 
						|
	Update(job Job)
 | 
						|
	SetStatus(status JobStatus, err error)
 | 
						|
	SetEnabled(enabled bool, desc string)
 | 
						|
}
 | 
						|
 | 
						|
type iterator[T Job] struct {
 | 
						|
	index int
 | 
						|
	data  []*wrapper[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].job, true
 | 
						|
}
 | 
						|
 | 
						|
type array[T Job] struct {
 | 
						|
	size int
 | 
						|
	data []*wrapper[T]
 | 
						|
}
 | 
						|
 | 
						|
func newArray[T Job](size int) *array[T] {
 | 
						|
	return &array[T]{
 | 
						|
		size: size,
 | 
						|
		data: make([]*wrapper[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 *wrapper[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) (*wrapper[T], bool) {
 | 
						|
	for _, job := range a.data {
 | 
						|
		if key == job.GetKey() {
 | 
						|
			return job, true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil, 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], nil
 | 
						|
			a.data = a.data[:length-1]
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type Runner[T Job] struct {
 | 
						|
	maxRunning  int
 | 
						|
	waiting     *linkedhashmap.Map
 | 
						|
	running     *array[T]
 | 
						|
	runJob      RunJobFunc[T]
 | 
						|
	runnableJob RunnableJobFunc[T]
 | 
						|
	scheduleJob ScheduleJobFunc[T]
 | 
						|
	updateJob   UpdateJobFunc[T]
 | 
						|
	mutex       sync.Mutex
 | 
						|
	wg          sync.WaitGroup
 | 
						|
	context     context.Context
 | 
						|
	cancel      context.CancelFunc
 | 
						|
	zero        T
 | 
						|
	signal      chan struct{}
 | 
						|
	all         map[JobKey]*wrapper[T]
 | 
						|
	delayQueue  *DelayQueue[*wrapper[T]]
 | 
						|
}
 | 
						|
 | 
						|
type Opt[T Job] func(runner *Runner[T])
 | 
						|
 | 
						|
func WithRunnableJob[T Job](runnableJob RunnableJobFunc[T]) Opt[T] {
 | 
						|
	return func(runner *Runner[T]) {
 | 
						|
		runner.runnableJob = runnableJob
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func WithScheduleJob[T Job](scheduleJob ScheduleJobFunc[T]) Opt[T] {
 | 
						|
	return func(runner *Runner[T]) {
 | 
						|
		runner.scheduleJob = scheduleJob
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func WithUpdateJob[T Job](updateJob UpdateJobFunc[T]) Opt[T] {
 | 
						|
	return func(runner *Runner[T]) {
 | 
						|
		runner.updateJob = updateJob
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func NewRunner[T Job](maxRunning int, runJob RunJobFunc[T], opts ...Opt[T]) *Runner[T] {
 | 
						|
	ctx, cancel := context.WithCancel(context.Background())
 | 
						|
	runner := &Runner[T]{
 | 
						|
		maxRunning: maxRunning,
 | 
						|
		all:        make(map[string]*wrapper[T], maxRunning),
 | 
						|
		waiting:    linkedhashmap.New(),
 | 
						|
		running:    newArray[T](maxRunning),
 | 
						|
		context:    ctx,
 | 
						|
		cancel:     cancel,
 | 
						|
		signal:     make(chan struct{}, 1),
 | 
						|
		delayQueue: NewDelayQueue[*wrapper[T]](0),
 | 
						|
	}
 | 
						|
	runner.runJob = runJob
 | 
						|
	runner.runnableJob = func(job T, _ NextJobFunc[T]) (bool, error) {
 | 
						|
		return true, nil
 | 
						|
	}
 | 
						|
	runner.updateJob = func(ctx context.Context, job T) error {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	for _, opt := range opts {
 | 
						|
		opt(runner)
 | 
						|
	}
 | 
						|
 | 
						|
	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 {
 | 
						|
			wrap, ok := runner.delayQueue.Dequeue(ctx)
 | 
						|
			if !ok {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			runner.mutex.Lock()
 | 
						|
			if old, ok := runner.all[wrap.key]; !ok || wrap != old {
 | 
						|
				runner.mutex.Unlock()
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			runner.waiting.Put(wrap.key, wrap)
 | 
						|
			wrap.status = 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:
 | 
						|
			wrap, ok := r.pickRunnableJob()
 | 
						|
			if !ok {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			r.doRun(wrap)
 | 
						|
			r.afterRun(wrap)
 | 
						|
		case <-r.context.Done():
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) doRun(wrap *wrapper[T]) {
 | 
						|
	defer func() {
 | 
						|
		if err := recover(); err != nil {
 | 
						|
			logx.Error(fmt.Sprintf("failed to run job: %v", err))
 | 
						|
			time.Sleep(time.Millisecond * 10)
 | 
						|
		}
 | 
						|
	}()
 | 
						|
	wrap.job.SetStatus(JobRunning, nil)
 | 
						|
	if err := r.updateJob(r.context, wrap.job); err != nil {
 | 
						|
		err = fmt.Errorf("任务状态保存失败: %w", err)
 | 
						|
		wrap.job.SetStatus(JobFailed, err)
 | 
						|
		_ = r.updateJob(r.context, wrap.job)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	runErr := r.runJob(r.context, wrap.job)
 | 
						|
	if runErr != nil {
 | 
						|
		wrap.job.SetStatus(JobFailed, runErr)
 | 
						|
	} else {
 | 
						|
		wrap.job.SetStatus(JobSuccess, nil)
 | 
						|
	}
 | 
						|
	if err := r.updateJob(r.context, wrap.job); err != nil {
 | 
						|
		if runErr != nil {
 | 
						|
			err = fmt.Errorf("任务状态保存失败: %w, %w", err, runErr)
 | 
						|
		} else {
 | 
						|
			err = fmt.Errorf("任务状态保存失败: %w", err)
 | 
						|
		}
 | 
						|
		wrap.job.SetStatus(JobFailed, err)
 | 
						|
		_ = r.updateJob(r.context, wrap.job)
 | 
						|
		return
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) afterRun(wrap *wrapper[T]) {
 | 
						|
	r.mutex.Lock()
 | 
						|
	defer r.mutex.Unlock()
 | 
						|
 | 
						|
	r.running.Remove(wrap.key)
 | 
						|
	delete(r.all, wrap.key)
 | 
						|
	wrap.status = JobUnknown
 | 
						|
	r.trigger()
 | 
						|
	if wrap.removed {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	deadline, err := r.doScheduleJob(wrap.job, true)
 | 
						|
	if err != nil {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	_ = r.schedule(r.context, deadline, wrap.job)
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) doScheduleJob(job T, finished bool) (time.Time, error) {
 | 
						|
	if r.scheduleJob == nil {
 | 
						|
		if finished {
 | 
						|
			return time.Time{}, ErrJobFinished
 | 
						|
		}
 | 
						|
		return time.Now(), nil
 | 
						|
	}
 | 
						|
	return r.scheduleJob(job)
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) pickRunnableJob() (*wrapper[T], bool) {
 | 
						|
	r.mutex.Lock()
 | 
						|
	defer r.mutex.Unlock()
 | 
						|
 | 
						|
	var disabled []JobKey
 | 
						|
	iter := r.running.Iterator()
 | 
						|
	var runnable *wrapper[T]
 | 
						|
	ok := r.waiting.Any(func(key interface{}, value interface{}) bool {
 | 
						|
		wrap := value.(*wrapper[T])
 | 
						|
		iter.Begin()
 | 
						|
		able, err := r.runnableJob(wrap.job, iter.Next)
 | 
						|
		if err != nil {
 | 
						|
			wrap.job.SetEnabled(false, err.Error())
 | 
						|
			r.updateJob(r.context, wrap.job)
 | 
						|
			disabled = append(disabled, key.(JobKey))
 | 
						|
		}
 | 
						|
		if able {
 | 
						|
			if r.running.Full() {
 | 
						|
				return false
 | 
						|
			}
 | 
						|
			r.waiting.Remove(key)
 | 
						|
			r.running.Append(wrap)
 | 
						|
			wrap.status = JobRunning
 | 
						|
			if !r.running.Full() && !r.waiting.Empty() {
 | 
						|
				r.trigger()
 | 
						|
			}
 | 
						|
			runnable = wrap
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	})
 | 
						|
	for _, key := range disabled {
 | 
						|
		r.waiting.Remove(key)
 | 
						|
		delete(r.all, key)
 | 
						|
	}
 | 
						|
	if !ok {
 | 
						|
		return nil, false
 | 
						|
	}
 | 
						|
	return runnable, true
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) schedule(ctx context.Context, deadline time.Time, job T) error {
 | 
						|
	wrap := newWrapper(job)
 | 
						|
	wrap.deadline = deadline
 | 
						|
	if wrap.deadline.After(time.Now()) {
 | 
						|
		r.delayQueue.Enqueue(ctx, wrap)
 | 
						|
		wrap.status = JobDelaying
 | 
						|
	} else {
 | 
						|
		r.waiting.Put(wrap.key, wrap)
 | 
						|
		wrap.status = JobWaiting
 | 
						|
		r.trigger()
 | 
						|
	}
 | 
						|
	r.all[wrap.key] = wrap
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
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 ErrJobExist
 | 
						|
	}
 | 
						|
	deadline, err := r.doScheduleJob(job, false)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return r.schedule(ctx, deadline, job)
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) Update(ctx context.Context, job T) error {
 | 
						|
	r.mutex.Lock()
 | 
						|
	defer r.mutex.Unlock()
 | 
						|
 | 
						|
	wrap, ok := r.all[job.GetKey()]
 | 
						|
	if !ok {
 | 
						|
		return ErrJobNotFound
 | 
						|
	}
 | 
						|
	wrap.job.Update(job)
 | 
						|
	switch wrap.status {
 | 
						|
	case JobDelaying:
 | 
						|
		r.delayQueue.Remove(ctx, wrap.key)
 | 
						|
	case JobWaiting:
 | 
						|
		r.waiting.Remove(wrap.key)
 | 
						|
	case JobRunning:
 | 
						|
		return nil
 | 
						|
	default:
 | 
						|
	}
 | 
						|
	deadline, err := r.doScheduleJob(job, false)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return r.schedule(ctx, deadline, wrap.job)
 | 
						|
}
 | 
						|
 | 
						|
func (r *Runner[T]) StartNow(ctx context.Context, job T) error {
 | 
						|
	r.mutex.Lock()
 | 
						|
	defer r.mutex.Unlock()
 | 
						|
 | 
						|
	if wrap, ok := r.all[job.GetKey()]; ok {
 | 
						|
		switch wrap.status {
 | 
						|
		case JobDelaying:
 | 
						|
			r.delayQueue.Remove(ctx, wrap.key)
 | 
						|
			delete(r.all, wrap.key)
 | 
						|
		case JobWaiting, JobRunning:
 | 
						|
			return nil
 | 
						|
		default:
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return r.schedule(ctx, time.Now(), job)
 | 
						|
}
 | 
						|
 | 
						|
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()
 | 
						|
 | 
						|
	wrap, ok := r.all[key]
 | 
						|
	if !ok {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	switch wrap.status {
 | 
						|
	case JobDelaying:
 | 
						|
		r.delayQueue.Remove(ctx, key)
 | 
						|
		delete(r.all, key)
 | 
						|
	case JobWaiting:
 | 
						|
		r.waiting.Remove(key)
 | 
						|
		delete(r.all, key)
 | 
						|
	case JobRunning:
 | 
						|
		// 统一标记为 removed, 待任务执行完成后再删除
 | 
						|
		wrap.removed = true
 | 
						|
		return ErrJobRunning
 | 
						|
	default:
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 |