code.gitea.io/gitea@v1.21.7/services/cron/tasks.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package cron 5 6 import ( 7 "context" 8 "fmt" 9 "reflect" 10 "strings" 11 "sync" 12 "time" 13 14 "code.gitea.io/gitea/models/db" 15 system_model "code.gitea.io/gitea/models/system" 16 user_model "code.gitea.io/gitea/models/user" 17 "code.gitea.io/gitea/modules/graceful" 18 "code.gitea.io/gitea/modules/log" 19 "code.gitea.io/gitea/modules/process" 20 "code.gitea.io/gitea/modules/setting" 21 "code.gitea.io/gitea/modules/translation" 22 ) 23 24 var ( 25 lock = sync.Mutex{} 26 started = false 27 tasks = []*Task{} 28 tasksMap = map[string]*Task{} 29 ) 30 31 // Task represents a Cron task 32 type Task struct { 33 lock sync.Mutex 34 Name string 35 config Config 36 fun func(context.Context, *user_model.User, Config) error 37 Status string 38 LastMessage string 39 LastDoer string 40 ExecTimes int64 41 // This stores the time of the last manual run of this task. 42 LastRun time.Time 43 } 44 45 // DoRunAtStart returns if this task should run at the start 46 func (t *Task) DoRunAtStart() bool { 47 return t.config.DoRunAtStart() 48 } 49 50 // IsEnabled returns if this task is enabled as cron task 51 func (t *Task) IsEnabled() bool { 52 return t.config.IsEnabled() 53 } 54 55 // GetConfig will return a copy of the task's config 56 func (t *Task) GetConfig() Config { 57 if reflect.TypeOf(t.config).Kind() == reflect.Ptr { 58 // Pointer: 59 return reflect.New(reflect.ValueOf(t.config).Elem().Type()).Interface().(Config) 60 } 61 // Not pointer: 62 return reflect.New(reflect.TypeOf(t.config)).Elem().Interface().(Config) 63 } 64 65 // Run will run the task incrementing the cron counter with no user defined 66 func (t *Task) Run() { 67 t.RunWithUser(&user_model.User{ 68 ID: -1, 69 Name: "(Cron)", 70 LowerName: "(cron)", 71 }, t.config) 72 } 73 74 // RunWithUser will run the task incrementing the cron counter at the time with User 75 func (t *Task) RunWithUser(doer *user_model.User, config Config) { 76 if !taskStatusTable.StartIfNotRunning(t.Name) { 77 return 78 } 79 t.lock.Lock() 80 if config == nil { 81 config = t.config 82 } 83 t.ExecTimes++ 84 t.lock.Unlock() 85 defer func() { 86 taskStatusTable.Stop(t.Name) 87 }() 88 graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) { 89 defer func() { 90 if err := recover(); err != nil { 91 // Recover a panic within the execution of the task. 92 combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2)) 93 log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr) 94 } 95 }() 96 // Store the time of this run, before the function is executed, so it 97 // matches the behavior of what the cron library does. 98 t.lock.Lock() 99 t.LastRun = time.Now() 100 t.lock.Unlock() 101 102 pm := process.GetManager() 103 doerName := "" 104 if doer != nil && doer.ID != -1 { 105 doerName = doer.Name 106 } 107 108 ctx, _, finished := pm.AddContext(baseCtx, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "process", doerName)) 109 defer finished() 110 111 if err := t.fun(ctx, doer, config); err != nil { 112 var message string 113 var status string 114 if db.IsErrCancelled(err) { 115 status = "cancelled" 116 message = err.(db.ErrCancelled).Message 117 } else { 118 status = "error" 119 message = err.Error() 120 } 121 122 t.lock.Lock() 123 t.LastMessage = message 124 t.Status = status 125 t.LastDoer = doerName 126 t.lock.Unlock() 127 128 if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "cancelled", doerName, message)); err != nil { 129 log.Error("CreateNotice: %v", err) 130 } 131 return 132 } 133 134 t.lock.Lock() 135 t.Status = "finished" 136 t.LastMessage = "" 137 t.LastDoer = doerName 138 t.lock.Unlock() 139 140 if config.DoNoticeOnSuccess() { 141 if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "finished", doerName)); err != nil { 142 log.Error("CreateNotice: %v", err) 143 } 144 } 145 }) 146 } 147 148 // GetTask gets the named task 149 func GetTask(name string) *Task { 150 lock.Lock() 151 defer lock.Unlock() 152 log.Info("Getting %s in %v", name, tasksMap[name]) 153 154 return tasksMap[name] 155 } 156 157 // RegisterTask allows a task to be registered with the cron service 158 func RegisterTask(name string, config Config, fun func(context.Context, *user_model.User, Config) error) error { 159 log.Debug("Registering task: %s", name) 160 161 i18nKey := "admin.dashboard." + name 162 if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey { 163 return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey) 164 } 165 166 _, err := setting.GetCronSettings(name, config) 167 if err != nil { 168 log.Error("Unable to register cron task with name: %s Error: %v", name, err) 169 return err 170 } 171 172 task := &Task{ 173 Name: name, 174 config: config, 175 fun: fun, 176 } 177 lock.Lock() 178 locked := true 179 defer func() { 180 if locked { 181 lock.Unlock() 182 } 183 }() 184 if _, has := tasksMap[task.Name]; has { 185 log.Error("A task with this name: %s has already been registered", name) 186 return fmt.Errorf("duplicate task with name: %s", task.Name) 187 } 188 189 if config.IsEnabled() { 190 // We cannot use the entry return as there is no way to lock it 191 if err := addTaskToScheduler(task); err != nil { 192 return err 193 } 194 } 195 196 tasks = append(tasks, task) 197 tasksMap[task.Name] = task 198 if started && config.IsEnabled() && config.DoRunAtStart() { 199 lock.Unlock() 200 locked = false 201 task.Run() 202 } 203 204 return nil 205 } 206 207 // RegisterTaskFatal will register a task but if there is an error log.Fatal 208 func RegisterTaskFatal(name string, config Config, fun func(context.Context, *user_model.User, Config) error) { 209 if err := RegisterTask(name, config, fun); err != nil { 210 log.Fatal("Unable to register cron task %s Error: %v", name, err) 211 } 212 } 213 214 func addTaskToScheduler(task *Task) error { 215 tags := []string{task.Name, task.config.GetSchedule()} // name and schedule can't be get from job, so we add them as tag 216 if scheduleHasSeconds(task.config.GetSchedule()) { 217 scheduler = scheduler.CronWithSeconds(task.config.GetSchedule()) 218 } else { 219 scheduler = scheduler.Cron(task.config.GetSchedule()) 220 } 221 if _, err := scheduler.Tag(tags...).Do(task.Run); err != nil { 222 log.Error("Unable to register cron task with name: %s Error: %v", task.Name, err) 223 return err 224 } 225 return nil 226 } 227 228 func scheduleHasSeconds(schedule string) bool { 229 return len(strings.Fields(schedule)) >= 6 230 }