gcron_schedule.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
  2. //
  3. // This Source Code Form is subject to the terms of the MIT License.
  4. // If a copy of the MIT was not distributed with this file,
  5. // You can obtain one at https://github.com/gogf/gf.
  6. package gcron
  7. import (
  8. "context"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/gogf/gf/v2/container/gtype"
  13. "github.com/gogf/gf/v2/errors/gcode"
  14. "github.com/gogf/gf/v2/errors/gerror"
  15. "github.com/gogf/gf/v2/os/gtime"
  16. "github.com/gogf/gf/v2/text/gregex"
  17. )
  18. // cronSchedule is the schedule for cron job.
  19. type cronSchedule struct {
  20. createTimestamp int64 // Created timestamp in seconds.
  21. everySeconds int64 // Running interval in seconds.
  22. pattern string // The raw cron pattern string.
  23. secondMap map[int]struct{} // Job can run in these second numbers.
  24. minuteMap map[int]struct{} // Job can run in these minute numbers.
  25. hourMap map[int]struct{} // Job can run in these hour numbers.
  26. dayMap map[int]struct{} // Job can run in these day numbers.
  27. weekMap map[int]struct{} // Job can run in these week numbers.
  28. monthMap map[int]struct{} // Job can run in these moth numbers.
  29. lastTimestamp *gtype.Int64 // Last timestamp number, for timestamp fix in some delay.
  30. }
  31. const (
  32. // regular expression for cron pattern, which contains 6 parts of time units.
  33. regexForCron = `^([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,A-Za-z]+)\s+([\-/\d\*\?,A-Za-z]+)$`
  34. patternItemTypeUnknown = iota
  35. patternItemTypeWeek
  36. patternItemTypeMonth
  37. )
  38. var (
  39. // Predefined pattern map.
  40. predefinedPatternMap = map[string]string{
  41. "@yearly": "0 0 0 1 1 *",
  42. "@annually": "0 0 0 1 1 *",
  43. "@monthly": "0 0 0 1 * *",
  44. "@weekly": "0 0 0 * * 0",
  45. "@daily": "0 0 0 * * *",
  46. "@midnight": "0 0 0 * * *",
  47. "@hourly": "0 0 * * * *",
  48. }
  49. // Short month name to its number.
  50. monthShortNameMap = map[string]int{
  51. "jan": 1,
  52. "feb": 2,
  53. "mar": 3,
  54. "apr": 4,
  55. "may": 5,
  56. "jun": 6,
  57. "jul": 7,
  58. "aug": 8,
  59. "sep": 9,
  60. "oct": 10,
  61. "nov": 11,
  62. "dec": 12,
  63. }
  64. // Full month name to its number.
  65. monthFullNameMap = map[string]int{
  66. "january": 1,
  67. "february": 2,
  68. "march": 3,
  69. "april": 4,
  70. "may": 5,
  71. "june": 6,
  72. "july": 7,
  73. "august": 8,
  74. "september": 9,
  75. "october": 10,
  76. "november": 11,
  77. "december": 12,
  78. }
  79. // Short week name to its number.
  80. weekShortNameMap = map[string]int{
  81. "sun": 0,
  82. "mon": 1,
  83. "tue": 2,
  84. "wed": 3,
  85. "thu": 4,
  86. "fri": 5,
  87. "sat": 6,
  88. }
  89. // Full week name to its number.
  90. weekFullNameMap = map[string]int{
  91. "sunday": 0,
  92. "monday": 1,
  93. "tuesday": 2,
  94. "wednesday": 3,
  95. "thursday": 4,
  96. "friday": 5,
  97. "saturday": 6,
  98. }
  99. )
  100. // newSchedule creates and returns a schedule object for given cron pattern.
  101. func newSchedule(pattern string) (*cronSchedule, error) {
  102. var currentTimestamp = time.Now().Unix()
  103. // Check if the predefined patterns.
  104. if match, _ := gregex.MatchString(`(@\w+)\s*(\w*)\s*`, pattern); len(match) > 0 {
  105. key := strings.ToLower(match[1])
  106. if v, ok := predefinedPatternMap[key]; ok {
  107. pattern = v
  108. } else if strings.Compare(key, "@every") == 0 {
  109. d, err := gtime.ParseDuration(match[2])
  110. if err != nil {
  111. return nil, err
  112. }
  113. return &cronSchedule{
  114. createTimestamp: currentTimestamp,
  115. everySeconds: int64(d.Seconds()),
  116. pattern: pattern,
  117. lastTimestamp: gtype.NewInt64(currentTimestamp),
  118. }, nil
  119. } else {
  120. return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
  121. }
  122. }
  123. // Handle the common cron pattern, like:
  124. // 0 0 0 1 1 2
  125. if match, _ := gregex.MatchString(regexForCron, pattern); len(match) == 7 {
  126. schedule := &cronSchedule{
  127. createTimestamp: currentTimestamp,
  128. everySeconds: 0,
  129. pattern: pattern,
  130. lastTimestamp: gtype.NewInt64(currentTimestamp),
  131. }
  132. // Second.
  133. if m, err := parsePatternItem(match[1], 0, 59, false); err != nil {
  134. return nil, err
  135. } else {
  136. schedule.secondMap = m
  137. }
  138. // Minute.
  139. if m, err := parsePatternItem(match[2], 0, 59, false); err != nil {
  140. return nil, err
  141. } else {
  142. schedule.minuteMap = m
  143. }
  144. // Hour.
  145. if m, err := parsePatternItem(match[3], 0, 23, false); err != nil {
  146. return nil, err
  147. } else {
  148. schedule.hourMap = m
  149. }
  150. // Day.
  151. if m, err := parsePatternItem(match[4], 1, 31, true); err != nil {
  152. return nil, err
  153. } else {
  154. schedule.dayMap = m
  155. }
  156. // Month.
  157. if m, err := parsePatternItem(match[5], 1, 12, false); err != nil {
  158. return nil, err
  159. } else {
  160. schedule.monthMap = m
  161. }
  162. // Week.
  163. if m, err := parsePatternItem(match[6], 0, 6, true); err != nil {
  164. return nil, err
  165. } else {
  166. schedule.weekMap = m
  167. }
  168. return schedule, nil
  169. }
  170. return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
  171. }
  172. // parsePatternItem parses every item in the pattern and returns the result as map, which is used for indexing.
  173. func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (map[int]struct{}, error) {
  174. m := make(map[int]struct{}, max-min+1)
  175. if item == "*" || (allowQuestionMark && item == "?") {
  176. for i := min; i <= max; i++ {
  177. m[i] = struct{}{}
  178. }
  179. return m, nil
  180. }
  181. // Like: MON,FRI
  182. for _, itemElem := range strings.Split(item, ",") {
  183. var (
  184. interval = 1
  185. intervalArray = strings.Split(itemElem, "/")
  186. )
  187. if len(intervalArray) == 2 {
  188. if number, err := strconv.Atoi(intervalArray[1]); err != nil {
  189. return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem)
  190. } else {
  191. interval = number
  192. }
  193. }
  194. var (
  195. rangeMin = min
  196. rangeMax = max
  197. itemType = patternItemTypeUnknown
  198. rangeArray = strings.Split(intervalArray[0], "-") // Like: 1-30, JAN-DEC
  199. )
  200. switch max {
  201. case 6:
  202. // It's checking week field.
  203. itemType = patternItemTypeWeek
  204. case 12:
  205. // It's checking month field.
  206. itemType = patternItemTypeMonth
  207. }
  208. // Eg: */5
  209. if rangeArray[0] != "*" {
  210. if number, err := parsePatternItemValue(rangeArray[0], itemType); err != nil {
  211. return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem)
  212. } else {
  213. rangeMin = number
  214. if len(intervalArray) == 1 {
  215. rangeMax = number
  216. }
  217. }
  218. }
  219. if len(rangeArray) == 2 {
  220. if number, err := parsePatternItemValue(rangeArray[1], itemType); err != nil {
  221. return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem)
  222. } else {
  223. rangeMax = number
  224. }
  225. }
  226. for i := rangeMin; i <= rangeMax; i += interval {
  227. m[i] = struct{}{}
  228. }
  229. }
  230. return m, nil
  231. }
  232. // parsePatternItemValue parses the field value to a number according to its field type.
  233. func parsePatternItemValue(value string, itemType int) (int, error) {
  234. if gregex.IsMatchString(`^\d+$`, value) {
  235. // It is pure number.
  236. if number, err := strconv.Atoi(value); err == nil {
  237. return number, nil
  238. }
  239. } else {
  240. // Check if it contains letter,
  241. // it converts the value to number according to predefined map.
  242. switch itemType {
  243. case patternItemTypeWeek:
  244. if number, ok := weekShortNameMap[strings.ToLower(value)]; ok {
  245. return number, nil
  246. }
  247. if number, ok := weekFullNameMap[strings.ToLower(value)]; ok {
  248. return number, nil
  249. }
  250. case patternItemTypeMonth:
  251. if number, ok := monthShortNameMap[strings.ToLower(value)]; ok {
  252. return number, nil
  253. }
  254. if number, ok := monthFullNameMap[strings.ToLower(value)]; ok {
  255. return number, nil
  256. }
  257. }
  258. }
  259. return 0, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern value: "%s"`, value)
  260. }
  261. // checkMeetAndUpdateLastSeconds checks if the given time `t` meets the runnable point for the job.
  262. func (s *cronSchedule) checkMeetAndUpdateLastSeconds(ctx context.Context, t time.Time) bool {
  263. var (
  264. lastTimestamp = s.getAndUpdateLastTimestamp(ctx, t)
  265. lastTime = gtime.NewFromTimeStamp(lastTimestamp)
  266. )
  267. if s.everySeconds != 0 {
  268. // It checks using interval.
  269. secondsAfterCreated := lastTime.Timestamp() - s.createTimestamp
  270. if secondsAfterCreated > 0 {
  271. return secondsAfterCreated%s.everySeconds == 0
  272. }
  273. return false
  274. }
  275. // It checks using normal cron pattern.
  276. if _, ok := s.secondMap[lastTime.Second()]; !ok {
  277. return false
  278. }
  279. if _, ok := s.minuteMap[lastTime.Minute()]; !ok {
  280. return false
  281. }
  282. if _, ok := s.hourMap[lastTime.Hour()]; !ok {
  283. return false
  284. }
  285. if _, ok := s.dayMap[lastTime.Day()]; !ok {
  286. return false
  287. }
  288. if _, ok := s.monthMap[lastTime.Month()]; !ok {
  289. return false
  290. }
  291. if _, ok := s.weekMap[int(lastTime.Weekday())]; !ok {
  292. return false
  293. }
  294. return true
  295. }
  296. // Next returns the next time this schedule is activated, greater than the given
  297. // time. If no time can be found to satisfy the schedule, return the zero time.
  298. func (s *cronSchedule) Next(t time.Time) time.Time {
  299. if s.everySeconds != 0 {
  300. var (
  301. diff = t.Unix() - s.createTimestamp
  302. count = diff/s.everySeconds + 1
  303. )
  304. return t.Add(time.Duration(count*s.everySeconds) * time.Second)
  305. }
  306. // Start at the earliest possible time (the upcoming second).
  307. t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
  308. var (
  309. loc = t.Location()
  310. added = false
  311. yearLimit = t.Year() + 5
  312. )
  313. WRAP:
  314. if t.Year() > yearLimit {
  315. return t // who will care the job that run in five years later
  316. }
  317. for !s.match(s.monthMap, int(t.Month())) {
  318. if !added {
  319. added = true
  320. t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
  321. }
  322. t = t.AddDate(0, 1, 0)
  323. // need recheck
  324. if t.Month() == time.January {
  325. goto WRAP
  326. }
  327. }
  328. for !s.dayMatches(t) {
  329. if !added {
  330. added = true
  331. t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
  332. }
  333. t = t.AddDate(0, 0, 1)
  334. // Notice if the hour is no longer midnight due to DST.
  335. // Add an hour if it's 23, subtract an hour if it's 1.
  336. if t.Hour() != 0 {
  337. if t.Hour() > 12 {
  338. t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
  339. } else {
  340. t = t.Add(time.Duration(-t.Hour()) * time.Hour)
  341. }
  342. }
  343. if t.Day() == 1 {
  344. goto WRAP
  345. }
  346. }
  347. for !s.match(s.hourMap, t.Hour()) {
  348. if !added {
  349. added = true
  350. t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
  351. }
  352. t = t.Add(time.Hour)
  353. // need recheck
  354. if t.Hour() == 0 {
  355. goto WRAP
  356. }
  357. }
  358. for !s.match(s.minuteMap, t.Minute()) {
  359. if !added {
  360. added = true
  361. t = t.Truncate(time.Minute)
  362. }
  363. t = t.Add(1 * time.Minute)
  364. if t.Minute() == 0 {
  365. goto WRAP
  366. }
  367. }
  368. for !s.match(s.secondMap, t.Second()) {
  369. if !added {
  370. added = true
  371. t = t.Truncate(time.Second)
  372. }
  373. t = t.Add(1 * time.Second)
  374. if t.Second() == 0 {
  375. goto WRAP
  376. }
  377. }
  378. return t.In(loc)
  379. }
  380. // dayMatches returns true if the schedule's day-of-week and day-of-month
  381. // restrictions are satisfied by the given time.
  382. func (s *cronSchedule) dayMatches(t time.Time) bool {
  383. _, ok1 := s.dayMap[t.Day()]
  384. _, ok2 := s.weekMap[int(t.Weekday())]
  385. return ok1 && ok2
  386. }
  387. func (s *cronSchedule) match(m map[int]struct{}, key int) bool {
  388. _, ok := m[key]
  389. return ok
  390. }