timer.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package models
  15. import (
  16. "context"
  17. "fmt"
  18. "sort"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "yunion.io/x/log"
  23. api "yunion.io/x/onecloud/pkg/apis/scheduledtask"
  24. "yunion.io/x/onecloud/pkg/i18n"
  25. "yunion.io/x/onecloud/pkg/util/bitmap"
  26. )
  27. type STimer struct {
  28. // Cycle type
  29. Type string `width:"8" charset:"ascii"`
  30. // 0-59
  31. Minute int `nullable:"false"`
  32. // 0-23
  33. Hour int `nullable:"false"`
  34. CycleNum int `nullable:"false"`
  35. // 0-7 1 is Monday 0 is unlimited
  36. WeekDays uint8 `nullable:"false"`
  37. // 0-31 0 is unlimited
  38. MonthDays uint32 `nullable:"false"`
  39. // StartTime represent the start time of this timer
  40. StartTime time.Time
  41. // EndTime represent deadline of this timer
  42. EndTime time.Time
  43. // NextTime represent the time timer should bell
  44. NextTime time.Time `index:"true"`
  45. IsExpired bool
  46. }
  47. // Update will update the SScalingTimer
  48. func (st *STimer) Update(now time.Time) {
  49. if now.IsZero() {
  50. now = time.Now()
  51. }
  52. if !now.Before(st.EndTime) {
  53. st.IsExpired = true
  54. return
  55. }
  56. if now.Before(st.StartTime) {
  57. now = st.StartTime
  58. }
  59. if !st.NextTime.Before(now) {
  60. return
  61. }
  62. newNextTime := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, time.UTC).In(now.Location())
  63. if now.After(newNextTime) {
  64. newNextTime = newNextTime.AddDate(0, 0, 1)
  65. }
  66. switch {
  67. case st.WeekDays != 0:
  68. // week
  69. nowDay, weekdays := int(newNextTime.Weekday()), st.GetWeekDays()
  70. if nowDay == 0 {
  71. nowDay = 7
  72. }
  73. // weekdays[0]+7 is for the case that all time nodes has been missed in this week
  74. weekdays = append(weekdays, weekdays[0]+7)
  75. index := sort.SearchInts(weekdays, nowDay)
  76. newNextTime = newNextTime.AddDate(0, 0, weekdays[index]-nowDay)
  77. case st.MonthDays != 0:
  78. // month
  79. monthdays := st.GetMonthDays()
  80. suitTime := newNextTime
  81. for {
  82. day := suitTime.Day()
  83. index := sort.SearchInts(monthdays, day)
  84. if index == len(monthdays) || monthdays[index] > st.MonthDaySum(suitTime) {
  85. // set suitTime as the first day of next month
  86. suitTime = suitTime.AddDate(0, 1, -suitTime.Day()+1)
  87. continue
  88. }
  89. newNextTime = time.Date(suitTime.Year(), suitTime.Month(), monthdays[index], suitTime.Hour(),
  90. suitTime.Minute(), 0, 0, suitTime.Location())
  91. break
  92. }
  93. case st.CycleNum != 0:
  94. switch st.Type {
  95. case api.TIMER_TYPE_HOUR:
  96. newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
  97. if now.After(newNextTime) {
  98. newNextTime = newNextTime.Add(time.Duration(st.CycleNum) * time.Hour)
  99. }
  100. case api.TIMER_TYPE_DAY:
  101. newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
  102. if now.After(newNextTime) {
  103. newNextTime = newNextTime.AddDate(0, 0, st.CycleNum)
  104. }
  105. case api.TIMER_TYPE_WEEK:
  106. newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
  107. if now.After(newNextTime) {
  108. newNextTime = newNextTime.AddDate(0, 0, st.CycleNum*7)
  109. }
  110. case api.TIMER_TYPE_MONTH:
  111. newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
  112. if now.After(newNextTime) {
  113. newNextTime = newNextTime.AddDate(0, st.CycleNum, 0)
  114. }
  115. }
  116. default:
  117. // day
  118. }
  119. log.Debugf("The final NextTime: %s", newNextTime)
  120. st.NextTime = newNextTime
  121. if st.NextTime.After(st.EndTime) {
  122. st.IsExpired = true
  123. }
  124. }
  125. // MonthDaySum calculate the number of month's days
  126. func (st *STimer) MonthDaySum(t time.Time) int {
  127. year, month := t.Year(), t.Month()
  128. monthDays := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
  129. if month != 2 {
  130. return monthDays[2]
  131. }
  132. if year%4 != 0 || (year%100 == 0 && year%400 != 0) {
  133. return 28
  134. }
  135. return 29
  136. }
  137. func (st *STimer) GetWeekDays() []int {
  138. return bitmap.Uint2IntArray(uint32(st.WeekDays))
  139. }
  140. func (st *STimer) GetMonthDays() []int {
  141. return bitmap.Uint2IntArray(st.MonthDays)
  142. }
  143. func (st *STimer) SetWeekDays(days []int) {
  144. st.WeekDays = uint8(bitmap.IntArray2Uint(days))
  145. }
  146. func (st *STimer) SetMonthDays(days []int) {
  147. st.MonthDays = bitmap.IntArray2Uint(days)
  148. }
  149. func (st *STimer) TimerDetails() api.TimerDetails {
  150. return api.TimerDetails{ExecTime: st.EndTime}
  151. }
  152. func (st *STimer) CycleTimerDetails() api.CycleTimerDetails {
  153. out := api.CycleTimerDetails{
  154. Minute: st.Minute,
  155. Hour: st.Hour,
  156. WeekDays: st.GetWeekDays(),
  157. MonthDays: st.GetMonthDays(),
  158. StartTime: st.StartTime,
  159. EndTime: st.EndTime,
  160. CycleType: st.Type,
  161. }
  162. return out
  163. }
  164. func checkTimerCreateInput(in api.TimerCreateInput) (api.TimerCreateInput, error) {
  165. now := time.Now()
  166. if now.After(in.ExecTime) {
  167. return in, fmt.Errorf("exec_time is earlier than now")
  168. }
  169. return in, nil
  170. }
  171. var (
  172. timerDescTable = i18n.Table{}
  173. TIMERLANG = "timerLang"
  174. )
  175. func init() {
  176. timerDescTable.Set("timerLang", i18n.NewTableEntry().EN("en").CN("cn"))
  177. }
  178. func (st *STimer) Description(ctx context.Context, createdAt time.Time, zone *time.Location) string {
  179. lang := timerDescTable.Lookup(ctx, TIMERLANG)
  180. switch lang {
  181. case "en":
  182. return st.descEnglish(createdAt, zone)
  183. case "cn":
  184. return st.descChinese(createdAt, zone)
  185. }
  186. return ""
  187. }
  188. var (
  189. wdsCN = []string{"", "一", "二", "三", "四", "五", "六", "日"}
  190. wdsEN = []string{"", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
  191. )
  192. func (st *STimer) descChinese(createdAt time.Time, zone *time.Location) string {
  193. format := "2006-01-02 15:04:05"
  194. var prefix string
  195. switch st.Type {
  196. case api.TIMER_TYPE_ONCE:
  197. return fmt.Sprintf("单次 %s触发", st.StartTime.In(zone).Format(format))
  198. case api.TIMER_TYPE_HOUR:
  199. prefix = fmt.Sprintf("每%d小时", st.CycleNum)
  200. case api.TIMER_TYPE_DAY:
  201. prefix = "每天"
  202. if st.CycleNum > 0 {
  203. prefix = fmt.Sprintf("每%d天", st.CycleNum)
  204. }
  205. case api.TIMER_TYPE_WEEK:
  206. wds := st.GetWeekDays()
  207. weekDays := make([]string, len(wds))
  208. for i := range wds {
  209. weekDays[i] = fmt.Sprintf("星期%s", wdsCN[wds[i]])
  210. }
  211. prefix = fmt.Sprintf("每周 【%s】", strings.Join(weekDays, "|"))
  212. if st.CycleNum > 0 {
  213. prefix = fmt.Sprintf("每%d周", st.CycleNum)
  214. }
  215. case api.TIMER_TYPE_MONTH:
  216. mns := st.GetMonthDays()
  217. monthDays := make([]string, len(mns))
  218. for i := range mns {
  219. monthDays[i] = fmt.Sprintf("%d号", mns[i])
  220. }
  221. prefix = fmt.Sprintf("每月 【%s】", strings.Join(monthDays, "|"))
  222. if st.CycleNum > 0 {
  223. prefix = fmt.Sprintf("每%d月", st.CycleNum)
  224. }
  225. }
  226. if st.CycleNum > 0 {
  227. return fmt.Sprintf("%s同步一次,开始时间:%s,有效时间为%s至%s", prefix, createdAt.In(zone).Format(format), st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
  228. }
  229. return fmt.Sprintf("%s %s触发 有效时间为%s至%s", prefix, st.hourMinutesDesc(zone), st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
  230. }
  231. func (st *STimer) hourMinutesDesc(zone *time.Location) string {
  232. now := time.Now()
  233. t := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, time.UTC).In(zone)
  234. return fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute())
  235. }
  236. func (st *STimer) descEnglish(createdAt time.Time, zone *time.Location) string {
  237. var detail string
  238. format := "2006-01-02 15:04:05"
  239. switch st.Type {
  240. case api.TIMER_TYPE_ONCE:
  241. return st.EndTime.In(zone).Format(format)
  242. case api.TIMER_TYPE_HOUR:
  243. detail = fmt.Sprintf("every %d hours", st.CycleNum)
  244. case api.TIMER_TYPE_DAY:
  245. if st.CycleNum > 0 {
  246. detail = fmt.Sprintf("every %d days", st.CycleNum)
  247. } else {
  248. detail = fmt.Sprintf("%s every day", st.hourMinutesDesc(zone))
  249. }
  250. case api.TIMER_TYPE_WEEK:
  251. if st.CycleNum > 0 {
  252. detail = fmt.Sprintf("every %d weeks", st.CycleNum)
  253. } else {
  254. detail = st.weekDaysDesc(zone)
  255. }
  256. case api.TIMER_TYPE_MONTH:
  257. if st.CycleNum > 0 {
  258. detail = fmt.Sprintf("every %d months", st.CycleNum)
  259. } else {
  260. detail = st.monthDaysDesc(zone)
  261. }
  262. }
  263. if st.CycleNum > 0 {
  264. return fmt.Sprintf("Execute %s, start at:%s , from %s to %s", detail, createdAt.In(zone).Format(format), st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
  265. }
  266. if st.EndTime.IsZero() {
  267. return detail
  268. }
  269. return fmt.Sprintf("%s, from %s to %s", detail, st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
  270. }
  271. func (st *STimer) weekDaysDesc(zone *time.Location) string {
  272. if st.WeekDays == 0 {
  273. return ""
  274. }
  275. var desc strings.Builder
  276. wds := st.GetWeekDays()
  277. i := 0
  278. desc.WriteString(fmt.Sprintf("%s every %s", st.hourMinutesDesc(zone), wdsEN[wds[i]]))
  279. for i++; i < len(wds)-1; i++ {
  280. desc.WriteString(", ")
  281. desc.WriteString(wdsEN[wds[i]])
  282. }
  283. if i == len(wds)-1 {
  284. desc.WriteString(" and ")
  285. desc.WriteString(wdsEN[wds[i]])
  286. }
  287. return desc.String()
  288. }
  289. func (st *STimer) monthDaysDesc(zone *time.Location) string {
  290. if st.MonthDays == 0 {
  291. return ""
  292. }
  293. var desc strings.Builder
  294. mds := st.GetMonthDays()
  295. i := 0
  296. desc.WriteString(fmt.Sprintf("%s on the %d%s", st.hourMinutesDesc(zone), mds[i], st.dateSuffix(mds[i])))
  297. for i++; i < len(mds)-1; i++ {
  298. desc.WriteString(", ")
  299. desc.WriteString(strconv.Itoa(mds[i]))
  300. desc.WriteString(st.dateSuffix(mds[i]))
  301. }
  302. if i == len(mds)-1 {
  303. desc.WriteString(" and ")
  304. desc.WriteString(strconv.Itoa(mds[i]))
  305. desc.WriteString(st.dateSuffix(mds[i]))
  306. }
  307. desc.WriteString(" of each month")
  308. return desc.String()
  309. }
  310. func (st *STimer) dateSuffix(date int) string {
  311. var ret string
  312. switch date {
  313. case 1:
  314. ret = "st"
  315. case 2:
  316. ret = "nd"
  317. case 3:
  318. ret = "rd"
  319. default:
  320. ret = "th"
  321. }
  322. return ret
  323. }
  324. func checkCycleTimerCreateInput(in api.CycleTimerCreateInput) (api.CycleTimerCreateInput, error) {
  325. now := time.Now()
  326. if in.Minute < 0 || in.Minute > 59 {
  327. return in, fmt.Errorf("minute should between 0 and 59")
  328. }
  329. if in.Hour < 0 || in.Hour > 23 {
  330. return in, fmt.Errorf("hour should between 0 and 23")
  331. }
  332. switch in.CycleType {
  333. case api.TIMER_TYPE_HOUR:
  334. if in.CycleNum <= 0 || in.CycleNum >= 24 {
  335. return in, fmt.Errorf("hour cycle_num should between 1 and 23")
  336. }
  337. in.WeekDays = []int{}
  338. in.MonthDays = []int{}
  339. case api.TIMER_TYPE_DAY:
  340. in.WeekDays = []int{}
  341. in.MonthDays = []int{}
  342. case api.TIMER_TYPE_WEEK:
  343. if len(in.WeekDays) == 0 && in.CycleNum == 0 {
  344. return in, fmt.Errorf("week_days should not be empty")
  345. }
  346. in.MonthDays = []int{}
  347. case api.TIMER_TYPE_MONTH:
  348. if len(in.MonthDays) == 0 && in.CycleNum == 0 {
  349. return in, fmt.Errorf("month_days should not be empty")
  350. }
  351. in.WeekDays = []int{}
  352. default:
  353. return in, fmt.Errorf("unkown cycle type %s", in.CycleType)
  354. }
  355. if now.After(in.EndTime) {
  356. return in, fmt.Errorf("end_time is earlier than now")
  357. }
  358. return in, nil
  359. }