forgejo/services/cron/tasks.go

229 lines
6.1 KiB
Go
Raw Normal View History

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cron
import (
"context"
"fmt"
"reflect"
"strings"
"sync"
[GITEA] Show manual cron run's last time - Currently in the cron tasks, the 'Previous Time' only displays the previous time of when the cron library executes the function, but not any of the manual executions of the task. - Store the last run's time in memory in the Task struct and use that, when that time is later than time that the cron library has executed this task. - This ensures that if an instance admin manually starts a task, there's feedback that this task is/has been run, because the task might be run that quick, that the status icon already has been changed to an checkmark, - Tasks that are executed at startup now reflect this as well, as the time of the execution of that task on startup is now being shown as 'Previous Time'. - Added integration tests for the API part, which is easier to test because querying the HTML table of cron tasks is non-trivial. - Resolves https://codeberg.org/forgejo/forgejo/issues/949 (cherry picked from commit 0475e2048e7641f6ca223d486ffb8e6cecddef87) (cherry picked from commit dcc952f0db883204a1585f3fec0abcacdcab4649) (cherry picked from commit 7168a240e8b5dcba5d6bd6d1395e79eea1e6c5f5) (cherry picked from commit 4bc4cccb1b3836c43fd6f8056fcb3605e7c53bfb) (cherry picked from commit 3fe019ca3c9bbc66ff1ba644c2cb3e118f99948c) [GITEA] Show manual cron run's last time (squash) 26 jobs in cron fixtures (cherry picked from commit 8473030628302f78a954b14d02b423cc180b2751) (cherry picked from commit 871c7297423efe5f2ed33a0dd52070d826f078c8) (cherry picked from commit daefb27d2caaf27ebb8c8142634aec9151515515) (cherry picked from commit 2f66c1e4ce5f3c6c5555de35a68f7cc9a986b62f) (cherry picked from commit cdaa9615f4f4b6563c383e691aaa57b3df308cf0) (cherry picked from commit 9d1701442fb8be4749aeefa0cbabef8d5e3fa0c0) (cherry picked from commit fd34fdac1408ece6b7d9fe6a76501ed9a45d06fa)
2023-07-21 15:50:14 +02:00
"time"
"code.gitea.io/gitea/models/db"
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
)
var (
lock = sync.Mutex{}
started = false
tasks = []*Task{}
tasksMap = map[string]*Task{}
)
// Task represents a Cron task
type Task struct {
lock sync.Mutex
Name string
config Config
fun func(context.Context, *user_model.User, Config) error
Status string
LastMessage string
LastDoer string
ExecTimes int64
[GITEA] Show manual cron run's last time - Currently in the cron tasks, the 'Previous Time' only displays the previous time of when the cron library executes the function, but not any of the manual executions of the task. - Store the last run's time in memory in the Task struct and use that, when that time is later than time that the cron library has executed this task. - This ensures that if an instance admin manually starts a task, there's feedback that this task is/has been run, because the task might be run that quick, that the status icon already has been changed to an checkmark, - Tasks that are executed at startup now reflect this as well, as the time of the execution of that task on startup is now being shown as 'Previous Time'. - Added integration tests for the API part, which is easier to test because querying the HTML table of cron tasks is non-trivial. - Resolves https://codeberg.org/forgejo/forgejo/issues/949 (cherry picked from commit 0475e2048e7641f6ca223d486ffb8e6cecddef87) (cherry picked from commit dcc952f0db883204a1585f3fec0abcacdcab4649) (cherry picked from commit 7168a240e8b5dcba5d6bd6d1395e79eea1e6c5f5) (cherry picked from commit 4bc4cccb1b3836c43fd6f8056fcb3605e7c53bfb) (cherry picked from commit 3fe019ca3c9bbc66ff1ba644c2cb3e118f99948c) [GITEA] Show manual cron run's last time (squash) 26 jobs in cron fixtures (cherry picked from commit 8473030628302f78a954b14d02b423cc180b2751) (cherry picked from commit 871c7297423efe5f2ed33a0dd52070d826f078c8) (cherry picked from commit daefb27d2caaf27ebb8c8142634aec9151515515) (cherry picked from commit 2f66c1e4ce5f3c6c5555de35a68f7cc9a986b62f) (cherry picked from commit cdaa9615f4f4b6563c383e691aaa57b3df308cf0) (cherry picked from commit 9d1701442fb8be4749aeefa0cbabef8d5e3fa0c0) (cherry picked from commit fd34fdac1408ece6b7d9fe6a76501ed9a45d06fa)
2023-07-21 15:50:14 +02:00
// This stores the time of the last manual run of this task.
LastRun time.Time
}
// DoRunAtStart returns if this task should run at the start
func (t *Task) DoRunAtStart() bool {
return t.config.DoRunAtStart()
}
// IsEnabled returns if this task is enabled as cron task
func (t *Task) IsEnabled() bool {
return t.config.IsEnabled()
}
// GetConfig will return a copy of the task's config
func (t *Task) GetConfig() Config {
if reflect.TypeOf(t.config).Kind() == reflect.Ptr {
// Pointer:
return reflect.New(reflect.ValueOf(t.config).Elem().Type()).Interface().(Config)
}
// Not pointer:
return reflect.New(reflect.TypeOf(t.config)).Elem().Interface().(Config)
}
// Run will run the task incrementing the cron counter with no user defined
func (t *Task) Run() {
t.RunWithUser(&user_model.User{
ID: -1,
Name: "(Cron)",
LowerName: "(cron)",
}, t.config)
}
// RunWithUser will run the task incrementing the cron counter at the time with User
func (t *Task) RunWithUser(doer *user_model.User, config Config) {
if !taskStatusTable.StartIfNotRunning(t.Name) {
return
}
t.lock.Lock()
if config == nil {
config = t.config
}
t.ExecTimes++
t.lock.Unlock()
defer func() {
taskStatusTable.Stop(t.Name)
if err := recover(); err != nil {
// Recover a panic within the
combinedErr := fmt.Errorf("%s\n%s", err, log.Stack(2))
log.Error("PANIC whilst running task: %s Value: %v", t.Name, combinedErr)
}
}()
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
[GITEA] Show manual cron run's last time - Currently in the cron tasks, the 'Previous Time' only displays the previous time of when the cron library executes the function, but not any of the manual executions of the task. - Store the last run's time in memory in the Task struct and use that, when that time is later than time that the cron library has executed this task. - This ensures that if an instance admin manually starts a task, there's feedback that this task is/has been run, because the task might be run that quick, that the status icon already has been changed to an checkmark, - Tasks that are executed at startup now reflect this as well, as the time of the execution of that task on startup is now being shown as 'Previous Time'. - Added integration tests for the API part, which is easier to test because querying the HTML table of cron tasks is non-trivial. - Resolves https://codeberg.org/forgejo/forgejo/issues/949 (cherry picked from commit 0475e2048e7641f6ca223d486ffb8e6cecddef87) (cherry picked from commit dcc952f0db883204a1585f3fec0abcacdcab4649) (cherry picked from commit 7168a240e8b5dcba5d6bd6d1395e79eea1e6c5f5) (cherry picked from commit 4bc4cccb1b3836c43fd6f8056fcb3605e7c53bfb) (cherry picked from commit 3fe019ca3c9bbc66ff1ba644c2cb3e118f99948c) [GITEA] Show manual cron run's last time (squash) 26 jobs in cron fixtures (cherry picked from commit 8473030628302f78a954b14d02b423cc180b2751) (cherry picked from commit 871c7297423efe5f2ed33a0dd52070d826f078c8) (cherry picked from commit daefb27d2caaf27ebb8c8142634aec9151515515) (cherry picked from commit 2f66c1e4ce5f3c6c5555de35a68f7cc9a986b62f) (cherry picked from commit cdaa9615f4f4b6563c383e691aaa57b3df308cf0) (cherry picked from commit 9d1701442fb8be4749aeefa0cbabef8d5e3fa0c0) (cherry picked from commit fd34fdac1408ece6b7d9fe6a76501ed9a45d06fa)
2023-07-21 15:50:14 +02:00
// Store the time of this run, before the function is executed, so it
// matches the behavior of what the cron library does.
t.lock.Lock()
t.LastRun = time.Now()
t.lock.Unlock()
pm := process.GetManager()
doerName := ""
if doer != nil && doer.ID != -1 {
doerName = doer.Name
}
ctx, _, finished := pm.AddContext(baseCtx, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "process", doerName))
defer finished()
if err := t.fun(ctx, doer, config); err != nil {
var message string
var status string
if db.IsErrCancelled(err) {
status = "cancelled"
message = err.(db.ErrCancelled).Message
} else {
status = "error"
message = err.Error()
}
t.lock.Lock()
t.LastMessage = message
t.Status = status
t.LastDoer = doerName
t.lock.Unlock()
if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "cancelled", doerName, message)); err != nil {
log.Error("CreateNotice: %v", err)
}
return
}
t.lock.Lock()
t.Status = "finished"
t.LastMessage = ""
t.LastDoer = doerName
t.lock.Unlock()
if config.DoNoticeOnSuccess() {
if err := system_model.CreateNotice(ctx, system_model.NoticeTask, config.FormatMessage(translation.NewLocale("en-US"), t.Name, "finished", doerName)); err != nil {
log.Error("CreateNotice: %v", err)
}
}
})
}
// GetTask gets the named task
func GetTask(name string) *Task {
lock.Lock()
defer lock.Unlock()
log.Info("Getting %s in %v", name, tasksMap[name])
return tasksMap[name]
}
// RegisterTask allows a task to be registered with the cron service
func RegisterTask(name string, config Config, fun func(context.Context, *user_model.User, Config) error) error {
log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name
if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey {
return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
}
_, err := setting.GetCronSettings(name, config)
if err != nil {
log.Error("Unable to register cron task with name: %s Error: %v", name, err)
return err
}
task := &Task{
Name: name,
config: config,
fun: fun,
}
lock.Lock()
locked := true
defer func() {
if locked {
lock.Unlock()
}
}()
if _, has := tasksMap[task.Name]; has {
log.Error("A task with this name: %s has already been registered", name)
return fmt.Errorf("duplicate task with name: %s", task.Name)
}
if config.IsEnabled() {
// We cannot use the entry return as there is no way to lock it
if err := addTaskToScheduler(task); err != nil {
return err
}
}
tasks = append(tasks, task)
tasksMap[task.Name] = task
if started && config.IsEnabled() && config.DoRunAtStart() {
lock.Unlock()
locked = false
task.Run()
}
return nil
}
// RegisterTaskFatal will register a task but if there is an error log.Fatal
func RegisterTaskFatal(name string, config Config, fun func(context.Context, *user_model.User, Config) error) {
if err := RegisterTask(name, config, fun); err != nil {
log.Fatal("Unable to register cron task %s Error: %v", name, err)
}
}
func addTaskToScheduler(task *Task) error {
tags := []string{task.Name, task.config.GetSchedule()} // name and schedule can't be get from job, so we add them as tag
if scheduleHasSeconds(task.config.GetSchedule()) {
scheduler = scheduler.CronWithSeconds(task.config.GetSchedule())
} else {
scheduler = scheduler.Cron(task.config.GetSchedule())
}
if _, err := scheduler.Tag(tags...).Do(task.Run); err != nil {
log.Error("Unable to register cron task with name: %s Error: %v", task.Name, err)
return err
}
return nil
}
func scheduleHasSeconds(schedule string) bool {
return len(strings.Fields(schedule)) >= 6
}