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 }