gitee.com/quant1x/gox@v1.21.2/cron/cron.go (about) 1 package cron 2 3 import ( 4 "context" 5 "gitee.com/quant1x/gox/logger" 6 "gitee.com/quant1x/gox/runtime" 7 "sort" 8 "sync" 9 "time" 10 ) 11 12 // Cron keeps track of any number of entries, invoking the associated func as 13 // specified by the schedule. It may be started, stopped, and the entries may 14 // be inspected while running. 15 type Cron struct { 16 entries []*Entry 17 chain Chain 18 stop chan struct{} 19 add chan *Entry 20 remove chan EntryID 21 snapshot chan chan []Entry 22 running bool 23 logger Logger 24 runningMu sync.Mutex 25 location *time.Location 26 parser ScheduleParser 27 nextID EntryID 28 jobWaiter sync.WaitGroup 29 } 30 31 // ScheduleParser is an interface for schedule spec parsers that return a Schedule 32 type ScheduleParser interface { 33 Parse(spec string) (Schedule, error) 34 } 35 36 // Job is an interface for submitted cron jobs. 37 type Job interface { 38 Run() 39 } 40 41 // Schedule describes a job's duty cycle. 42 type Schedule interface { 43 // Next returns the next activation time, later than the given time. 44 // Next is invoked initially, and then each time the job is run. 45 Next(time.Time) time.Time 46 } 47 48 // EntryID identifies an entry within a Cron instance 49 type EntryID int 50 51 // Entry consists of a schedule and the func to execute on that schedule. 52 type Entry struct { 53 // ID is the cron-assigned ID of this entry, which may be used to look up a 54 // snapshot or remove it. 55 ID EntryID 56 57 // Schedule on which this job should be run. 58 Schedule Schedule 59 60 // Next time the job will run, or the zero time if Cron has not been 61 // started or this entry's schedule is unsatisfiable 62 Next time.Time 63 64 // Prev is the last time this job was run, or the zero time if never. 65 Prev time.Time 66 67 // WrappedJob is the thing to run when the Schedule is activated. 68 WrappedJob Job 69 70 // Job is the thing that was submitted to cron. 71 // It is kept around so that user code that needs to get at the job later, 72 // e.g. via Entries() can do so. 73 Job Job 74 } 75 76 // Valid returns true if this is not the zero entry. 77 func (e Entry) Valid() bool { return e.ID != 0 } 78 79 // byTime is a wrapper for sorting the entry array by time 80 // (with zero time at the end). 81 type byTime []*Entry 82 83 func (s byTime) Len() int { return len(s) } 84 func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 85 func (s byTime) Less(i, j int) bool { 86 // Two zero times should return false. 87 // Otherwise, zero is "greater" than any other time. 88 // (To sort it at the end of the list.) 89 if s[i].Next.IsZero() { 90 return false 91 } 92 if s[j].Next.IsZero() { 93 return true 94 } 95 return s[i].Next.Before(s[j].Next) 96 } 97 98 // New returns a new Cron job runner, modified by the given options. 99 // 100 // Available Settings 101 // 102 // Time Zone 103 // Description: The time zone in which schedules are interpreted 104 // Default: time.Local 105 // 106 // Parser 107 // Description: Parser converts cron spec strings into cron.Schedules. 108 // Default: Accepts this spec: https://en.wikipedia.org/wiki/Cron 109 // 110 // Chain 111 // Description: Wrap submitted jobs to customize behavior. 112 // Default: A chain that recovers panics and logs them to stderr. 113 // 114 // See "cron.With*" to modify the default behavior. 115 func New(opts ...Option) *Cron { 116 c := &Cron{ 117 entries: nil, 118 chain: NewChain(), 119 add: make(chan *Entry), 120 stop: make(chan struct{}), 121 snapshot: make(chan chan []Entry), 122 remove: make(chan EntryID), 123 running: false, 124 runningMu: sync.Mutex{}, 125 logger: DefaultLogger, 126 location: time.Local, 127 parser: standardParser, 128 } 129 for _, opt := range opts { 130 opt(c) 131 } 132 return c 133 } 134 135 // FuncJob is a wrapper that turns a func() into a cron.Job 136 type FuncJob func() 137 138 func (f FuncJob) Run() { f() } 139 140 // AddFunc adds a func to the Cron to be run on the given schedule. 141 // The spec is parsed using the time zone of this Cron instance as the default. 142 // An opaque ID is returned that can be used to later remove it. 143 func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) { 144 return c.AddJob(spec, FuncJob(cmd)) 145 } 146 147 // AddJob adds a Job to the Cron to be run on the given schedule. 148 // The spec is parsed using the time zone of this Cron instance as the default. 149 // An opaque ID is returned that can be used to later remove it. 150 func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) { 151 schedule, err := c.parser.Parse(spec) 152 if err != nil { 153 return 0, err 154 } 155 return c.Schedule(schedule, cmd), nil 156 } 157 158 func (c *Cron) AddFuncWithSkipIfStillRunning(spec string, cmd func()) (EntryID, error) { 159 job := SkipIfStillRunning()(FuncJob(cmd)) 160 return c.AddJob(spec, job) 161 } 162 163 func (c *Cron) AddJobWithSkipIfStillRunning(spec string, cmd func()) (EntryID, error) { 164 var ch = make(chan struct{}, 1) 165 ch <- struct{}{} 166 funcName := runtime.FuncName(cmd) 167 jobFunc := func() { 168 select { 169 case v := <-ch: 170 defer func() { ch <- v }() 171 var tmStart time.Time 172 if runtime.Debug() { 173 tmStart = time.Now() 174 } 175 cmd() 176 if runtime.Debug() { 177 elapsedTime := time.Since(tmStart) / time.Millisecond 178 logger.Warnf("func: %s, 总耗时: %.3fs", funcName, float64(elapsedTime)/1000) 179 } 180 default: 181 if runtime.Debug() { 182 logger.Warnf("func: %s, skip", funcName) 183 } 184 } 185 } 186 187 return c.AddJob(spec, FuncJob(jobFunc)) 188 } 189 190 // Schedule adds a Job to the Cron to be run on the given schedule. 191 // The job is wrapped with the configured Chain. 192 func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID { 193 c.runningMu.Lock() 194 defer c.runningMu.Unlock() 195 c.nextID++ 196 entry := &Entry{ 197 ID: c.nextID, 198 Schedule: schedule, 199 WrappedJob: c.chain.Then(cmd), 200 Job: cmd, 201 } 202 if !c.running { 203 c.entries = append(c.entries, entry) 204 } else { 205 c.add <- entry 206 } 207 return entry.ID 208 } 209 210 // Entries returns a snapshot of the cron entries. 211 func (c *Cron) Entries() []Entry { 212 c.runningMu.Lock() 213 defer c.runningMu.Unlock() 214 if c.running { 215 replyChan := make(chan []Entry, 1) 216 c.snapshot <- replyChan 217 return <-replyChan 218 } 219 return c.entrySnapshot() 220 } 221 222 // Location gets the time zone location 223 func (c *Cron) Location() *time.Location { 224 return c.location 225 } 226 227 // Entry returns a snapshot of the given entry, or nil if it couldn't be found. 228 func (c *Cron) Entry(id EntryID) Entry { 229 for _, entry := range c.Entries() { 230 if id == entry.ID { 231 return entry 232 } 233 } 234 return Entry{} 235 } 236 237 // Remove an entry from being run in the future. 238 func (c *Cron) Remove(id EntryID) { 239 c.runningMu.Lock() 240 defer c.runningMu.Unlock() 241 if c.running { 242 c.remove <- id 243 } else { 244 c.removeEntry(id) 245 } 246 } 247 248 // Start the cron scheduler in its own goroutine, or no-op if already started. 249 func (c *Cron) Start() { 250 c.runningMu.Lock() 251 defer c.runningMu.Unlock() 252 if c.running { 253 return 254 } 255 c.running = true 256 go c.run() 257 } 258 259 // Run the cron scheduler, or no-op if already running. 260 func (c *Cron) Run() { 261 c.runningMu.Lock() 262 if c.running { 263 c.runningMu.Unlock() 264 return 265 } 266 c.running = true 267 c.runningMu.Unlock() 268 c.run() 269 } 270 271 // run the scheduler.. this is private just due to the need to synchronize 272 // access to the 'running' state variable. 273 func (c *Cron) run() { 274 c.logger.Info("start") 275 276 // Figure out the next activation times for each entry. 277 now := c.now() 278 for _, entry := range c.entries { 279 entry.Next = entry.Schedule.Next(now) 280 c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next) 281 } 282 283 for { 284 // Determine the next entry to run. 285 sort.Sort(byTime(c.entries)) 286 287 var timer *time.Timer 288 if len(c.entries) == 0 || c.entries[0].Next.IsZero() { 289 // If there are no entries yet, just sleep - it still handles new entries 290 // and stop requests. 291 timer = time.NewTimer(100000 * time.Hour) 292 } else { 293 timer = time.NewTimer(c.entries[0].Next.Sub(now)) 294 } 295 296 for { 297 select { 298 case now = <-timer.C: 299 now = now.In(c.location) 300 c.logger.Info("wake", "now", now) 301 302 // Run every entry whose next time was less than now 303 for _, e := range c.entries { 304 if e.Next.After(now) || e.Next.IsZero() { 305 break 306 } 307 c.startJob(e.WrappedJob) 308 e.Prev = e.Next 309 e.Next = e.Schedule.Next(now) 310 c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next) 311 } 312 313 case newEntry := <-c.add: 314 timer.Stop() 315 now = c.now() 316 newEntry.Next = newEntry.Schedule.Next(now) 317 c.entries = append(c.entries, newEntry) 318 c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next) 319 320 case replyChan := <-c.snapshot: 321 replyChan <- c.entrySnapshot() 322 continue 323 324 case <-c.stop: 325 timer.Stop() 326 c.logger.Info("stop") 327 return 328 329 case id := <-c.remove: 330 timer.Stop() 331 now = c.now() 332 c.removeEntry(id) 333 c.logger.Info("removed", "entry", id) 334 } 335 336 break 337 } 338 } 339 } 340 341 // startJob runs the given job in a new goroutine. 342 func (c *Cron) startJob(j Job) { 343 c.jobWaiter.Add(1) 344 go func() { 345 defer c.jobWaiter.Done() 346 j.Run() 347 }() 348 } 349 350 // now returns current time in c location 351 func (c *Cron) now() time.Time { 352 return time.Now().In(c.location) 353 } 354 355 // Stop stops the cron scheduler if it is running; otherwise it does nothing. 356 // A context is returned so the caller can wait for running jobs to complete. 357 func (c *Cron) Stop() context.Context { 358 c.runningMu.Lock() 359 defer c.runningMu.Unlock() 360 if c.running { 361 c.stop <- struct{}{} 362 c.running = false 363 } 364 ctx, cancel := context.WithCancel(context.Background()) 365 go func() { 366 c.jobWaiter.Wait() 367 cancel() 368 }() 369 return ctx 370 } 371 372 // entrySnapshot returns a copy of the current cron entry list. 373 func (c *Cron) entrySnapshot() []Entry { 374 var entries = make([]Entry, len(c.entries)) 375 for i, e := range c.entries { 376 entries[i] = *e 377 } 378 return entries 379 } 380 381 func (c *Cron) removeEntry(id EntryID) { 382 var entries []*Entry 383 for _, e := range c.entries { 384 if e.ID != id { 385 entries = append(entries, e) 386 } 387 } 388 c.entries = entries 389 }