sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/cron/cron.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package cron provides a wrapper of robfig/cron, which manages schedule cron jobs for horologium
    18  package cron
    19  
    20  import (
    21  	"fmt"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/sirupsen/logrus"
    26  	cron "gopkg.in/robfig/cron.v2" // using v2 api, doc at https://godoc.org/gopkg.in/robfig/cron.v2
    27  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  
    30  	"sigs.k8s.io/prow/pkg/config"
    31  )
    32  
    33  // jobStatus is a cache layer for tracking existing cron jobs
    34  type jobStatus struct {
    35  	// entryID is a unique-identifier for each cron entry generated from cronAgent
    36  	entryID cron.EntryID
    37  	// triggered marks if a job has been triggered for the next cron.QueuedJobs() call
    38  	triggered bool
    39  	// cronStr is a cache for job's cron status
    40  	// cron entry will be regenerated if cron string changes from the periodic job
    41  	cronStr string
    42  }
    43  
    44  // Cron is a wrapper for cron.Cron
    45  type Cron struct {
    46  	cronAgent *cron.Cron
    47  	jobs      map[string]*jobStatus
    48  	logger    *logrus.Entry
    49  	lock      sync.Mutex
    50  }
    51  
    52  // New makes a new Cron object
    53  func New() *Cron {
    54  	return &Cron{
    55  		cronAgent: cron.New(),
    56  		jobs:      map[string]*jobStatus{},
    57  		logger:    logrus.WithField("client", "cron"),
    58  	}
    59  }
    60  
    61  // Start kicks off current cronAgent scheduler
    62  func (c *Cron) Start() {
    63  	c.cronAgent.Start()
    64  }
    65  
    66  // Stop pauses current cronAgent scheduler
    67  func (c *Cron) Stop() {
    68  	c.cronAgent.Stop()
    69  }
    70  
    71  // QueuedJobs returns a list of jobs that need to be triggered
    72  // and reset trigger in jobStatus
    73  func (c *Cron) QueuedJobs() []string {
    74  	c.lock.Lock()
    75  	defer c.lock.Unlock()
    76  
    77  	res := []string{}
    78  	for k, v := range c.jobs {
    79  		if v.triggered {
    80  			res = append(res, k)
    81  		}
    82  		c.jobs[k].triggered = false
    83  	}
    84  	return res
    85  }
    86  
    87  // SyncConfig syncs current cronAgent with current prow config
    88  // which add/delete jobs accordingly.
    89  func (c *Cron) SyncConfig(cfg *config.Config) error {
    90  	c.lock.Lock()
    91  	defer c.lock.Unlock()
    92  
    93  	for _, p := range cfg.Periodics {
    94  		if err := c.addPeriodic(p); err != nil {
    95  			return err
    96  		}
    97  	}
    98  
    99  	periodicNames := sets.New[string]()
   100  	for _, p := range cfg.AllPeriodics() {
   101  		periodicNames.Insert(p.Name)
   102  	}
   103  
   104  	existing := sets.New[string]()
   105  	for k := range c.jobs {
   106  		existing.Insert(k)
   107  	}
   108  
   109  	var removalErrors []error
   110  	for _, job := range sets.List(existing.Difference(periodicNames)) {
   111  		if err := c.removeJob(job); err != nil {
   112  			removalErrors = append(removalErrors, err)
   113  		}
   114  	}
   115  
   116  	return utilerrors.NewAggregate(removalErrors)
   117  }
   118  
   119  // HasJob returns if a job has been scheduled in cronAgent or not
   120  func (c *Cron) HasJob(name string) bool {
   121  	c.lock.Lock()
   122  	defer c.lock.Unlock()
   123  
   124  	_, ok := c.jobs[name]
   125  	return ok
   126  }
   127  
   128  func (c *Cron) addPeriodic(p config.Periodic) error {
   129  	if p.Cron == "" {
   130  		return nil
   131  	}
   132  
   133  	if job, ok := c.jobs[p.Name]; ok {
   134  		if job.cronStr == p.Cron {
   135  			return nil
   136  		}
   137  		// job updated, remove old entry
   138  		if err := c.removeJob(p.Name); err != nil {
   139  			return err
   140  		}
   141  	}
   142  
   143  	if err := c.addJob(p.Name, p.Cron); err != nil {
   144  		return err
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  // addJob adds a cron entry for a job to cronAgent
   151  func (c *Cron) addJob(name, cron string) error {
   152  	id, err := c.cronAgent.AddFunc("TZ=UTC "+cron, func() {
   153  		c.lock.Lock()
   154  		defer c.lock.Unlock()
   155  
   156  		c.jobs[name].triggered = true
   157  		c.logger.Infof("Triggering cron job %s.", name)
   158  	})
   159  
   160  	if err != nil {
   161  		return fmt.Errorf("cronAgent fails to add job %s with cron %s: %w", name, cron, err)
   162  	}
   163  
   164  	c.jobs[name] = &jobStatus{
   165  		entryID: id,
   166  		cronStr: cron,
   167  		// try to kick of a periodic trigger right away
   168  		triggered: strings.HasPrefix(cron, "@every"),
   169  	}
   170  
   171  	c.logger.Infof("Added new cron job %s with trigger %s.", name, cron)
   172  	return nil
   173  }
   174  
   175  // removeJob removes the job from cronAgent
   176  func (c *Cron) removeJob(name string) error {
   177  	job, ok := c.jobs[name]
   178  	if !ok {
   179  		return fmt.Errorf("job %s has not been added to cronAgent yet", name)
   180  	}
   181  	c.cronAgent.Remove(job.entryID)
   182  	delete(c.jobs, name)
   183  	c.logger.Infof("Removed previous cron job %s.", name)
   184  	return nil
   185  }