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  }