Refactor Cron and merge dashboard tasks (#10745)
* Refactor Cron and merge dashboard tasks * Merge Cron and Dashboard tasks * Make every cron task report a system notice on completion * Refactor the creation of these tasks * Ensure that execution counts of tasks is correct * Allow cron tasks to be started from the cron page * golangci-lint fixes * Enforce that only one task with the same name can be registered Signed-off-by: Andrew Thornton <art27@cantab.net> * fix name check Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @guillep2k * as per @lafriks Signed-off-by: Andrew Thornton <art27@cantab.net> * Add git.CommandContext variants Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>tokarchuk/v1.17
parent
c18144086f
commit
9a2e47b23a
@ -0,0 +1,72 @@ |
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cron |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"github.com/unknwon/i18n" |
||||
) |
||||
|
||||
// Config represents a basic configuration interface that cron task
|
||||
type Config interface { |
||||
IsEnabled() bool |
||||
DoRunAtStart() bool |
||||
GetSchedule() string |
||||
FormatMessage(name, status string, doer *models.User, args ...interface{}) string |
||||
} |
||||
|
||||
// BaseConfig represents the basic config for a Cron task
|
||||
type BaseConfig struct { |
||||
Enabled bool |
||||
RunAtStart bool |
||||
Schedule string |
||||
} |
||||
|
||||
// OlderThanConfig represents a cron task with OlderThan setting
|
||||
type OlderThanConfig struct { |
||||
BaseConfig |
||||
OlderThan time.Duration |
||||
} |
||||
|
||||
// UpdateExistingConfig represents a cron task with UpdateExisting setting
|
||||
type UpdateExistingConfig struct { |
||||
BaseConfig |
||||
UpdateExisting bool |
||||
} |
||||
|
||||
// GetSchedule returns the schedule for the base config
|
||||
func (b *BaseConfig) GetSchedule() string { |
||||
return b.Schedule |
||||
} |
||||
|
||||
// IsEnabled returns the enabled status for the config
|
||||
func (b *BaseConfig) IsEnabled() bool { |
||||
return b.Enabled |
||||
} |
||||
|
||||
// DoRunAtStart returns whether the task should be run at the start
|
||||
func (b *BaseConfig) DoRunAtStart() bool { |
||||
return b.RunAtStart |
||||
} |
||||
|
||||
// FormatMessage returns a message for the task
|
||||
func (b *BaseConfig) FormatMessage(name, status string, doer *models.User, args ...interface{}) string { |
||||
realArgs := make([]interface{}, 0, len(args)+2) |
||||
realArgs = append(realArgs, i18n.Tr("en-US", "admin.dashboard."+name)) |
||||
if doer == nil { |
||||
realArgs = append(realArgs, "(Cron)") |
||||
} else { |
||||
realArgs = append(realArgs, doer.Name) |
||||
} |
||||
if len(args) > 0 { |
||||
realArgs = append(realArgs, args...) |
||||
} |
||||
if doer == nil || (doer.ID == -1 && doer.Name == "(Cron)") { |
||||
return i18n.Tr("en-US", "admin.dashboard.cron."+status, realArgs...) |
||||
} |
||||
return i18n.Tr("en-US", "admin.dashboard.task."+status, realArgs...) |
||||
} |
@ -0,0 +1,166 @@ |
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cron |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"reflect" |
||||
"sync" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/modules/graceful" |
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/process" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
) |
||||
|
||||
var lock = sync.Mutex{} |
||||
var started = false |
||||
var tasks = []*Task{} |
||||
var tasksMap = map[string]*Task{} |
||||
|
||||
// Task represents a Cron task
|
||||
type Task struct { |
||||
lock sync.Mutex |
||||
Name string |
||||
config Config |
||||
fun func(context.Context, *models.User, Config) error |
||||
ExecTimes int64 |
||||
} |
||||
|
||||
// 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(&models.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 *models.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) { |
||||
ctx, cancel := context.WithCancel(baseCtx) |
||||
defer cancel() |
||||
pm := process.GetManager() |
||||
pid := pm.Add(config.FormatMessage(t.Name, "process", doer), cancel) |
||||
defer pm.Remove(pid) |
||||
if err := t.fun(ctx, doer, config); err != nil { |
||||
if models.IsErrCancelled(err) { |
||||
message := err.(models.ErrCancelled).Message |
||||
if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "aborted", doer, message)); err != nil { |
||||
log.Error("CreateNotice: %v", err) |
||||
} |
||||
return |
||||
} |
||||
if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "error", doer, err)); err != nil { |
||||
log.Error("CreateNotice: %v", err) |
||||
} |
||||
return |
||||
} |
||||
if err := models.CreateNotice(models.NoticeTask, config.FormatMessage(t.Name, "finished", doer)); 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, *models.User, Config) error) error { |
||||
log.Debug("Registering task: %s", name) |
||||
_, 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 = c.AddJob(name, config.GetSchedule(), task); err != nil { |
||||
log.Error("Unable to register cron task with name: %s Error: %v", name, err) |
||||
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, *models.User, Config) error) { |
||||
if err := RegisterTask(name, config, fun); err != nil { |
||||
log.Fatal("Unable to register cron task %s Error: %v", name, err) |
||||
} |
||||
} |
@ -0,0 +1,118 @@ |
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cron |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
"code.gitea.io/gitea/modules/migrations" |
||||
repository_service "code.gitea.io/gitea/modules/repository" |
||||
mirror_service "code.gitea.io/gitea/services/mirror" |
||||
) |
||||
|
||||
func registerUpdateMirrorTask() { |
||||
RegisterTaskFatal("update_mirrors", &BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: false, |
||||
Schedule: "@every 10m", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return mirror_service.Update(ctx) |
||||
}) |
||||
} |
||||
|
||||
func registerRepoHealthCheck() { |
||||
type RepoHealthCheckConfig struct { |
||||
BaseConfig |
||||
Timeout time.Duration |
||||
Args []string `delim:" "` |
||||
} |
||||
RegisterTaskFatal("repo_health_check", &RepoHealthCheckConfig{ |
||||
BaseConfig: BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: false, |
||||
Schedule: "@every 24h", |
||||
}, |
||||
Timeout: 60 * time.Second, |
||||
Args: []string{}, |
||||
}, func(ctx context.Context, _ *models.User, config Config) error { |
||||
rhcConfig := config.(*RepoHealthCheckConfig) |
||||
return repository_service.GitFsck(ctx, rhcConfig.Timeout, rhcConfig.Args) |
||||
}) |
||||
} |
||||
|
||||
func registerCheckRepoStats() { |
||||
RegisterTaskFatal("check_repo_stats", &BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: true, |
||||
Schedule: "@every 24h", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return models.CheckRepoStats(ctx) |
||||
}) |
||||
} |
||||
|
||||
func registerArchiveCleanup() { |
||||
RegisterTaskFatal("archive_cleanup", &OlderThanConfig{ |
||||
BaseConfig: BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: true, |
||||
Schedule: "@every 24h", |
||||
}, |
||||
OlderThan: 24 * time.Hour, |
||||
}, func(ctx context.Context, _ *models.User, config Config) error { |
||||
acConfig := config.(*OlderThanConfig) |
||||
return models.DeleteOldRepositoryArchives(ctx, acConfig.OlderThan) |
||||
}) |
||||
} |
||||
|
||||
func registerSyncExternalUsers() { |
||||
RegisterTaskFatal("sync_external_users", &UpdateExistingConfig{ |
||||
BaseConfig: BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: false, |
||||
Schedule: "@every 24h", |
||||
}, |
||||
UpdateExisting: true, |
||||
}, func(ctx context.Context, _ *models.User, config Config) error { |
||||
realConfig := config.(*UpdateExistingConfig) |
||||
return models.SyncExternalUsers(ctx, realConfig.UpdateExisting) |
||||
}) |
||||
} |
||||
|
||||
func registerDeletedBranchesCleanup() { |
||||
RegisterTaskFatal("deleted_branches_cleanup", &OlderThanConfig{ |
||||
BaseConfig: BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: true, |
||||
Schedule: "@every 24h", |
||||
}, |
||||
OlderThan: 24 * time.Hour, |
||||
}, func(ctx context.Context, _ *models.User, config Config) error { |
||||
realConfig := config.(*OlderThanConfig) |
||||
models.RemoveOldDeletedBranches(ctx, realConfig.OlderThan) |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
func registerUpdateMigrationPosterID() { |
||||
RegisterTaskFatal("update_migration_poster_id", &BaseConfig{ |
||||
Enabled: true, |
||||
RunAtStart: true, |
||||
Schedule: "@every 24h", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return migrations.UpdateMigrationPosterID(ctx) |
||||
}) |
||||
} |
||||
|
||||
func initBasicTasks() { |
||||
registerUpdateMirrorTask() |
||||
registerRepoHealthCheck() |
||||
registerCheckRepoStats() |
||||
registerArchiveCleanup() |
||||
registerSyncExternalUsers() |
||||
registerDeletedBranchesCleanup() |
||||
registerUpdateMigrationPosterID() |
||||
} |
@ -0,0 +1,119 @@ |
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cron |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"code.gitea.io/gitea/models" |
||||
repo_module "code.gitea.io/gitea/modules/repository" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
) |
||||
|
||||
func registerDeleteInactiveUsers() { |
||||
RegisterTaskFatal("delete_inactive_accounts", &OlderThanConfig{ |
||||
BaseConfig: BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@annually", |
||||
}, |
||||
OlderThan: 0 * time.Second, |
||||
}, func(ctx context.Context, _ *models.User, config Config) error { |
||||
olderThanConfig := config.(*OlderThanConfig) |
||||
return models.DeleteInactiveUsers(ctx, olderThanConfig.OlderThan) |
||||
}) |
||||
} |
||||
|
||||
func registerDeleteRepositoryArchives() { |
||||
RegisterTaskFatal("delete_repo_archives", &BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@annually", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return models.DeleteRepositoryArchives(ctx) |
||||
}) |
||||
} |
||||
|
||||
func registerGarbageCollectRepositories() { |
||||
type RepoHealthCheckConfig struct { |
||||
BaseConfig |
||||
Timeout time.Duration |
||||
Args []string `delim:" "` |
||||
} |
||||
RegisterTaskFatal("git_gc_repos", &RepoHealthCheckConfig{ |
||||
BaseConfig: BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@every 72h", |
||||
}, |
||||
Timeout: time.Duration(setting.Git.Timeout.GC) * time.Second, |
||||
Args: setting.Git.GCArgs, |
||||
}, func(ctx context.Context, _ *models.User, config Config) error { |
||||
rhcConfig := config.(*RepoHealthCheckConfig) |
||||
return repo_module.GitGcRepos(ctx, rhcConfig.Timeout, rhcConfig.Args...) |
||||
}) |
||||
} |
||||
|
||||
func registerRewriteAllPublicKeys() { |
||||
RegisterTaskFatal("resync_all_sshkeys", &BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@every 72h", |
||||
}, func(_ context.Context, _ *models.User, _ Config) error { |
||||
return models.RewriteAllPublicKeys() |
||||
}) |
||||
} |
||||
|
||||
func registerRepositoryUpdateHook() { |
||||
RegisterTaskFatal("resync_all_hooks", &BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@every 72h", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return repo_module.SyncRepositoryHooks(ctx) |
||||
}) |
||||
} |
||||
|
||||
func registerReinitMissingRepositories() { |
||||
RegisterTaskFatal("reinit_missing_repos", &BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@every 72h", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return repo_module.ReinitMissingRepositories(ctx) |
||||
}) |
||||
} |
||||
|
||||
func registerDeleteMissingRepositories() { |
||||
RegisterTaskFatal("delete_missing_repos", &BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@every 72h", |
||||
}, func(ctx context.Context, user *models.User, _ Config) error { |
||||
return repo_module.DeleteMissingRepositories(ctx, user) |
||||
}) |
||||
} |
||||
|
||||
func registerRemoveRandomAvatars() { |
||||
RegisterTaskFatal("delete_generated_repository_avatars", &BaseConfig{ |
||||
Enabled: false, |
||||
RunAtStart: false, |
||||
Schedule: "@every 72h", |
||||
}, func(ctx context.Context, _ *models.User, _ Config) error { |
||||
return models.RemoveRandomAvatars(ctx) |
||||
}) |
||||
} |
||||
|
||||
func initExtendedTasks() { |
||||
registerDeleteInactiveUsers() |
||||
registerDeleteRepositoryArchives() |
||||
registerGarbageCollectRepositories() |
||||
registerRewriteAllPublicKeys() |
||||
registerRepositoryUpdateHook() |
||||
registerReinitMissingRepositories() |
||||
registerDeleteMissingRepositories() |
||||
registerRemoveRandomAvatars() |
||||
} |
Loading…
Reference in new issue