fix: 防止监控任务重复调度

This commit is contained in:
dushixiang committed 2026-08-31 23:06:08 +08:00
1 parent 78006a7120
commit 217da343a0
2 files changed
+85 -10

No files matched your search

+22 -10
View File
@@ -122,19 +122,21 @@ func (s *MonitorScheduler) addTaskLocked(monitorID string, interval int) error {
// 构建 cron 表达式: @every Ns // 构建 cron 表达式: @every Ns
spec := fmt.Sprintf("@every %ds", interval) spec := fmt.Sprintf("@every %ds", interval)
// 添加到 cron 调度器 // 回调绑定本次注册的任务代际。即使异常并发或未来的新路径留下了
// 未被 map 追踪的 cron entry,旧代际在执行前也会自行失效。
task := &MonitorTask{ID: monitorID}
entryID, err := s.cron.AddFunc(spec, func() { entryID, err := s.cron.AddFunc(spec, func() {
s.executeTask(monitorID) if s.isCurrentTask(task) {
s.executeTask(monitorID)
}
}) })
if err != nil { if err != nil {
return fmt.Errorf("添加 cron 任务失败: %w", err) return fmt.Errorf("添加 cron 任务失败: %w", err)
} }
// 保存任务信息 // 保存任务信息
s.tasks[monitorID] = &MonitorTask{ task.EntryID = entryID
ID: monitorID, s.tasks[monitorID] = task
EntryID: entryID,
}
s.logger.Info("添加监控任务", s.logger.Info("添加监控任务",
zap.String("taskID", monitorID), zap.String("taskID", monitorID),
@@ -143,11 +145,21 @@ func (s *MonitorScheduler) addTaskLocked(monitorID string, interval int) error {
return nil return nil
} }
// UpdateTask 更新监控任务(先删除再添加) // isCurrentTask 判断 cron 回调是否仍属于该监控任务的当前注册代际。
func (s *MonitorScheduler) isCurrentTask(task *MonitorTask) bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.tasks[task.ID] == task
}
// UpdateTask 原子更新监控任务。所有 cron entry 与 tasks 映射的变更必须
// 共用同一把锁,否则并发更新可能各自添加 entry,最终只有一个被记录,
// 其余 entry 会成为无法移除且持续重复执行的孤儿任务。
func (s *MonitorScheduler) UpdateTask(monitorID string, interval int) error { func (s *MonitorScheduler) UpdateTask(monitorID string, interval int) error {
// 移除旧监控任务 s.mu.Lock()
s.removeTaskLocked(monitorID) defer s.mu.Unlock()
// 添加新任务
// addTaskLocked 会在添加前移除已有 entry。
return s.addTaskLocked(monitorID, interval) return s.addTaskLocked(monitorID, interval)
} }
@@ -0,0 +1,63 @@
package scheduler
import (
"sync"
"testing"
"go.uber.org/zap"
)
func TestMonitorSchedulerConcurrentUpdateKeepsSingleCronEntry(t *testing.T) {
scheduler := NewMonitorScheduler(nil, zap.NewNop())
const monitorID = "monitor-1"
if err := scheduler.AddTask(monitorID, 3600); err != nil {
t.Fatalf("AddTask() error = %v", err)
}
scheduler.mu.RLock()
firstGeneration := scheduler.tasks[monitorID]
scheduler.mu.RUnlock()
const updates = 64
start := make(chan struct{})
var wg sync.WaitGroup
wg.Add(updates)
for i := 0; i < updates; i++ {
interval := 3600 + i
go func() {
defer wg.Done()
<-start
if err := scheduler.UpdateTask(monitorID, interval); err != nil {
t.Errorf("UpdateTask() error = %v", err)
}
}()
}
close(start)
wg.Wait()
if got := scheduler.GetTaskCount(); got != 1 {
t.Fatalf("GetTaskCount() = %d, want 1", got)
}
entries := scheduler.cron.Entries()
if len(entries) != 1 {
t.Fatalf("len(cron.Entries()) = %d, want 1", len(entries))
}
scheduler.mu.RLock()
task := scheduler.tasks[monitorID]
scheduler.mu.RUnlock()
if task == nil {
t.Fatal("task is missing from scheduler map")
}
if task.EntryID != entries[0].ID {
t.Fatalf("tracked entry ID = %d, cron entry ID = %d", task.EntryID, entries[0].ID)
}
if scheduler.isCurrentTask(firstGeneration) {
t.Fatal("replaced task generation must not remain executable")
}
if !scheduler.isCurrentTask(task) {
t.Fatal("tracked task generation must remain executable")
}
}