优化结构

This commit is contained in:
Yvan
2026-08-23 07:48:02 +08:00
parent 40632c5756
commit 7a9a738c6a
15 changed files with 590 additions and 83 deletions
+120 -14
View File
@@ -23,6 +23,8 @@ type TaskScheduler struct {
seconds *cron.Cron
mu sync.Mutex
entries map[uint]scheduledEntry
lifecycleMu sync.Mutex
started bool
ctxMu sync.RWMutex
runContext context.Context
cancel context.CancelFunc
@@ -48,31 +50,49 @@ func NewTaskScheduler(tasks *taskbiz.TaskUsecase, authorities *system.AuthorityU
return &TaskScheduler{tasks: tasks, authorities: authorities, executor: executor, logger: logger.With("mod", "timedTask"), standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, running: map[uint]struct{}{}, idle: idle, subscribers: map[uint]map[chan []byte]struct{}{}}
}
func NewTaskRuntime(scheduler *TaskScheduler) taskbiz.TaskRuntime { return scheduler }
func NewTaskRuntime(scheduler *TaskScheduler) taskbiz.TaskRuntime {
if scheduler == nil {
return nil
}
return scheduler
}
func NewTaskReloader(scheduler *TaskScheduler) system.TaskReloader { return scheduler }
func NewTaskReloader(scheduler *TaskScheduler) system.TaskReloader {
if scheduler == nil {
return nil
}
return scheduler
}
func (s *TaskScheduler) Start(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
runContext, cancel := context.WithCancel(ctx)
s.ctxMu.Lock()
s.runContext, s.cancel = runContext, cancel
s.ctxMu.Unlock()
logger := s.logger
if logger == nil {
logger = slog.Default()
}
s.lifecycleMu.Lock()
s.runMu.Lock()
if s.stopping {
if s.started || s.stopping {
s.runMu.Unlock()
s.lifecycleMu.Unlock()
cancel()
return nil
}
s.started = true
s.stopping = false
s.runMu.Unlock()
s.ctxMu.Lock()
s.runContext, s.cancel = runContext, cancel
s.ctxMu.Unlock()
s.standard.Start()
s.seconds.Start()
s.lifecycleMu.Unlock()
if err := s.Reload(runContext); err != nil {
s.logger.WarnContext(runContext, "timed task table is not ready", "error", err)
logger.WarnContext(runContext, "timed task table is not ready", "error", err)
}
s.runMu.Unlock()
// Stop cancels runContext. Waiting on the child context is important: the
// application lifecycle passes a long-lived parent context to Start and
// invokes Stop separately during graceful shutdown.
@@ -89,8 +109,10 @@ func (s *TaskScheduler) Stop(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
s.lifecycleMu.Lock()
s.runMu.Lock()
s.stopping = true
s.started = false
idle := s.idle
if idle == nil {
idle = make(chan struct{})
@@ -110,6 +132,7 @@ func (s *TaskScheduler) Stop(ctx context.Context) error {
s.ctxMu.Lock()
cancel := s.cancel
s.ctxMu.Unlock()
s.lifecycleMu.Unlock()
if cancel != nil {
cancel()
}
@@ -142,6 +165,12 @@ func (s *TaskScheduler) removeLocked(id uint) {
}
func (s *TaskScheduler) Reload(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
if s.tasks == nil {
return errors.New("任务用例未初始化")
}
s.mu.Lock()
defer s.mu.Unlock()
@@ -149,12 +178,18 @@ func (s *TaskScheduler) Reload(ctx context.Context) error {
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
type preparedTask struct {
task *taskbiz.TimedTask
schedule cron.Schedule
}
prepared := make([]preparedTask, 0, len(items))
for _, task := range items {
if task == nil {
return errors.New("任务列表包含空任务")
}
if task.Enabled {
schedule, parseErr := parseTaskSchedule(task)
if parseErr != nil {
@@ -164,13 +199,27 @@ func (s *TaskScheduler) Reload(ctx context.Context) error {
}
}
s.runMu.Lock()
if err := ctx.Err(); err != nil {
s.runMu.Unlock()
return err
}
if s.stopping {
s.runMu.Unlock()
return errors.New("任务调度器正在停止")
}
for id := range s.entries {
s.removeLocked(id)
}
for _, item := range prepared {
s.scheduleLocked(item.task, item.schedule)
}
s.logger.InfoContext(ctx, "定时任务重载完成", "task_count", len(items))
s.runMu.Unlock()
logger := s.logger
if logger == nil {
logger = slog.Default()
}
logger.InfoContext(ctx, "定时任务重载完成", "task_count", len(items))
return nil
}
@@ -216,9 +265,16 @@ func (s *TaskScheduler) finishRun(id uint) {
}
func (s *TaskScheduler) dispatch(task *taskbiz.TimedTask, trigger string, async bool) bool {
if task == nil {
return false
}
ctx, ok := s.beginRun(task.ID)
if !ok {
s.logger.Warn("timed task skipped because it is already running or the scheduler is stopping", "task_id", task.ID, "task_name", task.Name, "trigger_type", trigger)
logger := s.logger
if logger == nil {
logger = slog.Default()
}
logger.Warn("timed task skipped because it is already running or the scheduler is stopping", "task_id", task.ID, "task_name", task.Name, "trigger_type", trigger)
return false
}
run := func() {
@@ -234,25 +290,45 @@ func (s *TaskScheduler) dispatch(task *taskbiz.TimedTask, trigger string, async
}
func (s *TaskScheduler) run(ctx context.Context, task *taskbiz.TimedTask, trigger string) {
if ctx == nil {
ctx = context.Background()
}
logger := s.logger
if logger == nil {
logger = slog.Default()
}
var taskID uint
var taskName string
if task != nil {
taskID, taskName = task.ID, task.Name
}
if s.executor == nil {
logger.Error("timed task execution skipped because executor is unavailable", "task_id", taskID, "task_name", taskName, "trigger_type", trigger)
return
}
log := s.executor.Run(ctx, task, trigger)
attributes := []any{"task_id", log.TaskID, "task_name", log.TaskName, "trigger_type", log.TriggerType, "status", log.Status, "duration_ms", log.DurationMS, "started_at", log.StartedAt, "finished_at", log.FinishedAt}
if log.ErrorMsg != "" {
attributes = append(attributes, "error", log.ErrorMsg)
}
if log.Status == "success" {
s.logger.Info("timed task finished", attributes...)
logger.Info("timed task finished", attributes...)
} else {
s.logger.Error("timed task finished", attributes...)
logger.Error("timed task finished", attributes...)
}
if log.Status != "success" {
if s.authorities == nil {
logger.Warn("timed task alert skipped because authority service is unavailable", attributes...)
return
}
alertCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
defer cancel()
ids, err := s.authorities.AuthorityUserIDs(alertCtx, 888)
if err != nil {
s.logger.Error("query timed task alert recipients failed", "error", err)
logger.Error("query timed task alert recipients failed", "error", err)
return
}
s.PublishToUsers(ids, map[string]any{"taskId": task.ID, "name": task.Name, "error": log.ErrorMsg, "time": time.Now().Format(time.RFC3339)})
s.PublishToUsers(ids, map[string]any{"taskId": log.TaskID, "name": log.TaskName, "error": log.ErrorMsg, "time": time.Now().Format(time.RFC3339)})
}
}
@@ -270,6 +346,12 @@ func (s *TaskScheduler) Schedule(task *taskbiz.TimedTask) error {
}
s.mu.Lock()
defer s.mu.Unlock()
s.runMu.Lock()
stopping := s.stopping
s.runMu.Unlock()
if stopping {
return errors.New("任务调度器正在停止")
}
s.removeLocked(task.ID)
if !task.Enabled {
return nil
@@ -306,12 +388,21 @@ func (s *TaskScheduler) scheduleLocked(task *taskbiz.TimedTask, schedule cron.Sc
}
func (s *TaskScheduler) ScheduleID(ctx context.Context, id uint) error {
if ctx == nil {
ctx = context.Background()
}
if s.tasks == nil {
return errors.New("任务用例未初始化")
}
s.mu.Lock()
defer s.mu.Unlock()
task, err := s.tasks.FindTask(ctx, id)
if err != nil {
return err
}
if task == nil {
return errors.New("任务不存在")
}
var schedule cron.Schedule
if task.Enabled {
schedule, err = parseTaskSchedule(task)
@@ -319,6 +410,12 @@ func (s *TaskScheduler) ScheduleID(ctx context.Context, id uint) error {
return err
}
}
s.runMu.Lock()
stopping := s.stopping
s.runMu.Unlock()
if stopping {
return errors.New("任务调度器正在停止")
}
s.removeLocked(task.ID)
if task.Enabled {
s.scheduleLocked(cloneTimedTask(task), schedule)
@@ -327,10 +424,19 @@ func (s *TaskScheduler) ScheduleID(ctx context.Context, id uint) error {
}
func (s *TaskScheduler) TriggerID(ctx context.Context, id uint) error {
if ctx == nil {
ctx = context.Background()
}
if s.tasks == nil {
return errors.New("任务用例未初始化")
}
task, err := s.tasks.FindTask(ctx, id)
if err != nil {
return err
}
if task == nil {
return errors.New("任务不存在")
}
if !s.Trigger(task) {
return errors.New("任务正在执行或调度器正在停止")
}