Files
likedream/internal/framework/schedule/scanner.go
T
sugarBecomer f71ee4fab4 🐛 定时任务
2025-11-06 16:31:17 +08:00

177 lines
5.0 KiB
Go

package schedule
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"likedream-goframe/internal/cst"
"likedream-goframe/internal/db"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/ent/cptask"
"likedream-goframe/internal/framework/queue"
"likedream-goframe/internal/utils"
"slices"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/hibiken/asynq"
"github.com/samber/oops"
)
var globalScanner *TaskScanner
func initScanner() {
globalScanner = NewTaskScanner()
go globalScanner.StartScan()
}
type TaskScanner struct {
ctx context.Context
stopSign chan struct{}
cancelFunc context.CancelFunc
}
func NewTaskScanner() *TaskScanner {
ctx, cancelFunc := context.WithCancel(gctx.GetInitCtx())
return &TaskScanner{
ctx: ctx,
stopSign: make(chan struct{}),
cancelFunc: cancelFunc,
}
}
func waitWithContext(ctx context.Context, d time.Duration) bool {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
return false
case <-t.C:
return true
}
}
func (ts *TaskScanner) StartScan() {
for {
if !waitWithContext(ts.ctx, 30*time.Second) {
close(ts.stopSign)
return
}
ts.scanCpdailyTask()
}
}
func (ts *TaskScanner) StopScan() {
ts.cancelFunc()
timeoutTimer := time.NewTimer(10 * time.Second)
defer timeoutTimer.Stop()
select {
case <-timeoutTimer.C:
g.Log().Warningf(ts.ctx, "task scanner stop timeout")
case <-ts.stopSign:
}
}
func (ts *TaskScanner) scanCpdailyTask() {
currentTime := time.Now().In(cst.TimeZoneShangHai)
// 只查询30分钟以内的任务
minExecuteTime := currentTime.Add(-30 * time.Minute).Unix()
allTasks := db.Global.CpTask.Query().
Where(cptask.NextExecuteTimeLTE(currentTime.Unix()), cptask.NextExecuteTimeNotNil(), cptask.NextExecuteTimeGT(0), cptask.StepEQ(cptask.StepComplete), cptask.TaskStatusEQ(cptask.TaskStatusNotStart)).
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute, cptask.FieldNextExecuteTime).
AllX(ts.ctx)
var failedIDS []int64
for _, task := range allTasks {
// 30分钟之前的任务不再执行,但是要重置下次执行时间
if *task.NextExecuteTime < minExecuteTime {
originExecuteTime := time.Unix(*task.NextExecuteTime, 0).In(cst.TimeZoneShangHai).Format("2006-01-02 15:04:05")
g.Log().Debugf(ts.ctx, "任务ID %d 已过期,原来预计执行时间:%s,跳过执行", task.ID, originExecuteTime)
continue
}
payload := &CpdailyTaskPayload{
CpTaskID: task.ID,
}
payloadWriter := bytes.NewBuffer(nil)
e := gob.NewEncoder(payloadWriter).Encode(payload)
utils.OkOrPanic(e, oops.WithContext(ts.ctx).With("payload", payload).In("CpdailyTaskScanner.scanCpdailyTask").Wrap)
year, month, day := currentTime.Date()
execTime := time.Date(year, month, day, *task.ExecuteHour, *task.ExecuteMinute, 0, 0, cst.TimeZoneShangHai)
e = utils.Catch(func() {
queue.AddTask(ts.ctx, asynq.NewTask(cst.TaskCpdaily, payloadWriter.Bytes()),
asynq.TaskID(fmt.Sprintf("cpdaily:task:%d", task.ID)),
asynq.MaxRetry(3),
asynq.Unique(10*time.Minute),
asynq.ProcessAt(execTime),
asynq.Deadline(execTime.Add(12*time.Minute)),
asynq.Queue(cst.TaskQueueCpdaily),
)
})
if e != nil {
if errors.Is(e, asynq.ErrDuplicateTask) {
continue
} else if errors.Is(e, context.Canceled) {
return
} else {
utils.LogError(ts.ctx, "扫描今日校园任务时,入列失败", oops.With("task_id", task.ID).Wrap(e))
failedIDS = append(failedIDS, task.ID)
}
}
}
if len(failedIDS) > 0 {
allTasks = slices.DeleteFunc(allTasks, func(task *ent.CpTask) bool {
return slices.Contains(failedIDS, task.ID)
})
}
for _, task := range allTasks {
nextExecuteAt := utils.GetCpdailyNextExecuteTime(task)
e := db.Global.CpTask.UpdateOneID(task.ID).Where(cptask.NextExecuteTimeNotNil()).SetNextExecuteTime(nextExecuteAt).Exec(ts.ctx)
utils.LogError(ts.ctx, "更新任务下次执行时间失败", oops.With("task_id", task.ID, "next_execute_at", nextExecuteAt).Wrap(e))
}
// 重置30分钟之前的任务到下次执行时间
expiredTasks := db.Global.CpTask.Query().
Where(cptask.NextExecuteTimeLT(minExecuteTime), cptask.NextExecuteTimeNotNil(), cptask.NextExecuteTimeGT(0)).
Select(cptask.FieldID, cptask.FieldExecuteHour, cptask.FieldExecuteMinute).
AllX(ts.ctx)
expiredIDs := slices.Collect(func(yield func(string) bool) {
for _, task := range expiredTasks {
if !yield(strconv.FormatInt(task.ID, 10)) {
return
}
}
})
if len(expiredIDs) > 0 {
g.Log().Warningf(ts.ctx, "以下任务ID已过期,重置了下次执行时间: %v", strings.Join(expiredIDs, ", "))
}
for _, task := range expiredTasks {
nextExecuteAt := utils.GetCpdailyNextExecuteTime(task)
e := db.Global.CpTask.UpdateOneID(task.ID).Where(cptask.NextExecuteTimeNotNil()).SetNextExecuteTime(nextExecuteAt).Exec(ts.ctx)
utils.LogError(ts.ctx, "重置过期任务下次执行时间失败", oops.With("task_id", task.ID, "next_execute_at", nextExecuteAt).Wrap(e))
}
}