You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

269 lines
6.0 KiB

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