1// SPDX-License-Identifier: ISC 2// Copyright (c) 2014-2020 Bitmark Inc. 3// Use of this source code is governed by an ISC 4// license that can be found in the LICENSE file. 5 6package main 7 8import ( 9 "fmt" 10 "reflect" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 "unicode" 16 17 "github.com/bitmark-inc/logger" 18) 19 20const ( 21 defaultNum = 0 22 defaultTimeStrErrorMsg = "clock time out of range" 23 defaultTimePeriodErrorMsg = "time period format error" 24 timePeriodSeparator = "," 25 clockSeparator = "-" 26 spaceChar = " " 27 allDayClockStr = "" 28 defaultIndex = 0 29 oneWeekDuration = time.Duration(24*7) * time.Hour 30 delayOfStartStop = time.Duration(5) * time.Second 31 jobCalendarPrefix = "calendar" 32) 33 34type JobCalendar interface { 35 PickNextStartEvent(time.Time) interface{} 36 PickNextStopEvent(time.Time) interface{} 37 PickInitialiseStartEvent(time.Time) interface{} 38 PickInitialiseStopEvent(time.Time) interface{} 39 Refresh(calendar ConfigCalendar) 40 RescheduleStartEventsPrior(event time.Time) 41 RescheduleStopEventsPrior(event time.Time) 42 RunForever() bool 43 SetLog(l *logger.L) 44} 45 46type NumberRange struct { 47 min uint32 48 max uint32 49} 50 51// collapse events, all start time put in one place, all end time put in one place 52type FlattenEvents struct { 53 start []time.Time 54 stop []time.Time 55} 56 57type SingleEvent struct { 58 start time.Time 59 stop time.Time 60} 61 62type JobCalendarData struct { 63 flattenEvents FlattenEvents 64 events map[time.Weekday][]SingleEvent 65 rawData ConfigCalendar 66 rescheduleChannel chan<- struct{} 67 log *logger.L 68} 69 70type TimeData struct { 71 hour, minute uint32 72} 73 74func newJobCalendar(channel chan<- struct{}) JobCalendar { 75 return &JobCalendarData{ 76 flattenEvents: FlattenEvents{ 77 start: []time.Time{}, 78 stop: []time.Time{}, 79 }, 80 events: map[time.Weekday][]SingleEvent{ 81 time.Sunday: {}, 82 time.Monday: {}, 83 time.Tuesday: {}, 84 time.Wednesday: {}, 85 time.Thursday: {}, 86 time.Friday: {}, 87 time.Saturday: {}, 88 }, 89 rawData: ConfigCalendar{}, 90 rescheduleChannel: channel, 91 } 92} 93 94func (j *JobCalendarData) newEmptyFlattenEvents() FlattenEvents { 95 return FlattenEvents{ 96 start: []time.Time{}, 97 stop: []time.Time{}, 98 } 99} 100 101func (j *JobCalendarData) newEmptyEvents() map[time.Weekday][]SingleEvent { 102 return map[time.Weekday][]SingleEvent{ 103 time.Sunday: {}, 104 time.Monday: {}, 105 time.Tuesday: {}, 106 time.Wednesday: {}, 107 time.Thursday: {}, 108 time.Friday: {}, 109 time.Saturday: {}, 110 } 111} 112 113func (j *JobCalendarData) SetLog(l *logger.L) { 114 j.log = l 115} 116 117func (j *JobCalendarData) RunForever() bool { 118 return 0 == len(j.flattenEvents.stop) 119} 120 121func (j *JobCalendarData) Refresh(calendar ConfigCalendar) { 122 j.log.Debug("refresh calendar") 123 if !j.isSameCalendar(calendar) { 124 j.log.Debug("calendar change") 125 j.setNewCalendar(calendar) 126 j.resetEvents() 127 j.parseRawData(calendar) 128 j.removeRedundantStopEvent() 129 j.printEvents() 130 j.notifyJobManager() 131 } 132} 133 134func (j *JobCalendarData) resetEvents() { 135 j.flattenEvents = j.newEmptyFlattenEvents() 136 j.events = j.newEmptyEvents() 137} 138 139func (j *JobCalendarData) notifyJobManager() { 140 j.log.Debug("notify manager for new calendar settings...") 141 j.rescheduleChannel <- struct{}{} 142} 143 144func (j *JobCalendarData) setNewCalendar(calendar ConfigCalendar) { 145 j.rawData = calendar 146} 147 148func (j *JobCalendarData) parseRawData(calendar ConfigCalendar) { 149 j.convertWeekScheduleToEvents() 150 j.sortFlattenEventsFromEarlier2Later() 151} 152 153func (j *JobCalendarData) removeRedundantStopEvent() { 154 j.log.Debug("removing redundant events...") 155 start := j.flattenEvents.start 156 stop := j.flattenEvents.stop 157 redundantIdx := make([]bool, len(j.flattenEvents.stop)) 158 159loop: 160 for i, k := 0, 0; i < len(start) && k < len(stop); { 161 if start[i].Equal(stop[k]) { 162 j.log.Debugf("%+v stop event is redundant", j.flattenEvents.stop[k]) 163 redundantIdx[k] = true 164 i++ 165 k++ 166 continue loop 167 } 168 if start[i].After(stop[k]) { 169 k++ 170 } else { 171 i++ 172 } 173 } 174 newSlice := make([]time.Time, 0, len(j.flattenEvents.stop)) 175 for i := 0; i < len(redundantIdx); i++ { 176 if !redundantIdx[i] { 177 newSlice = append(newSlice, stop[i]) 178 } 179 } 180 j.flattenEvents.stop = newSlice 181} 182 183func isEventAlreadyExist(times []time.Time, event time.Time) (bool, int) { 184 if 0 == len(times) { 185 return false, defaultIndex 186 } 187 for i, v := range times { 188 if v == event { 189 return true, i 190 } 191 } 192 return false, defaultIndex 193} 194 195func isSameTime(t1 TimeData, t2 TimeData) bool { 196 if t1.hour == t2.hour && t1.minute == t2.minute { 197 return true 198 } 199 return false 200} 201 202func isTimeDataFirstEarlierThanSecond(first TimeData, second TimeData) bool { 203 if first.hour < second.hour { 204 return true 205 } 206 if first.minute < second.minute { 207 return true 208 } 209 return false 210} 211 212func (j *JobCalendarData) isTimeBooked(event time.Time) bool { 213 weekDay := event.Weekday() 214 events := j.events[weekDay] 215 216 for _, t := range events { 217 afterOrEqualToStartTime := t.start.Before(event) || t.start.Equal(event) 218 beforeOrEqualToEndTime := t.stop.After(event) || t.stop.Equal(event) 219 220 if afterOrEqualToStartTime && beforeOrEqualToEndTime { 221 return true 222 } 223 224 if events[0].stop.IsZero() && afterOrEqualToStartTime { 225 return true 226 } 227 } 228 return false 229} 230 231func (j *JobCalendarData) PickInitialiseStartEvent(event time.Time) interface{} { 232 if j.isTimeBooked(event) { 233 j.log.Debugf("working time, start after %s", delayOfStartStop.String()) 234 return event.Add(delayOfStartStop) 235 } 236 return j.PickNextStartEvent(event) 237} 238 239func (j *JobCalendarData) PickInitialiseStopEvent(event time.Time) interface{} { 240 if j.isTimeBooked(event) { 241 return j.PickNextStopEvent(event) 242 } 243 j.log.Debugf("not working time, stop after %s", delayOfStartStop.String()) 244 return event.Add(delayOfStartStop) 245} 246 247func (j *JobCalendarData) PickNextStartEvent(event time.Time) interface{} { 248 for _, e := range j.flattenEvents.start { 249 if e.After(event) { 250 j.log.Infof("next start event at %s", e) 251 return e 252 } 253 } 254 j.log.Error("cannot find next start event") 255 j.printEvents() 256 return nil 257} 258 259func (j *JobCalendarData) PickNextStopEvent(event time.Time) interface{} { 260 for _, e := range j.flattenEvents.stop { 261 if e.After(event) { 262 j.log.Infof("next stop event at %s", e) 263 return e 264 } 265 } 266 j.log.Info("cannot find next stop event") 267 j.printEvents() 268 return nil 269} 270 271func (j *JobCalendarData) RescheduleStartEventsPrior(event time.Time) { 272 if 0 == len(j.flattenEvents.start) || j.flattenEvents.start[0].After(event) { 273 return 274 } 275 times := j.flattenEvents.start 276 newSlices := make([]time.Time, 0, len(times)) 277 schedules := make([]time.Time, 0, len(times)) 278loop: 279 for i, t := range times { 280 if t.Before(event) || t.Equal(event) { 281 schedules = append(schedules, t.Add(oneWeekDuration)) 282 } else { 283 newSlices = append(newSlices, times[i:]...) 284 break loop 285 } 286 } 287 newSlices = append(newSlices, schedules...) 288 j.flattenEvents.start = newSlices 289} 290 291func (j *JobCalendarData) RescheduleStopEventsPrior(event time.Time) { 292 if 0 == len(j.flattenEvents.stop) || j.flattenEvents.stop[0].After(event) { 293 return 294 } 295 times := j.flattenEvents.stop 296 newSlices := make([]time.Time, 0, len(times)) 297 schedules := make([]time.Time, 0, len(times)) 298loop: 299 for i, t := range times { 300 if t.Before(event) || t.Equal(event) { 301 schedules = append(schedules, t.Add(oneWeekDuration)) 302 } else { 303 newSlices = append(newSlices, times[i:]...) 304 break loop 305 } 306 } 307 newSlices = append(newSlices, schedules...) 308 j.flattenEvents.stop = newSlices 309} 310 311func (j *JobCalendarData) removeEventFrom(times []time.Time, event time.Time) ([]time.Time, error) { 312 exist, idx := isEventAlreadyExist(times, event) 313 if !exist { 314 return times, nil 315 } 316 return append(times[:idx], times[idx+1:]...), nil 317} 318 319func (j *JobCalendarData) weekDayCurrent2Target(current time.Weekday, target time.Weekday) int { 320 return int(target) - int(current) 321} 322 323func (j *JobCalendarData) parseClockStr(clock string) (TimeData, error) { 324 if clock == "24:00" || clock == "24:0" { 325 return TimeData{ 326 hour: uint32(24), 327 minute: uint32(0), 328 }, nil 329 } 330 331 t, err := time.Parse("15:04", clock) 332 if nil != err { 333 j.log.Errorf("%s\n", err.Error()) 334 return TimeData{}, err 335 } 336 return TimeData{ 337 hour: uint32(t.Hour()), 338 minute: uint32(t.Minute()), 339 }, nil 340} 341 342func (j *JobCalendarData) convertStr2NumberWithLimit(str string, numRange NumberRange) (uint32, error) { 343 num, err := strconv.Atoi(str) 344 if err != nil || uint32(num) < numRange.min || uint32(num) > numRange.max { 345 return defaultNum, fmt.Errorf(defaultTimeStrErrorMsg) 346 } 347 return uint32(num), nil 348} 349 350// period: 2:12 - 3:14 351// clock: 2:12 352func (j *JobCalendarData) parseTimePeriod(period string) (TimeData, TimeData, error) { 353 str := strings.ReplaceAll(period, spaceChar, "") 354 clocks := strings.Split(str, clockSeparator) 355 if len(clocks) > 2 { 356 return TimeData{}, TimeData{}, fmt.Errorf(defaultTimePeriodErrorMsg) 357 } 358 timeFirst, err := j.parseClockStr(clocks[0]) 359 if nil != err { 360 return TimeData{}, TimeData{}, fmt.Errorf(defaultTimePeriodErrorMsg) 361 } 362 timeSecond, err := j.parseClockStr(clocks[1]) 363 if nil != err { 364 return TimeData{}, TimeData{}, fmt.Errorf(defaultTimePeriodErrorMsg) 365 } 366 367 if isTimeDataFirstEarlierThanSecond(timeFirst, timeSecond) { 368 return timeFirst, timeSecond, nil 369 } 370 371 return timeSecond, timeFirst, nil 372} 373 374func (j *JobCalendarData) timeByWeekdayAndOffset(day time.Weekday, clock TimeData) time.Time { 375 now := time.Now() 376 dayDiffNum := j.weekDayCurrent2Target(now.Weekday(), day) 377 return time.Date(now.Year(), now.Month(), now.Day()+dayDiffNum, 378 int(clock.hour), int(clock.minute), 0, 0, now.Location()) 379} 380 381func (j *JobCalendarData) timeOfWeekdayStartFromBeginning(day time.Weekday) FlattenEvents { 382 flattenEvents := FlattenEvents{ 383 start: []time.Time{j.timeByWeekdayAndOffset(day, TimeData{hour: 0, minute: 0})}, 384 stop: []time.Time{}, 385 } 386 return flattenEvents 387} 388 389func (j *JobCalendarData) sortFlattenEventsFromEarlier2Later() { 390 j.log.Debug("sort events") 391 events := j.flattenEvents 392 sort.Slice(events.start, func(i, j int) bool { 393 return events.start[i].Before(events.start[j]) 394 }) 395 sort.Slice(events.stop, func(i, j int) bool { 396 return events.stop[i].Before(events.stop[j]) 397 }) 398} 399 400// TODO: refactor to use for loop, currently no idea how to use code for 401// time.Sunday & time.rawData.Sunday 402func (j *JobCalendarData) convertWeekScheduleToEvents() { 403 j.convertDayScheduleToEvents(time.Sunday, j.rawData.Sunday) 404 j.convertDayScheduleToEvents(time.Monday, j.rawData.Monday) 405 j.convertDayScheduleToEvents(time.Tuesday, j.rawData.Tuesday) 406 j.convertDayScheduleToEvents(time.Wednesday, j.rawData.Wednesday) 407 j.convertDayScheduleToEvents(time.Thursday, j.rawData.Thursday) 408 j.convertDayScheduleToEvents(time.Friday, j.rawData.Friday) 409 j.convertDayScheduleToEvents(time.Saturday, j.rawData.Saturday) 410} 411 412func (j *JobCalendarData) convertDayScheduleToEvents(day time.Weekday, clock string) { 413 if allDayClockStr == strings.Trim(clock, spaceChar) { 414 j.log.Debugf("%s work all day", day.String()) 415 j.scheduleStartEventWhenDayBegin(day) 416 return 417 } 418 j.scheduleEvents(day, clock) 419} 420 421func containsLetter(s string) bool { 422 for _, c := range s { 423 if unicode.IsLetter(c) { 424 return true 425 } 426 } 427 return false 428} 429 430func (j *JobCalendarData) isValidPeriod(str string) bool { 431 s := strings.Split(str, clockSeparator) 432 if len(s) != 2 { 433 j.log.Errorf("invalid caledar string %s, contains too many clock string", str) 434 return false 435 } 436 t1 := strings.Trim(s[0], spaceChar) 437 t2 := strings.Trim(s[1], spaceChar) 438 if t1 == t2 { 439 j.log.Errorf("invalid caledar string %s, 2 clock strings equal", str) 440 return false 441 } 442 443 if containsLetter(t1) || containsLetter(t2) { 444 j.log.Errorf("invalid caledar string %s, contains letter", str) 445 return false 446 } 447 448 return true 449} 450 451func (j *JobCalendarData) scheduleEvents(day time.Weekday, clock string) { 452 periods := strings.Split(clock, timePeriodSeparator) 453 events := make([]SingleEvent, 0) 454 flattenEvents := FlattenEvents{ 455 start: []time.Time{}, 456 stop: []time.Time{}, 457 } 458 459loop: 460 for _, period := range periods { 461 if !j.isValidPeriod(period) { 462 continue loop 463 } 464 t1, t2, err := j.parseTimePeriod(period) 465 if nil != err { 466 j.log.Errorf("error parse time period %s, error: %s", period, err) 467 continue loop 468 } 469 events = append(events, SingleEvent{ 470 start: j.timeByWeekdayAndOffset(day, t1), 471 stop: j.timeByWeekdayAndOffset(day, t2), 472 }) 473 flattenEvents.start = append( 474 flattenEvents.start, 475 j.timeByWeekdayAndOffset(day, t1), 476 ) 477 flattenEvents.stop = append( 478 flattenEvents.stop, 479 j.timeByWeekdayAndOffset(day, t2), 480 ) 481 } 482 483 if 0 == len(flattenEvents.start) { 484 j.log.Debugf("empty flatten start event, add start event to day start") 485 j.scheduleStartEventWhenDayBegin(day) 486 return 487 } 488 489 j.events[day] = events 490 j.flattenEvents.start = append(j.flattenEvents.start, flattenEvents.start...) 491 j.flattenEvents.stop = append(j.flattenEvents.stop, flattenEvents.stop...) 492} 493 494func (j *JobCalendarData) scheduleStartEventWhenDayBegin(day time.Weekday) { 495 flattenEvent := j.timeOfWeekdayStartFromBeginning(day) 496 j.flattenEvents.start = append(j.flattenEvents.start, flattenEvent.start[0]) 497 j.events[day] = []SingleEvent{ 498 {start: flattenEvent.start[0]}, 499 } 500} 501 502func (j *JobCalendarData) isSameCalendar(new ConfigCalendar) bool { 503 equal := reflect.DeepEqual(j.rawData, new) 504 if !equal { 505 j.log.Debug("config changed") 506 j.log.Debugf("previous: %+v", j.rawData) 507 j.log.Debugf("new: %+v", new) 508 } 509 return equal 510} 511 512func (j *JobCalendarData) printEvents() { 513 weekdays := []time.Weekday{ 514 time.Sunday, 515 time.Monday, 516 time.Tuesday, 517 time.Wednesday, 518 time.Thursday, 519 time.Friday, 520 time.Saturday, 521 } 522 523 for _, d := range weekdays { 524 j.log.Debugf("%d start flattenEvents: %+v", 525 len(j.flattenEvents.start), 526 j.flattenEvents.start, 527 ) 528 j.log.Debugf("%d stop flattenEvents: %+v", 529 len(j.flattenEvents.stop), 530 j.flattenEvents.stop, 531 ) 532 j.log.Debugf("%d events on %s", len(j.events[d]), d.String()) 533 for _, e := range j.events[d] { 534 if e.stop.IsZero() { 535 j.log.Debugf("%s: start: %s", 536 d.String(), 537 e.start, 538 ) 539 } else { 540 j.log.Debugf("%s: start: %s, end: %s", 541 d.String(), 542 e.start, 543 e.stop, 544 ) 545 } 546 } 547 } 548} 549