github.com/matrixorigin/matrixone@v1.2.0/pkg/taskservice/task_service_cron.go (about) 1 // Copyright 2022 Matrix Origin 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package taskservice 16 17 import ( 18 "context" 19 "fmt" 20 "time" 21 22 "github.com/matrixorigin/matrixone/pkg/common/stopper" 23 "github.com/matrixorigin/matrixone/pkg/logutil" 24 "github.com/matrixorigin/matrixone/pkg/pb/task" 25 "github.com/robfig/cron/v3" 26 "go.uber.org/zap" 27 ) 28 29 var ( 30 fetchInterval = time.Second * 3 31 ) 32 33 type crons struct { 34 state cronServiceState 35 36 stopper *stopper.Stopper 37 cron *cron.Cron 38 jobs map[uint64]*cronJob 39 entries map[uint64]cron.EntryID 40 } 41 42 func (s *taskService) StartScheduleCronTask() { 43 if !s.crons.state.canStart() { 44 s.rt.Logger().Info("cron task scheduler started or is stopping") 45 return 46 } 47 48 s.crons.stopper = stopper.NewStopper("cronTasks") 49 s.crons.jobs = make(map[uint64]*cronJob) 50 s.crons.entries = make(map[uint64]cron.EntryID) 51 s.crons.cron = cron.New(cron.WithParser(s.cronParser), cron.WithLogger(logutil.GetCronLogger(false))) 52 s.crons.cron.Start() 53 if err := s.crons.stopper.RunTask(s.fetchCronTasks); err != nil { 54 panic(err) 55 } 56 } 57 58 func (s *taskService) StopScheduleCronTask() { 59 if !s.crons.state.canStop() { 60 s.rt.Logger().Info("cron task scheduler stopped or is stopping") 61 return 62 } 63 64 s.crons.stopper.Stop() 65 <-s.crons.cron.Stop().Done() 66 s.crons.cron = nil 67 s.crons.jobs = nil 68 s.crons.entries = nil 69 s.crons.stopper = nil 70 71 s.crons.state.endStop() 72 } 73 74 func (s *taskService) fetchCronTasks(ctx context.Context) { 75 s.rt.Logger().Info("start to fetch cron tasks") 76 defer func() { 77 s.rt.Logger().Info("stop to fetch cron tasks") 78 }() 79 80 ticker := time.NewTicker(fetchInterval) 81 defer ticker.Stop() 82 83 for { 84 select { 85 case <-ctx.Done(): 86 return 87 case <-ticker.C: 88 } 89 c, cancel := context.WithTimeout(ctx, time.Second*10) 90 tasks, err := s.QueryCronTask(c) 91 cancel() 92 if err != nil { 93 s.rt.Logger().Error("query cron tasks failed", 94 zap.Error(err)) 95 continue 96 } 97 98 s.rt.Logger().Debug("new cron tasks fetched", 99 zap.Int("current-count", len(s.crons.entries)), 100 zap.Int("fetch-count", len(tasks))) 101 102 currentTasks := make(map[uint64]struct{}, len(tasks)) 103 // add new cron tasks to cron scheduler 104 for _, v := range tasks { 105 currentTasks[v.ID] = struct{}{} 106 if job, ok := s.crons.jobs[v.ID]; !ok { 107 s.addCronTask(v) 108 } else { 109 if job.state.canUpdate() { 110 if job.task.TriggerTimes != v.TriggerTimes { 111 s.rt.Logger().Info("cron task updated", 112 zap.String("cause", "trigger-times changed"), 113 zap.Uint64("old-trigger-times", job.task.TriggerTimes), 114 zap.Uint64("new-trigger-times", v.TriggerTimes), 115 zap.String("task", v.DebugString())) 116 s.replaceCronTask(v) 117 } 118 job.state.endUpdate() 119 } 120 } 121 } 122 123 // remove deleted cron tasks 124 for id := range s.crons.entries { 125 if _, ok := currentTasks[id]; !ok { 126 s.removeCronTask(id) 127 } 128 } 129 } 130 } 131 132 func (s *taskService) addCronTask(task task.CronTask) { 133 job := newCronJob(task, s) 134 if time.Now().After(time.UnixMilli(task.NextTime)) { 135 s.rt.Logger().Info("cron task triggered", 136 zap.String("cause", "now > next"), 137 zap.String("task", task.DebugString())) 138 if err := s.crons.stopper.RunTask(func(ctx context.Context) { job.Run() }); err != nil { 139 panic(err) 140 } 141 } 142 143 entryId, err := s.crons.cron.AddJob(task.CronExpr, job) 144 if err != nil { 145 panic(err) 146 } 147 s.crons.jobs[task.ID] = job 148 s.crons.entries[task.ID] = entryId 149 } 150 151 func (s *taskService) removeCronTask(id uint64) { 152 s.crons.cron.Remove(s.crons.entries[id]) 153 delete(s.crons.jobs, id) 154 delete(s.crons.entries, id) 155 } 156 157 func (s *taskService) replaceCronTask(v task.CronTask) { 158 s.crons.cron.Remove(s.crons.entries[v.ID]) 159 160 job := newCronJob(v, s) 161 entryId, err := s.crons.cron.AddJob(v.CronExpr, job) 162 if err != nil { 163 panic(err) 164 } 165 s.crons.jobs[v.ID] = job 166 s.crons.entries[v.ID] = entryId 167 } 168 169 func newTaskFromMetadata(metadata task.TaskMetadata) task.AsyncTask { 170 return task.AsyncTask{ 171 Metadata: metadata, 172 Status: task.TaskStatus_Created, 173 CreateAt: time.Now().UnixMilli(), 174 } 175 } 176 177 type cronJob struct { 178 state cronJobState 179 s *taskService 180 schedule cron.Schedule 181 task task.CronTask 182 } 183 184 func newCronJob(task task.CronTask, s *taskService) *cronJob { 185 schedule, err := s.cronParser.Parse(task.CronExpr) 186 if err != nil { 187 panic(err) 188 } 189 return &cronJob{ 190 schedule: schedule, 191 task: task, 192 s: s, 193 } 194 } 195 196 func (j *cronJob) Run() { 197 if !j.state.canRun() { 198 return 199 } 200 defer func() { 201 j.state.endRun() 202 }() 203 204 if j.task.Metadata.Options.Concurrency != 0 && !j.checkConcurrency() { 205 return 206 } 207 208 j.s.rt.Logger().Info("cron task triggered", 209 zap.String("cause", "normal"), 210 zap.String("task", j.task.DebugString())) 211 j.doRun() 212 } 213 214 func (j *cronJob) doRun() { 215 now := time.Now() 216 cronTasks, err := j.s.QueryCronTask(context.Background(), WithCronTaskId(EQ, j.task.ID)) 217 if err != nil { 218 j.s.rt.Logger().Error("failed to query cron task", zap.Error(err)) 219 return 220 } 221 if len(cronTasks) != 1 { 222 j.s.rt.Logger().Panic(fmt.Sprintf("query cron_task_id = %d, return %d records", j.task.ID, len(cronTasks))) 223 return 224 } 225 cronTask := cronTasks[0] 226 cronTask.UpdateAt = now.UnixMilli() 227 cronTask.NextTime = j.schedule.Next(now).UnixMilli() 228 229 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 230 defer cancel() 231 232 cronTask.TriggerTimes++ 233 asyncTask := newTaskFromMetadata(cronTask.Metadata) 234 asyncTask.ParentTaskID = asyncTask.Metadata.ID 235 asyncTask.Metadata.ID = fmt.Sprintf("%s:%d", asyncTask.ParentTaskID, cronTask.TriggerTimes) 236 237 _, err = j.s.store.UpdateCronTask(ctx, cronTask, asyncTask) 238 if err != nil { 239 j.s.rt.Logger().Error("trigger cron task failed", 240 zap.String("cron-task", j.task.DebugString()), 241 zap.Error(err)) 242 return 243 } 244 j.task = cronTask 245 } 246 247 func (j *cronJob) checkConcurrency() bool { 248 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 249 defer cancel() 250 251 queryTask, err := j.s.QueryAsyncTask(ctx, 252 WithTaskStatusCond(task.TaskStatus_Running), 253 WithTaskExecutorCond(EQ, j.task.Metadata.Executor)) 254 if err != nil || 255 uint32(len(queryTask)) >= j.task.Metadata.Options.Concurrency { 256 j.s.rt.Logger().Debug("cron task not triggered", 257 zap.String("cause", "reach max concurrency"), 258 zap.String("task", j.task.DebugString()), 259 zap.Error(err)) 260 return false 261 } 262 return true 263 }