Browse Source
Refactor Cron and merge dashboard tasks (#10745)
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>mj
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 850 additions and 452 deletions
-
4integrations/auth_ldap_test.go
-
15models/admin.go
-
5models/branches.go
-
22models/error.go
-
65models/repo.go
-
51models/user.go
-
2modules/auth/admin.go
-
177modules/cron/cron.go
-
72modules/cron/setting.go
-
166modules/cron/tasks.go
-
118modules/cron/tasks_basic.go
-
119modules/cron/tasks_extended.go
-
14modules/git/command.go
-
4modules/git/git.go
-
10modules/migrations/update.go
-
102modules/repository/check.go
-
9modules/repository/hooks.go
-
129modules/setting/cron.go
-
1modules/setting/setting.go
-
36options/locale/locale_en-US.ini
-
65routers/admin/admin.go
-
35routers/init.go
-
9services/mirror/mirror.go
-
27templates/admin/dashboard.tmpl
-
45templates/admin/monitor.tmpl
@ -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() |
|||
} |