timer.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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/compute"
  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. // 0-7 1 is Monday 0 is unlimited
  35. WeekDays uint8 `nullable:"false"`
  36. // 0-31 0 is unlimited
  37. MonthDays uint32 `nullable:"false"`
  38. // StartTime represent the start time of this timer
  39. StartTime time.Time
  40. // EndTime represent deadline of this timer
  41. EndTime time.Time
  42. // NextTime represent the time timer should bell
  43. NextTime time.Time `index:"true"`
  44. IsExpired bool
  45. }
  46. // Update will update the SScalingTimer
  47. func (st *STimer) Update(now time.Time) {
  48. if now.IsZero() {
  49. now = time.Now()
  50. }
  51. if !now.Before(st.EndTime) {
  52. st.IsExpired = true
  53. return
  54. }
  55. if now.Before(st.StartTime) {
  56. now = st.StartTime
  57. }
  58. if !st.NextTime.Before(now) {
  59. return
  60. }
  61. newNextTime := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, time.UTC).In(now.Location())
  62. if now.After(newNextTime) {
  63. newNextTime = newNextTime.AddDate(0, 0, 1)
  64. }
  65. switch {
  66. case st.WeekDays != 0:
  67. // week
  68. nowDay, weekdays := int(newNextTime.Weekday()), st.GetWeekDays()
  69. if nowDay == 0 {
  70. nowDay = 7
  71. }
  72. // weekdays[0]+7 is for the case that all time nodes has been missed in this week
  73. weekdays = append(weekdays, weekdays[0]+7)
  74. index := sort.SearchInts(weekdays, nowDay)
  75. newNextTime = newNextTime.AddDate(0, 0, weekdays[index]-nowDay)
  76. case st.MonthDays != 0:
  77. // month
  78. monthdays := st.GetMonthDays()
  79. suitTime := newNextTime
  80. for {
  81. day := suitTime.Day()
  82. index := sort.SearchInts(monthdays, day)
  83. if index == len(monthdays) || monthdays[index] > st.MonthDaySum(suitTime) {
  84. // set suitTime as the first day of next month
  85. suitTime = suitTime.AddDate(0, 1, -suitTime.Day()+1)
  86. continue
  87. }
  88. newNextTime = time.Date(suitTime.Year(), suitTime.Month(), monthdays[index], suitTime.Hour(),
  89. suitTime.Minute(), 0, 0, suitTime.Location())
  90. break
  91. }
  92. default:
  93. // day
  94. }
  95. log.Debugf("The final NextTime: %s", newNextTime)
  96. st.NextTime = newNextTime
  97. if st.NextTime.After(st.EndTime) {
  98. st.IsExpired = true
  99. }
  100. }
  101. // MonthDaySum calculate the number of month's days
  102. func (st *STimer) MonthDaySum(t time.Time) int {
  103. year, month := t.Year(), t.Month()
  104. monthDays := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
  105. if month != 2 {
  106. return monthDays[2]
  107. }
  108. if year%4 != 0 || (year%100 == 0 && year%400 != 0) {
  109. return 28
  110. }
  111. return 29
  112. }
  113. func (st *STimer) GetWeekDays() []int {
  114. return bitmap.Uint2IntArray(uint32(st.WeekDays))
  115. }
  116. func (st *STimer) GetMonthDays() []int {
  117. return bitmap.Uint2IntArray(st.MonthDays)
  118. }
  119. func (st *STimer) SetWeekDays(days []int) {
  120. st.WeekDays = uint8(bitmap.IntArray2Uint(days))
  121. }
  122. func (st *STimer) SetMonthDays(days []int) {
  123. st.MonthDays = bitmap.IntArray2Uint(days)
  124. }
  125. func (st *STimer) TimerDetails() api.TimerDetails {
  126. return api.TimerDetails{ExecTime: st.EndTime}
  127. }
  128. func (st *STimer) CycleTimerDetails() api.CycleTimerDetails {
  129. out := api.CycleTimerDetails{
  130. Minute: st.Minute,
  131. Hour: st.Hour,
  132. WeekDays: st.GetWeekDays(),
  133. MonthDays: st.GetMonthDays(),
  134. StartTime: st.StartTime,
  135. EndTime: st.EndTime,
  136. CycleType: st.Type,
  137. }
  138. return out
  139. }
  140. func checkTimerCreateInput(in api.TimerCreateInput) (api.TimerCreateInput, error) {
  141. now := time.Now()
  142. if now.After(in.ExecTime) {
  143. return in, fmt.Errorf("exec_time is earlier than now")
  144. }
  145. return in, nil
  146. }
  147. var (
  148. timerDescTable = i18n.Table{}
  149. TIMERLANG = "timerLang"
  150. )
  151. func init() {
  152. timerDescTable.Set("timerLang", i18n.NewTableEntry().EN("en").CN("cn"))
  153. }
  154. func (st *STimer) Description(ctx context.Context) string {
  155. lang := timerDescTable.Lookup(ctx, TIMERLANG)
  156. switch lang {
  157. case "en":
  158. return st.descEnglish()
  159. case "cn":
  160. return st.descChinese()
  161. }
  162. return ""
  163. }
  164. var (
  165. wdsCN = []string{"", "一", "二", "三", "四", "五", "六", "日"}
  166. wdsEN = []string{"", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
  167. zone = time.Now().Local().Location()
  168. //zone = time.FixedZone("GMT", 8*3600)
  169. )
  170. func (st *STimer) descChinese() string {
  171. format := "2006-01-02 15:04:05"
  172. var prefix string
  173. switch st.Type {
  174. case api.TIMER_TYPE_ONCE:
  175. return fmt.Sprintf("单次 %s触发", st.StartTime.In(zone).Format(format))
  176. case api.TIMER_TYPE_DAY:
  177. prefix = "每天"
  178. case api.TIMER_TYPE_WEEK:
  179. wds := st.GetWeekDays()
  180. weekDays := make([]string, len(wds))
  181. for i := range wds {
  182. weekDays[i] = fmt.Sprintf("星期%s", wdsCN[wds[i]])
  183. }
  184. prefix = fmt.Sprintf("每周 【%s】", strings.Join(weekDays, "|"))
  185. case api.TIMER_TYPE_MONTH:
  186. mns := st.GetMonthDays()
  187. monthDays := make([]string, len(mns))
  188. for i := range mns {
  189. monthDays[i] = fmt.Sprintf("%d号", mns[i])
  190. }
  191. prefix = fmt.Sprintf("每月 【%s】", strings.Join(monthDays, "|"))
  192. }
  193. return fmt.Sprintf("%s %02d:%02d触发 有效时间为%s至%s", prefix, st.Hour, st.Minute, st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
  194. }
  195. func (st *STimer) descEnglish() string {
  196. var detail string
  197. format := "2006-01-02 15:04:05"
  198. switch st.Type {
  199. case api.TIMER_TYPE_ONCE:
  200. return st.EndTime.In(zone).Format(format)
  201. case api.TIMER_TYPE_DAY:
  202. detail = fmt.Sprintf("%d:%d every day", st.Hour, st.Minute)
  203. case api.TIMER_TYPE_WEEK:
  204. detail = st.weekDaysDesc()
  205. case api.TIMER_TYPE_MONTH:
  206. detail = st.monthDaysDesc()
  207. }
  208. if st.EndTime.IsZero() {
  209. return detail
  210. }
  211. return fmt.Sprintf("%s, from %s to %s", detail, st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
  212. }
  213. func (st *STimer) weekDaysDesc() string {
  214. if st.WeekDays == 0 {
  215. return ""
  216. }
  217. var desc strings.Builder
  218. wds := st.GetWeekDays()
  219. i := 0
  220. desc.WriteString(fmt.Sprintf("%d:%d every %s", st.Hour, st.Minute, wdsEN[wds[i]]))
  221. for i++; i < len(wds)-1; i++ {
  222. desc.WriteString(", ")
  223. desc.WriteString(wdsEN[wds[i]])
  224. }
  225. if i == len(wds)-1 {
  226. desc.WriteString(" and ")
  227. desc.WriteString(wdsEN[wds[i]])
  228. }
  229. return desc.String()
  230. }
  231. func (st *STimer) monthDaysDesc() string {
  232. if st.MonthDays == 0 {
  233. return ""
  234. }
  235. var desc strings.Builder
  236. mds := st.GetMonthDays()
  237. i := 0
  238. desc.WriteString(fmt.Sprintf("%d:%d on the %d%s", st.Hour, st.Minute, mds[i], st.dateSuffix(mds[i])))
  239. for i++; i < len(mds)-1; i++ {
  240. desc.WriteString(", ")
  241. desc.WriteString(strconv.Itoa(mds[i]))
  242. desc.WriteString(st.dateSuffix(mds[i]))
  243. }
  244. if i == len(mds)-1 {
  245. desc.WriteString(" and ")
  246. desc.WriteString(strconv.Itoa(mds[i]))
  247. desc.WriteString(st.dateSuffix(mds[i]))
  248. }
  249. desc.WriteString(" of each month")
  250. return desc.String()
  251. }
  252. func (st *STimer) dateSuffix(date int) string {
  253. var ret string
  254. switch date {
  255. case 1:
  256. ret = "st"
  257. case 2:
  258. ret = "nd"
  259. case 3:
  260. ret = "rd"
  261. default:
  262. ret = "th"
  263. }
  264. return ret
  265. }
  266. func checkCycleTimerCreateInput(in api.CycleTimerCreateInput) (api.CycleTimerCreateInput, error) {
  267. now := time.Now()
  268. if in.Minute < 0 || in.Minute > 59 {
  269. return in, fmt.Errorf("minute should between 0 and 59")
  270. }
  271. if in.Hour < 0 || in.Hour > 23 {
  272. return in, fmt.Errorf("hour should between 0 and 23")
  273. }
  274. switch in.CycleType {
  275. case api.TIMER_TYPE_HOUR:
  276. if in.CycleHour <= 0 || in.CycleHour >= 24 {
  277. return in, fmt.Errorf("cycle_hour should between 0 and 23")
  278. }
  279. in.WeekDays = []int{}
  280. in.MonthDays = []int{}
  281. case api.TIMER_TYPE_DAY:
  282. in.WeekDays = []int{}
  283. in.MonthDays = []int{}
  284. case api.TIMER_TYPE_WEEK:
  285. if len(in.WeekDays) == 0 {
  286. return in, fmt.Errorf("week_days should not be empty")
  287. }
  288. in.MonthDays = []int{}
  289. case api.TIMER_TYPE_MONTH:
  290. if len(in.MonthDays) == 0 {
  291. return in, fmt.Errorf("month_days should not be empty")
  292. }
  293. in.WeekDays = []int{}
  294. default:
  295. return in, fmt.Errorf("unkown cycle type %s", in.CycleType)
  296. }
  297. if now.After(in.EndTime) {
  298. return in, fmt.Errorf("end_time is earlier than now")
  299. }
  300. return in, nil
  301. }