fix: 防止监控任务重复调度
This commit is contained in:
1 parent
78006a7120
commit
217da343a0
2 files changed
+85
-10
No files matched your search
@@ -122,19 +122,21 @@ func (s *MonitorScheduler) addTaskLocked(monitorID string, interval int) error {
|
||||
// 构建 cron 表达式: @every Ns
|
||||
spec := fmt.Sprintf("@every %ds", interval)
|
||||
|
||||
// 添加到 cron 调度器
|
||||
// 回调绑定本次注册的任务代际。即使异常并发或未来的新路径留下了
|
||||
// 未被 map 追踪的 cron entry,旧代际在执行前也会自行失效。
|
||||
task := &MonitorTask{ID: monitorID}
|
||||
entryID, err := s.cron.AddFunc(spec, func() {
|
||||
s.executeTask(monitorID)
|
||||
if s.isCurrentTask(task) {
|
||||
s.executeTask(monitorID)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加 cron 任务失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存任务信息
|
||||
s.tasks[monitorID] = &MonitorTask{
|
||||
ID: monitorID,
|
||||
EntryID: entryID,
|
||||
}
|
||||
task.EntryID = entryID
|
||||
s.tasks[monitorID] = task
|
||||
|
||||
s.logger.Info("添加监控任务",
|
||||
zap.String("taskID", monitorID),
|
||||
@@ -143,11 +145,21 @@ func (s *MonitorScheduler) addTaskLocked(monitorID string, interval int) error {
|
||||
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 {
|
||||
// 移除旧监控任务
|
||||
s.removeTaskLocked(monitorID)
|
||||
// 添加新任务
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// addTaskLocked 会在添加前移除已有 entry。
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user