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