| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- // Copyright 2019 Yunion
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package models
- import (
- "context"
- "fmt"
- "sort"
- "strconv"
- "strings"
- "time"
- "yunion.io/x/log"
- api "yunion.io/x/onecloud/pkg/apis/scheduledtask"
- "yunion.io/x/onecloud/pkg/i18n"
- "yunion.io/x/onecloud/pkg/util/bitmap"
- )
- type STimer struct {
- // Cycle type
- Type string `width:"8" charset:"ascii"`
- // 0-59
- Minute int `nullable:"false"`
- // 0-23
- Hour int `nullable:"false"`
- CycleNum int `nullable:"false"`
- // 0-7 1 is Monday 0 is unlimited
- WeekDays uint8 `nullable:"false"`
- // 0-31 0 is unlimited
- MonthDays uint32 `nullable:"false"`
- // StartTime represent the start time of this timer
- StartTime time.Time
- // EndTime represent deadline of this timer
- EndTime time.Time
- // NextTime represent the time timer should bell
- NextTime time.Time `index:"true"`
- IsExpired bool
- }
- // Update will update the SScalingTimer
- func (st *STimer) Update(now time.Time) {
- if now.IsZero() {
- now = time.Now()
- }
- if !now.Before(st.EndTime) {
- st.IsExpired = true
- return
- }
- if now.Before(st.StartTime) {
- now = st.StartTime
- }
- if !st.NextTime.Before(now) {
- return
- }
- newNextTime := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, time.UTC).In(now.Location())
- if now.After(newNextTime) {
- newNextTime = newNextTime.AddDate(0, 0, 1)
- }
- switch {
- case st.WeekDays != 0:
- // week
- nowDay, weekdays := int(newNextTime.Weekday()), st.GetWeekDays()
- if nowDay == 0 {
- nowDay = 7
- }
- // weekdays[0]+7 is for the case that all time nodes has been missed in this week
- weekdays = append(weekdays, weekdays[0]+7)
- index := sort.SearchInts(weekdays, nowDay)
- newNextTime = newNextTime.AddDate(0, 0, weekdays[index]-nowDay)
- case st.MonthDays != 0:
- // month
- monthdays := st.GetMonthDays()
- suitTime := newNextTime
- for {
- day := suitTime.Day()
- index := sort.SearchInts(monthdays, day)
- if index == len(monthdays) || monthdays[index] > st.MonthDaySum(suitTime) {
- // set suitTime as the first day of next month
- suitTime = suitTime.AddDate(0, 1, -suitTime.Day()+1)
- continue
- }
- newNextTime = time.Date(suitTime.Year(), suitTime.Month(), monthdays[index], suitTime.Hour(),
- suitTime.Minute(), 0, 0, suitTime.Location())
- break
- }
- case st.CycleNum != 0:
- switch st.Type {
- case api.TIMER_TYPE_HOUR:
- newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
- if now.After(newNextTime) {
- newNextTime = newNextTime.Add(time.Duration(st.CycleNum) * time.Hour)
- }
- case api.TIMER_TYPE_DAY:
- newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
- if now.After(newNextTime) {
- newNextTime = newNextTime.AddDate(0, 0, st.CycleNum)
- }
- case api.TIMER_TYPE_WEEK:
- newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
- if now.After(newNextTime) {
- newNextTime = newNextTime.AddDate(0, 0, st.CycleNum*7)
- }
- case api.TIMER_TYPE_MONTH:
- newNextTime = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, time.UTC).In(now.Location())
- if now.After(newNextTime) {
- newNextTime = newNextTime.AddDate(0, st.CycleNum, 0)
- }
- }
- default:
- // day
- }
- log.Debugf("The final NextTime: %s", newNextTime)
- st.NextTime = newNextTime
- if st.NextTime.After(st.EndTime) {
- st.IsExpired = true
- }
- }
- // MonthDaySum calculate the number of month's days
- func (st *STimer) MonthDaySum(t time.Time) int {
- year, month := t.Year(), t.Month()
- monthDays := []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
- if month != 2 {
- return monthDays[2]
- }
- if year%4 != 0 || (year%100 == 0 && year%400 != 0) {
- return 28
- }
- return 29
- }
- func (st *STimer) GetWeekDays() []int {
- return bitmap.Uint2IntArray(uint32(st.WeekDays))
- }
- func (st *STimer) GetMonthDays() []int {
- return bitmap.Uint2IntArray(st.MonthDays)
- }
- func (st *STimer) SetWeekDays(days []int) {
- st.WeekDays = uint8(bitmap.IntArray2Uint(days))
- }
- func (st *STimer) SetMonthDays(days []int) {
- st.MonthDays = bitmap.IntArray2Uint(days)
- }
- func (st *STimer) TimerDetails() api.TimerDetails {
- return api.TimerDetails{ExecTime: st.EndTime}
- }
- func (st *STimer) CycleTimerDetails() api.CycleTimerDetails {
- out := api.CycleTimerDetails{
- Minute: st.Minute,
- Hour: st.Hour,
- WeekDays: st.GetWeekDays(),
- MonthDays: st.GetMonthDays(),
- StartTime: st.StartTime,
- EndTime: st.EndTime,
- CycleType: st.Type,
- }
- return out
- }
- func checkTimerCreateInput(in api.TimerCreateInput) (api.TimerCreateInput, error) {
- now := time.Now()
- if now.After(in.ExecTime) {
- return in, fmt.Errorf("exec_time is earlier than now")
- }
- return in, nil
- }
- var (
- timerDescTable = i18n.Table{}
- TIMERLANG = "timerLang"
- )
- func init() {
- timerDescTable.Set("timerLang", i18n.NewTableEntry().EN("en").CN("cn"))
- }
- func (st *STimer) Description(ctx context.Context, createdAt time.Time, zone *time.Location) string {
- lang := timerDescTable.Lookup(ctx, TIMERLANG)
- switch lang {
- case "en":
- return st.descEnglish(createdAt, zone)
- case "cn":
- return st.descChinese(createdAt, zone)
- }
- return ""
- }
- var (
- wdsCN = []string{"", "一", "二", "三", "四", "五", "六", "日"}
- wdsEN = []string{"", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
- )
- func (st *STimer) descChinese(createdAt time.Time, zone *time.Location) string {
- format := "2006-01-02 15:04:05"
- var prefix string
- switch st.Type {
- case api.TIMER_TYPE_ONCE:
- return fmt.Sprintf("单次 %s触发", st.StartTime.In(zone).Format(format))
- case api.TIMER_TYPE_HOUR:
- prefix = fmt.Sprintf("每%d小时", st.CycleNum)
- case api.TIMER_TYPE_DAY:
- prefix = "每天"
- if st.CycleNum > 0 {
- prefix = fmt.Sprintf("每%d天", st.CycleNum)
- }
- case api.TIMER_TYPE_WEEK:
- wds := st.GetWeekDays()
- weekDays := make([]string, len(wds))
- for i := range wds {
- weekDays[i] = fmt.Sprintf("星期%s", wdsCN[wds[i]])
- }
- prefix = fmt.Sprintf("每周 【%s】", strings.Join(weekDays, "|"))
- if st.CycleNum > 0 {
- prefix = fmt.Sprintf("每%d周", st.CycleNum)
- }
- case api.TIMER_TYPE_MONTH:
- mns := st.GetMonthDays()
- monthDays := make([]string, len(mns))
- for i := range mns {
- monthDays[i] = fmt.Sprintf("%d号", mns[i])
- }
- prefix = fmt.Sprintf("每月 【%s】", strings.Join(monthDays, "|"))
- if st.CycleNum > 0 {
- prefix = fmt.Sprintf("每%d月", st.CycleNum)
- }
- }
- if st.CycleNum > 0 {
- 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))
- }
- return fmt.Sprintf("%s %s触发 有效时间为%s至%s", prefix, st.hourMinutesDesc(zone), st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
- }
- func (st *STimer) hourMinutesDesc(zone *time.Location) string {
- now := time.Now()
- t := time.Date(now.Year(), now.Month(), now.Day(), st.Hour, st.Minute, 0, 0, time.UTC).In(zone)
- return fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute())
- }
- func (st *STimer) descEnglish(createdAt time.Time, zone *time.Location) string {
- var detail string
- format := "2006-01-02 15:04:05"
- switch st.Type {
- case api.TIMER_TYPE_ONCE:
- return st.EndTime.In(zone).Format(format)
- case api.TIMER_TYPE_HOUR:
- detail = fmt.Sprintf("every %d hours", st.CycleNum)
- case api.TIMER_TYPE_DAY:
- if st.CycleNum > 0 {
- detail = fmt.Sprintf("every %d days", st.CycleNum)
- } else {
- detail = fmt.Sprintf("%s every day", st.hourMinutesDesc(zone))
- }
- case api.TIMER_TYPE_WEEK:
- if st.CycleNum > 0 {
- detail = fmt.Sprintf("every %d weeks", st.CycleNum)
- } else {
- detail = st.weekDaysDesc(zone)
- }
- case api.TIMER_TYPE_MONTH:
- if st.CycleNum > 0 {
- detail = fmt.Sprintf("every %d months", st.CycleNum)
- } else {
- detail = st.monthDaysDesc(zone)
- }
- }
- if st.CycleNum > 0 {
- 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))
- }
- if st.EndTime.IsZero() {
- return detail
- }
- return fmt.Sprintf("%s, from %s to %s", detail, st.StartTime.In(zone).Format(format), st.EndTime.In(zone).Format(format))
- }
- func (st *STimer) weekDaysDesc(zone *time.Location) string {
- if st.WeekDays == 0 {
- return ""
- }
- var desc strings.Builder
- wds := st.GetWeekDays()
- i := 0
- desc.WriteString(fmt.Sprintf("%s every %s", st.hourMinutesDesc(zone), wdsEN[wds[i]]))
- for i++; i < len(wds)-1; i++ {
- desc.WriteString(", ")
- desc.WriteString(wdsEN[wds[i]])
- }
- if i == len(wds)-1 {
- desc.WriteString(" and ")
- desc.WriteString(wdsEN[wds[i]])
- }
- return desc.String()
- }
- func (st *STimer) monthDaysDesc(zone *time.Location) string {
- if st.MonthDays == 0 {
- return ""
- }
- var desc strings.Builder
- mds := st.GetMonthDays()
- i := 0
- desc.WriteString(fmt.Sprintf("%s on the %d%s", st.hourMinutesDesc(zone), mds[i], st.dateSuffix(mds[i])))
- for i++; i < len(mds)-1; i++ {
- desc.WriteString(", ")
- desc.WriteString(strconv.Itoa(mds[i]))
- desc.WriteString(st.dateSuffix(mds[i]))
- }
- if i == len(mds)-1 {
- desc.WriteString(" and ")
- desc.WriteString(strconv.Itoa(mds[i]))
- desc.WriteString(st.dateSuffix(mds[i]))
- }
- desc.WriteString(" of each month")
- return desc.String()
- }
- func (st *STimer) dateSuffix(date int) string {
- var ret string
- switch date {
- case 1:
- ret = "st"
- case 2:
- ret = "nd"
- case 3:
- ret = "rd"
- default:
- ret = "th"
- }
- return ret
- }
- func checkCycleTimerCreateInput(in api.CycleTimerCreateInput) (api.CycleTimerCreateInput, error) {
- now := time.Now()
- if in.Minute < 0 || in.Minute > 59 {
- return in, fmt.Errorf("minute should between 0 and 59")
- }
- if in.Hour < 0 || in.Hour > 23 {
- return in, fmt.Errorf("hour should between 0 and 23")
- }
- switch in.CycleType {
- case api.TIMER_TYPE_HOUR:
- if in.CycleNum <= 0 || in.CycleNum >= 24 {
- return in, fmt.Errorf("hour cycle_num should between 1 and 23")
- }
- in.WeekDays = []int{}
- in.MonthDays = []int{}
- case api.TIMER_TYPE_DAY:
- in.WeekDays = []int{}
- in.MonthDays = []int{}
- case api.TIMER_TYPE_WEEK:
- if len(in.WeekDays) == 0 && in.CycleNum == 0 {
- return in, fmt.Errorf("week_days should not be empty")
- }
- in.MonthDays = []int{}
- case api.TIMER_TYPE_MONTH:
- if len(in.MonthDays) == 0 && in.CycleNum == 0 {
- return in, fmt.Errorf("month_days should not be empty")
- }
- in.WeekDays = []int{}
- default:
- return in, fmt.Errorf("unkown cycle type %s", in.CycleType)
- }
- if now.After(in.EndTime) {
- return in, fmt.Errorf("end_time is earlier than now")
- }
- return in, nil
- }
|