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