github.com/isyscore/isc-gobase@v1.5.3-0.20231218061332-cbc7451899e9/cron/cron.go (about) 1 package cron 2 3 import ( 4 "log" 5 "runtime" 6 "sort" 7 "time" 8 ) 9 10 // Cron keeps track of any number of entries, invoking the associated func as 11 // specified by the schedule. It may be started, stopped, and the entries may 12 // be inspected while running. 13 type Cron struct { 14 entries []*Entry 15 stop chan struct{} 16 add chan *Entry 17 snapshot chan []*Entry 18 running bool 19 ErrorLog *log.Logger 20 location *time.Location 21 } 22 23 // Job is an interface for submitted cron jobs. 24 type Job interface { 25 Run() 26 } 27 28 // The Schedule describes a job's duty cycle. 29 type Schedule interface { 30 // Next Return the next activation time, later than the given time. 31 // Next is invoked initially, and then each time the job is run. 32 Next(time.Time) time.Time 33 } 34 35 // Entry consists of a schedule and the func to execute on that schedule. 36 type Entry struct { 37 // The schedule on which this job should be run. 38 Schedule Schedule 39 40 // The next time the job will run. This is the zero time if Cron has not been 41 // started or this entry's schedule is unsatisfiable 42 Next time.Time 43 44 // The last time this job was run. This is the zero time if the job has never 45 // been run. 46 Prev time.Time 47 48 // The Job to run. 49 Job Job 50 } 51 52 // byTime is a wrapper for sorting the entry array by time 53 // (with zero time at the end). 54 type byTime []*Entry 55 56 func (s byTime) Len() int { return len(s) } 57 func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 58 func (s byTime) Less(i, j int) bool { 59 // Two zero times should return false. 60 // Otherwise, zero is "greater" than any other time. 61 // (To sort it at the end of the list.) 62 if s[i].Next.IsZero() { 63 return false 64 } 65 if s[j].Next.IsZero() { 66 return true 67 } 68 return s[i].Next.Before(s[j].Next) 69 } 70 71 // New returns a new Cron job runner, in the Local time zone. 72 func New() *Cron { 73 return NewWithLocation(time.Now().Location()) 74 } 75 76 // NewWithLocation returns a new Cron job runner. 77 func NewWithLocation(location *time.Location) *Cron { 78 return &Cron{ 79 entries: nil, 80 add: make(chan *Entry), 81 stop: make(chan struct{}), 82 snapshot: make(chan []*Entry), 83 running: false, 84 ErrorLog: nil, 85 location: location, 86 } 87 } 88 89 // FuncJob A wrapper that turns a func() into a cron.Job 90 type FuncJob func() 91 92 func (f FuncJob) Run() { f() } 93 94 // AddFunc adds a func to the Cron to be run on the given schedule. 95 func (c *Cron) AddFunc(spec string, cmd func()) error { 96 return c.AddJob(spec, FuncJob(cmd)) 97 } 98 99 // AddJob adds a Job to the Cron to be run on the given schedule. 100 func (c *Cron) AddJob(spec string, cmd Job) error { 101 schedule, err := Parse(spec) 102 if err != nil { 103 return err 104 } 105 c.Schedule(schedule, cmd) 106 return nil 107 } 108 109 // Schedule adds a Job to the Cron to be run on the given schedule. 110 func (c *Cron) Schedule(schedule Schedule, cmd Job) { 111 entry := &Entry{ 112 Schedule: schedule, 113 Job: cmd, 114 } 115 if !c.running { 116 c.entries = append(c.entries, entry) 117 return 118 } 119 120 c.add <- entry 121 } 122 123 // Entries returns a snapshot of the cron entries. 124 func (c *Cron) Entries() []*Entry { 125 if c.running { 126 c.snapshot <- nil 127 x := <-c.snapshot 128 return x 129 } 130 return c.entrySnapshot() 131 } 132 133 // Location gets the time zone location 134 func (c *Cron) Location() *time.Location { 135 return c.location 136 } 137 138 // Start the cron scheduler in its own go-routine, or no-op if already started. 139 func (c *Cron) Start() { 140 if c.running { 141 return 142 } 143 c.running = true 144 go c.run() 145 } 146 147 // Run the cron scheduler, or no-op if already running. 148 func (c *Cron) Run() { 149 if c.running { 150 return 151 } 152 c.running = true 153 c.run() 154 } 155 156 func (c *Cron) runWithRecovery(j Job) { 157 defer func() { 158 if r := recover(); r != nil { 159 const size = 64 << 10 160 buf := make([]byte, size) 161 buf = buf[:runtime.Stack(buf, false)] 162 c.logf("cron: panic running job: %v\n%s", r, buf) 163 } 164 }() 165 j.Run() 166 } 167 168 // Run the scheduler. this is private just due to the need to synchronize 169 // access to the 'running' state variable. 170 func (c *Cron) run() { 171 // Figure out the next activation times for each entry. 172 now := c.now() 173 for _, entry := range c.entries { 174 entry.Next = entry.Schedule.Next(now) 175 } 176 177 for { 178 // Determine the next entry to run. 179 sort.Sort(byTime(c.entries)) 180 181 var timer *time.Timer 182 if len(c.entries) == 0 || c.entries[0].Next.IsZero() { 183 // If there are no entries yet, just sleep - it still handles new entries 184 // and stop requests. 185 timer = time.NewTimer(100000 * time.Hour) 186 } else { 187 timer = time.NewTimer(c.entries[0].Next.Sub(now)) 188 } 189 190 for { 191 select { 192 case now = <-timer.C: 193 now = now.In(c.location) 194 // Run every entry whose next time was less than now 195 for _, e := range c.entries { 196 if e.Next.After(now) || e.Next.IsZero() { 197 break 198 } 199 go c.runWithRecovery(e.Job) 200 e.Prev = e.Next 201 e.Next = e.Schedule.Next(now) 202 } 203 204 case newEntry := <-c.add: 205 timer.Stop() 206 now = c.now() 207 newEntry.Next = newEntry.Schedule.Next(now) 208 c.entries = append(c.entries, newEntry) 209 210 case <-c.snapshot: 211 c.snapshot <- c.entrySnapshot() 212 continue 213 214 case <-c.stop: 215 timer.Stop() 216 return 217 } 218 219 break 220 } 221 } 222 } 223 224 // Logs an error to stderr or to the configured error log 225 func (c *Cron) logf(format string, args ...any) { 226 if c.ErrorLog != nil { 227 c.ErrorLog.Printf(format, args...) 228 } else { 229 log.Printf(format, args...) 230 } 231 } 232 233 // Stop stops the cron scheduler if it is running; otherwise it does nothing. 234 func (c *Cron) Stop() { 235 if !c.running { 236 return 237 } 238 c.stop <- struct{}{} 239 c.running = false 240 } 241 242 // entrySnapshot returns a copy of the current cron entry list. 243 func (c *Cron) entrySnapshot() []*Entry { 244 var entries []*Entry 245 for _, e := range c.entries { 246 entries = append(entries, &Entry{ 247 Schedule: e.Schedule, 248 Next: e.Next, 249 Prev: e.Prev, 250 Job: e.Job, 251 }) 252 } 253 return entries 254 } 255 256 // now returns current time in c location 257 func (c *Cron) now() time.Time { 258 return time.Now().In(c.location) 259 } 260 261 // ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". 262 // It does not support jobs more frequent than once a second. 263 type ConstantDelaySchedule struct { 264 Delay time.Duration 265 } 266 267 // Every returns a crontab Schedule that activates once every duration. 268 // Delays of less than a second are not supported (will round up to 1 second). 269 // Any fields less than a Second are truncated. 270 func Every(duration time.Duration) ConstantDelaySchedule { 271 if duration < time.Second { 272 duration = time.Second 273 } 274 return ConstantDelaySchedule{ 275 Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, 276 } 277 } 278 279 // Next returns the next time this should be run. 280 // This rounds so that the next activation time will be on the second. 281 func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { 282 return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) 283 }