diff --git a/CHANGELOG.md b/CHANGELOG.md index 05abc97f1..b4ad0ac59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Added: Fixes: * Fixed an issue with filtering from results files in Race Weekends when the Race Weekend is within a Championship. +* We've changed the method that we use to check if scheduled events need running. Hopefully this should make scheduled events more reliable! --- diff --git a/championship_manager.go b/championship_manager.go index 313cdbfef..9288f4bed 100644 --- a/championship_manager.go +++ b/championship_manager.go @@ -16,6 +16,7 @@ import ( "time" "github.com/cj123/assetto-server-manager/pkg/udp" + "github.com/cj123/assetto-server-manager/pkg/when" "github.com/cj123/caldav-go/icalendar" "github.com/cj123/caldav-go/icalendar/components" @@ -32,8 +33,8 @@ type ChampionshipManager struct { activeChampionship *ActiveChampionship mutex sync.Mutex - championshipEventStartTimers map[string]*time.Timer - championshipEventReminderTimers map[string]*time.Timer + championshipEventStartTimers map[string]*when.Timer + championshipEventReminderTimers map[string]*when.Timer } func NewChampionshipManager(raceManager *RaceManager) *ChampionshipManager { @@ -658,8 +659,6 @@ func (cm *ChampionshipManager) ScheduleEvent(championshipID string, eventID stri } // add a scheduled event on date - duration := time.Until(date) - if recurrence != "already-set" { if recurrence != "" { err := event.SetRecurrenceRule(recurrence) @@ -683,7 +682,7 @@ func (cm *ChampionshipManager) ScheduleEvent(championshipID string, eventID stri } } - cm.championshipEventStartTimers[event.ID.String()] = time.AfterFunc(duration, func() { + cm.championshipEventStartTimers[event.ID.String()], err = when.When(date, func() { err := cm.StartScheduledEvent(championship, event) if err != nil { @@ -691,12 +690,15 @@ func (cm *ChampionshipManager) ScheduleEvent(championshipID string, eventID stri } }) + if err != nil { + return err + } + if cm.notificationManager.HasNotificationReminders() { for _, timer := range cm.notificationManager.GetNotificationReminders() { - duration = time.Until(date.Add(time.Duration(0-timer) * time.Minute)) thisTimer := timer - cm.championshipEventReminderTimers[event.ID.String()] = time.AfterFunc(duration, func() { + cm.championshipEventReminderTimers[event.ID.String()], err = when.When(date.Add(time.Duration(0-timer)*time.Minute), func() { cm.notificationManager.SendChampionshipReminderMessage(championship, event, thisTimer) }) } @@ -1602,8 +1604,8 @@ func (cm *ChampionshipManager) HandleChampionshipSignUp(r *http.Request) (respon } func (cm *ChampionshipManager) InitScheduledChampionships() error { - cm.championshipEventStartTimers = make(map[string]*time.Timer) - cm.championshipEventReminderTimers = make(map[string]*time.Timer) + cm.championshipEventStartTimers = make(map[string]*when.Timer) + cm.championshipEventReminderTimers = make(map[string]*when.Timer) championships, err := cm.ListChampionships() if err != nil { @@ -1622,9 +1624,7 @@ func (cm *ChampionshipManager) InitScheduledChampionships() error { if event.Scheduled.After(time.Now()) { // add a scheduled event on date - duration := time.Until(event.Scheduled) - - cm.championshipEventStartTimers[event.ID.String()] = time.AfterFunc(duration, func() { + cm.championshipEventStartTimers[event.ID.String()], err = when.When(event.Scheduled, func() { err := cm.StartScheduledEvent(championship, event) if err != nil { @@ -1632,16 +1632,25 @@ func (cm *ChampionshipManager) InitScheduledChampionships() error { } }) + if err != nil { + logrus.WithError(err).Errorf("Could not schedule event: %s", event.ID.String()) + continue + } + if cm.notificationManager.HasNotificationReminders() { for _, timer := range cm.notificationManager.GetNotificationReminders() { if event.Scheduled.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { // add reminder - duration = time.Until(event.Scheduled.Add(time.Duration(0-timer) * time.Minute)) thisTimer := timer - cm.championshipEventReminderTimers[event.ID.String()] = time.AfterFunc(duration, func() { + cm.championshipEventReminderTimers[event.ID.String()], err = when.When(event.Scheduled.Add(time.Duration(0-timer)*time.Minute), func() { cm.notificationManager.SendChampionshipReminderMessage(championship, event, thisTimer) }) + + if err != nil { + logrus.WithError(err).Errorf("Could not schedule event: %s", event.ID.String()) + continue + } } } } diff --git a/championships_handler.go b/championships_handler.go index e727d9eb5..26fa5e8c8 100644 --- a/championships_handler.go +++ b/championships_handler.go @@ -8,6 +8,7 @@ import ( "sort" "time" + "4d63.com/tz" "github.com/go-chi/chi" "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -368,7 +369,7 @@ func (ch *ChampionshipsHandler) scheduleEvent(w http.ResponseWriter, r *http.Req timeString := r.FormValue("event-schedule-time") timezone := r.FormValue("event-schedule-timezone") - location, err := time.LoadLocation(timezone) + location, err := tz.LoadLocation(timezone) if err != nil { logrus.WithError(err).Errorf("could not find location: %s", location) diff --git a/cmd/server-manager/typescript/sass/_calendar.scss b/cmd/server-manager/typescript/sass/_calendar.scss index 906d8caec..c6170d358 100644 --- a/cmd/server-manager/typescript/sass/_calendar.scss +++ b/cmd/server-manager/typescript/sass/_calendar.scss @@ -33,4 +33,8 @@ .pl-4-5 { padding-left: 2rem!important; -} \ No newline at end of file +} + +.fc-today { + background: #9ec6f5; +} diff --git a/go.mod b/go.mod index 8f3c7e8e3..268084e01 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/cj123/assetto-server-manager require ( + 4d63.com/tz v1.1.0 github.com/Clinet/discordgo-embed v0.0.0-20190411043415-d754bc1a576c github.com/Masterminds/goutils v1.1.0 // indirect github.com/Masterminds/semver v1.4.2 diff --git a/go.sum b/go.sum index de6abeb8a..55396c9ae 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +4d63.com/embedfiles v0.0.0-20190311033909-995e0740726f h1:oyYjGRBNq1TxAIG8aHqtxlvqUfzdZf+MbcRb/oweNfY= +4d63.com/embedfiles v0.0.0-20190311033909-995e0740726f/go.mod h1:HxEsUxoVZyRxsZML/S6e2xAuieFMlGO0756ncWx1aXE= +4d63.com/tz v1.1.0 h1:Hi58WbeFjiUH4XOWuCpl5iSzuUuw1axZzTqIfMKPKrg= +4d63.com/tz v1.1.0/go.mod h1:SHGqVdL7hd2ZaX2T9uEiOZ/OFAUfCCLURdLPJsd8ZNs= github.com/Clinet/discordgo-embed v0.0.0-20190411043415-d754bc1a576c h1:XB4X3MWxiq+Tb0lmc6CY1S9t5sJG1zFCrfpGQuKEGFc= github.com/Clinet/discordgo-embed v0.0.0-20190411043415-d754bc1a576c/go.mod h1:0ydUl+01209LCyzJk68BeRtCN1IMrNJgX4IBmwmC1f8= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= diff --git a/pkg/when/when.go b/pkg/when/when.go new file mode 100644 index 000000000..32256f2f6 --- /dev/null +++ b/pkg/when/when.go @@ -0,0 +1,78 @@ +package when + +import ( + "errors" + "sync" + "time" + + "github.com/sirupsen/logrus" +) + +var ( + // Resolution is how often each When is checked. + Resolution = time.Second + + timers = make(map[*Timer]bool) + mutex = sync.Mutex{} + once = sync.Once{} +) + +type Timer struct { + fn func() + t time.Time +} + +func newTimer(t time.Time, fn func()) *Timer { + r := &Timer{ + t: t, + fn: fn, + } + + return r +} + +func (t *Timer) Stop() { + mutex.Lock() + delete(timers, t) + mutex.Unlock() +} + +var ErrTimeInPast = errors.New("when: time specified is in the past") + +func When(t time.Time, fn func()) (*Timer, error) { + if t.Before(time.Now()) { + return nil, ErrTimeInPast + } + + once.Do(func() { + go func() { + ticker := time.NewTicker(Resolution) + + for tick := range ticker.C { + var toStop []*Timer + + mutex.Lock() + for timer := range timers { + if tick.Round(Resolution).Equal(timer.t.Round(Resolution)) || tick.Round(Resolution).After(timer.t.Round(Resolution)) { + logrus.Debugf("Starting scheduled event (is now %s)", timer.t) + go timer.fn() + toStop = append(toStop, timer) + } + } + mutex.Unlock() + + for _, timer := range toStop { + timer.Stop() + } + } + }() + }) + + x := newTimer(t, fn) + + mutex.Lock() + timers[x] = true + mutex.Unlock() + + return x, nil +} diff --git a/race_custom.go b/race_custom.go index 959616da8..dbe3561e9 100644 --- a/race_custom.go +++ b/race_custom.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "4d63.com/tz" "github.com/go-chi/chi" "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -182,7 +183,7 @@ func (crh *CustomRaceHandler) schedule(w http.ResponseWriter, r *http.Request) { timeString := r.FormValue("event-schedule-time") timezone := r.FormValue("event-schedule-timezone") - location, err := time.LoadLocation(timezone) + location, err := tz.LoadLocation(timezone) if err != nil { logrus.WithError(err).Errorf("could not find location: %s", location) diff --git a/race_manager.go b/race_manager.go index 88b42d68f..cfd3bb862 100644 --- a/race_manager.go +++ b/race_manager.go @@ -13,7 +13,9 @@ import ( "time" "github.com/cj123/assetto-server-manager/pkg/udp" + "github.com/cj123/assetto-server-manager/pkg/when" + "4d63.com/tz" "github.com/etcd-io/bbolt" "github.com/go-chi/chi" "github.com/google/uuid" @@ -39,8 +41,8 @@ type RaceManager struct { loopedRaceWaitForSecondRace bool // scheduled races - customRaceStartTimers map[string]*time.Timer - customRaceReminderTimers map[string]*time.Timer + customRaceStartTimers map[string]*when.Timer + customRaceReminderTimers map[string]*when.Timer } func NewRaceManager( @@ -680,7 +682,7 @@ func (rm *RaceManager) SetupCustomRace(r *http.Request) error { timeString := r.FormValue("CustomRaceScheduledTime") timezone := r.FormValue("CustomRaceScheduledTimezone") - location, err := time.LoadLocation(timezone) + location, err := tz.LoadLocation(timezone) if err != nil { logrus.WithError(err).Errorf("could not find location: %s", location) @@ -1068,8 +1070,6 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, } // add a scheduled event on date - duration := time.Until(race.Scheduled) - if recurrence != "already-set" { if recurrence != "" { err := race.SetRecurrenceRule(recurrence) @@ -1093,7 +1093,7 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, } } - rm.customRaceStartTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + rm.customRaceStartTimers[race.UUID.String()], err = when.When(race.Scheduled, func() { err := rm.StartScheduledRace(race) if err != nil { @@ -1101,16 +1101,23 @@ func (rm *RaceManager) ScheduleRace(uuid string, date time.Time, action string, } }) + if err != nil { + return err + } + if rm.notificationManager.HasNotificationReminders() { _ = rm.notificationManager.SendRaceScheduledMessage(race, race.Scheduled) for _, timer := range rm.notificationManager.GetNotificationReminders() { - duration = time.Until(race.Scheduled.Add(time.Duration(0-timer) * time.Minute)) thisTimer := timer - rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + rm.customRaceReminderTimers[race.UUID.String()], err = when.When(race.Scheduled.Add(time.Duration(0-timer)*time.Minute), func() { _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) }) + + if err != nil { + logrus.WithError(err).Error("Could not set up race reminder timer") + } } } @@ -1404,8 +1411,8 @@ func (rm *RaceManager) clearLoopedRaceSessionTypes() { } func (rm *RaceManager) InitScheduledRaces() error { - rm.customRaceStartTimers = make(map[string]*time.Timer) - rm.customRaceReminderTimers = make(map[string]*time.Timer) + rm.customRaceStartTimers = make(map[string]*when.Timer) + rm.customRaceReminderTimers = make(map[string]*when.Timer) races, err := rm.store.ListCustomRaces() @@ -1422,9 +1429,7 @@ func (rm *RaceManager) InitScheduledRaces() error { if race.Scheduled.After(time.Now()) { // add a scheduled event on date - duration := time.Until(race.Scheduled) - - rm.customRaceStartTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + rm.customRaceStartTimers[race.UUID.String()], err = when.When(race.Scheduled, func() { err := rm.StartScheduledRace(race) if err != nil { @@ -1432,16 +1437,23 @@ func (rm *RaceManager) InitScheduledRaces() error { } }) + if err != nil { + logrus.WithError(err).Error("Could not set up scheduled race timer") + } + if rm.notificationManager.HasNotificationReminders() { for _, timer := range rm.notificationManager.GetNotificationReminders() { if race.Scheduled.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { // add reminder - duration = time.Until(race.Scheduled.Add(time.Duration(0-timer) * time.Minute)) thisTimer := timer - rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { + rm.customRaceReminderTimers[race.UUID.String()], err = when.When(race.Scheduled.Add(time.Duration(0-timer)*time.Minute), func() { _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) }) + + if err != nil { + logrus.WithError(err).Error("Could not set up scheduled race reminder timer") + } } } } @@ -1492,7 +1504,7 @@ func (rm *RaceManager) RescheduleNotifications(oldServerOpts *GlobalServerConfig } // rebuild the timers - rm.customRaceReminderTimers = make(map[string]*time.Timer) + rm.customRaceReminderTimers = make(map[string]*when.Timer) if rm.notificationManager.HasNotificationReminders() { races, err := rm.store.ListCustomRaces() @@ -1505,17 +1517,16 @@ func (rm *RaceManager) RescheduleNotifications(oldServerOpts *GlobalServerConfig for _, race := range races { race := race - if race.Scheduled.After(time.Now()) { - duration := time.Until(race.Scheduled) + if race.Scheduled.After(time.Now()) && race.Scheduled.Add(time.Duration(0-timer)*time.Minute).After(time.Now()) { + // add reminder + thisTimer := timer - if race.Scheduled.Add(time.Duration(0-timer) * time.Minute).After(time.Now()) { - // add reminder - duration = time.Until(race.Scheduled.Add(time.Duration(0-timer) * time.Minute)) - thisTimer := timer + rm.customRaceReminderTimers[race.UUID.String()], err = when.When(race.Scheduled.Add(time.Duration(0-timer)*time.Minute), func() { + _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) + }) - rm.customRaceReminderTimers[race.UUID.String()] = time.AfterFunc(duration, func() { - _ = rm.notificationManager.SendRaceReminderMessage(race, thisTimer) - }) + if err != nil { + logrus.WithError(err).Error("Could not set up scheduled race reminder timer") } } } diff --git a/race_weekend_handler.go b/race_weekend_handler.go index ae27164d0..895e12b1b 100644 --- a/race_weekend_handler.go +++ b/race_weekend_handler.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "4d63.com/tz" "github.com/go-chi/chi" "github.com/sirupsen/logrus" ) @@ -485,7 +486,7 @@ func (rwh *RaceWeekendHandler) scheduleSession(w http.ResponseWriter, r *http.Re if !startWhenParentFinished { var location *time.Location - location, err := time.LoadLocation(timezone) + location, err := tz.LoadLocation(timezone) if err != nil { logrus.WithError(err).Errorf("could not find location: %s", location) diff --git a/race_weekend_manager.go b/race_weekend_manager.go index ce4030cad..0a18c06c2 100644 --- a/race_weekend_manager.go +++ b/race_weekend_manager.go @@ -12,6 +12,7 @@ import ( "time" "github.com/cj123/assetto-server-manager/pkg/udp" + "github.com/cj123/assetto-server-manager/pkg/when" "github.com/cj123/ini" "github.com/go-chi/chi" @@ -30,8 +31,8 @@ type RaceWeekendManager struct { activeRaceWeekend *ActiveRaceWeekend mutex sync.Mutex - scheduledSessionTimers map[string]*time.Timer - scheduledSessionReminderTimers map[string]*time.Timer + scheduledSessionTimers map[string]*when.Timer + scheduledSessionReminderTimers map[string]*when.Timer } func NewRaceWeekendManager(raceManager *RaceManager, championshipManager *ChampionshipManager, store Store, process ServerProcess, notificationManager NotificationDispatcher) *RaceWeekendManager { @@ -42,8 +43,8 @@ func NewRaceWeekendManager(raceManager *RaceManager, championshipManager *Champi store: store, process: process, - scheduledSessionTimers: make(map[string]*time.Timer), - scheduledSessionReminderTimers: make(map[string]*time.Timer), + scheduledSessionTimers: make(map[string]*when.Timer), + scheduledSessionReminderTimers: make(map[string]*when.Timer), } } @@ -1095,7 +1096,9 @@ func (rwm *RaceWeekendManager) clearScheduledSessionTimer(session *RaceWeekendSe func (rwm *RaceWeekendManager) setupScheduledSessionTimer(raceWeekend *RaceWeekend, session *RaceWeekendSession) error { rwm.clearScheduledSessionTimer(session) - rwm.scheduledSessionTimers[session.ID.String()] = time.AfterFunc(time.Until(session.ScheduledTime), func() { + var err error + + rwm.scheduledSessionTimers[session.ID.String()], err = when.When(session.ScheduledTime, func() { err := rwm.StartSession(raceWeekend.ID.String(), session.ID.String(), false) if err != nil { @@ -1116,22 +1119,29 @@ func (rwm *RaceWeekendManager) setupScheduledSessionTimer(raceWeekend *RaceWeeke } }) + if err != nil { + return err + } + if rwm.notificationManager.HasNotificationReminders() { for _, timer := range rwm.notificationManager.GetNotificationReminders() { reminderTime := session.ScheduledTime.Add(time.Duration(-timer) * time.Minute) if reminderTime.After(time.Now()) { // add reminder - duration := time.Until(reminderTime) thisTimer := timer - rwm.scheduledSessionReminderTimers[session.ID.String()] = time.AfterFunc(duration, func() { + rwm.scheduledSessionReminderTimers[session.ID.String()], err = when.When(reminderTime, func() { err := rwm.notificationManager.SendRaceWeekendReminderMessage(raceWeekend, session, thisTimer) if err != nil { logrus.WithError(err).Errorf("Could not send race weekend reminder message") } }) + + if err != nil { + logrus.WithError(err).Error("Could not set up race weekend reminder timer") + } } } }