diff --git a/main.go b/main.go index 6b9c28da..0f0689fa 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,6 @@ import ( "os/signal" "strconv" "strings" - "sync/atomic" "syscall" "time" @@ -352,14 +351,6 @@ func main() { } log.Print("Initialising the task system") - // TODO: Name the tasks so we can figure out which one it was when something goes wrong? Or maybe toss it up WithStack down there? - var runTasks = func(tasks []func() error) { - for _, task := range tasks { - if task() != nil { - common.LogError(err) - } - } - } // Thumbnailer goroutine, we only want one image being thumbnailed at a time, otherwise they might wind up consuming all the CPU time and leave no resources left to service the actual requests // TODO: Could we expand this to attachments and other things too? @@ -413,109 +404,14 @@ func main() { } } }() + // TODO: Write tests for these // Run this goroutine once every half second halfSecondTicker := time.NewTicker(time.Second / 2) secondTicker := time.NewTicker(time.Second) fifteenMinuteTicker := time.NewTicker(15 * time.Minute) hourTicker := time.NewTicker(time.Hour) - go func() { - var startTick = func() (abort bool) { - var isDBDown = atomic.LoadInt32(&common.IsDBDown) - err := db.Ping() - if err != nil { - // TODO: There's a bit of a race here, but it doesn't matter if this error appears multiple times in the logs as it's capped at three times, we just want to cut it down 99% of the time - if isDBDown == 0 { - common.LogWarning(err) - common.LogWarning(errors.New("The database is down")) - } - atomic.StoreInt32(&common.IsDBDown, 1) - return true - } - if isDBDown == 1 { - log.Print("The database is back") - } - atomic.StoreInt32(&common.IsDBDown, 0) - return false - } - var runHook = func(name string) { - err := common.RunTaskHook(name) - if err != nil { - common.LogError(err, "Failed at task '"+name+"'") - } - } - for { - select { - case <-halfSecondTicker.C: - if startTick() { - continue - } - runHook("before_half_second_tick") - runTasks(common.ScheduledHalfSecondTasks) - runHook("after_half_second_tick") - case <-secondTicker.C: - if startTick() { - continue - } - runHook("before_second_tick") - go func() { thumbChan <- true }() - runTasks(common.ScheduledSecondTasks) - - // TODO: Stop hard-coding this - err := common.HandleExpiredScheduledGroups() - if err != nil { - common.LogError(err) - } - - // TODO: Handle delayed moderation tasks - - // Sync with the database, if there are any changes - err = common.HandleServerSync() - if err != nil { - common.LogError(err) - } - - // TODO: Manage the TopicStore, UserStore, and ForumStore - // TODO: Alert the admin, if CPU usage, RAM usage, or the number of posts in the past second are too high - // TODO: Clean-up alerts with no unread matches which are over two weeks old. Move this to a 24 hour task? - // TODO: Rescan the static files for changes - runHook("after_second_tick") - case <-fifteenMinuteTicker.C: - if startTick() { - continue - } - runHook("before_fifteen_minute_tick") - runTasks(common.ScheduledFifteenMinuteTasks) - - // TODO: Automatically lock topics, if they're really old, and the associated setting is enabled. - // TODO: Publish scheduled posts. - runHook("after_fifteen_minute_tick") - case <-hourTicker.C: - if startTick() { - continue - } - runHook("before_hour_tick") - - jsToken, err := common.GenerateSafeString(80) - if err != nil { - common.LogError(err) - } - common.JSTokenBox.Store(jsToken) - - common.OldSessionSigningKeyBox.Store(common.SessionSigningKeyBox.Load().(string)) // TODO: We probably don't need this type conversion - sessionSigningKey, err := common.GenerateSafeString(80) - if err != nil { - common.LogError(err) - } - common.SessionSigningKeyBox.Store(sessionSigningKey) - - runTasks(common.ScheduledHourTasks) - runHook("after_hour_tick") - } - - // TODO: Handle the daily clean-up. - } - }() + go tickLoop(thumbChan, halfSecondTicker, secondTicker, fifteenMinuteTicker, hourTicker) log.Print("Initialising the router") router, err = NewGenRouter(http.FileServer(http.Dir("./uploads"))) @@ -549,7 +445,7 @@ func main() { // TODO: Add a graceful shutdown function func stoppedServer(msg ...interface{}) { - log.Print("stopped server") + //log.Print("stopped server") stopServerChan <- msg } diff --git a/tickloop.go b/tickloop.go new file mode 100644 index 00000000..d5ced549 --- /dev/null +++ b/tickloop.go @@ -0,0 +1,120 @@ +package main + +import ( + "errors" + "log" + "sync/atomic" + "time" + + "./common" +) + +// TODO: Name the tasks so we can figure out which one it was when something goes wrong? Or maybe toss it up WithStack down there? +func runTasks(tasks []func() error) { + for _, task := range tasks { + err := task() + if err != nil { + common.LogError(err) + } + } +} + +func startTick() (abort bool) { + var isDBDown = atomic.LoadInt32(&common.IsDBDown) + err := db.Ping() + if err != nil { + // TODO: There's a bit of a race here, but it doesn't matter if this error appears multiple times in the logs as it's capped at three times, we just want to cut it down 99% of the time + if isDBDown == 0 { + common.LogWarning(err) + common.LogWarning(errors.New("The database is down")) + } + atomic.StoreInt32(&common.IsDBDown, 1) + return true + } + if isDBDown == 1 { + log.Print("The database is back") + } + atomic.StoreInt32(&common.IsDBDown, 0) + return false +} + +func runHook(name string) { + err := common.RunTaskHook(name) + if err != nil { + common.LogError(err, "Failed at task '"+name+"'") + } +} + +func tickLoop(thumbChan chan bool, halfSecondTicker *time.Ticker, secondTicker *time.Ticker, fifteenMinuteTicker *time.Ticker, hourTicker *time.Ticker) { + for { + select { + case <-halfSecondTicker.C: + if startTick() { + continue + } + runHook("before_half_second_tick") + runTasks(common.ScheduledHalfSecondTasks) + runHook("after_half_second_tick") + case <-secondTicker.C: + if startTick() { + continue + } + runHook("before_second_tick") + go func() { thumbChan <- true }() + runTasks(common.ScheduledSecondTasks) + + // TODO: Stop hard-coding this + err := common.HandleExpiredScheduledGroups() + if err != nil { + common.LogError(err) + } + + // TODO: Handle delayed moderation tasks + + // Sync with the database, if there are any changes + err = common.HandleServerSync() + if err != nil { + common.LogError(err) + } + + // TODO: Manage the TopicStore, UserStore, and ForumStore + // TODO: Alert the admin, if CPU usage, RAM usage, or the number of posts in the past second are too high + // TODO: Clean-up alerts with no unread matches which are over two weeks old. Move this to a 24 hour task? + // TODO: Rescan the static files for changes + runHook("after_second_tick") + case <-fifteenMinuteTicker.C: + if startTick() { + continue + } + runHook("before_fifteen_minute_tick") + runTasks(common.ScheduledFifteenMinuteTasks) + + // TODO: Automatically lock topics, if they're really old, and the associated setting is enabled. + // TODO: Publish scheduled posts. + runHook("after_fifteen_minute_tick") + case <-hourTicker.C: + if startTick() { + continue + } + runHook("before_hour_tick") + + jsToken, err := common.GenerateSafeString(80) + if err != nil { + common.LogError(err) + } + common.JSTokenBox.Store(jsToken) + + common.OldSessionSigningKeyBox.Store(common.SessionSigningKeyBox.Load().(string)) // TODO: We probably don't need this type conversion + sessionSigningKey, err := common.GenerateSafeString(80) + if err != nil { + common.LogError(err) + } + common.SessionSigningKeyBox.Store(sessionSigningKey) + + runTasks(common.ScheduledHourTasks) + runHook("after_hour_tick") + } + + // TODO: Handle the daily clean-up. + } +}