package asynctask import ( "fmt" "time" "github.com/splitio/go-toolkit/v5/logging" "github.com/splitio/go-toolkit/v5/struct/traits/lifecycle" ) // AsyncTask is a struct that wraps tasks that should run periodically and can be remotely stopped & started, // as well as making it's status (running/stopped) available. type AsyncTask struct { lifecycle lifecycle.Manager task func(l logging.LoggerInterface) error name string incoming chan int period int onInit func(l logging.LoggerInterface) error onStop func(l logging.LoggerInterface) logger logging.LoggerInterface } const ( taskMessageWakeup = iota ) // Start initiates the task. It wraps the execution in a closure guarded by a call to recover() in order // to prevent the main application from crashin if something goes wrong while the sdk interacts with the backend. func (t *AsyncTask) Start() { if !t.lifecycle.BeginInitialization() { if t.logger != nil { t.logger.Warning(fmt.Sprintf("Task %s is not idle. Aborting new execution.", t.name)) } return } go func() { defer func() { if r := recover(); r != nil { if t.logger != nil { t.logger.Error(fmt.Sprintf( "AsyncTask %s is panicking! shutting down. Consider restarting this instance and raising an issue", t.name, )) t.logger.Error(r) } } }() defer t.lifecycle.ShutdownComplete() if !t.lifecycle.InitializationComplete() { return } // If there's an initialization function, execute it if t.onInit != nil { err := t.onInit(t.logger) if err != nil { // If something goes wrong during initialization, abort. if t.logger != nil { t.logger.Error(fmt.Sprintf("task '%s' initialization failed: %s", t.name, err.Error())) } t.lifecycle.AbnormalShutdown() return } } // Create timeout timer idleDuration := time.Second * time.Duration(t.period) taskTimer := time.NewTimer(idleDuration) defer taskTimer.Stop() if t.onStop != nil { defer t.onStop(t.logger) } // Task execution for { select { case <-t.lifecycle.ShutdownRequested(): return case <-t.incoming: // wake up signal case <-taskTimer.C: // Timedout } // Run the wrapped task and handle the returned error if any. err := t.task(t.logger) if err != nil && t.logger != nil { t.logger.Error(fmt.Sprintf("task '%s' failed with error: %s", t.name, err.Error())) } // Resetting timer taskTimer.Reset(idleDuration) } }() } func (t *AsyncTask) sendSignal(signal int) error { select { case t.incoming <- signal: return nil default: return fmt.Errorf("Couldn't send message to task %s", t.name) } } // Stop executes onStop hook if any, blocks until its done (if blocking = true) and prevents future executions of the task. func (t *AsyncTask) Stop(blocking bool) error { if !t.lifecycle.BeginShutdown() { return fmt.Errorf("task '%s' not running", t.name) } if blocking { t.lifecycle.AwaitShutdownComplete() } return nil } // WakeUp interrupts the task's sleep period and resumes execution func (t *AsyncTask) WakeUp() error { return t.sendSignal(taskMessageWakeup) } // IsRunning returns true if the task is currently running func (t *AsyncTask) IsRunning() bool { return t.lifecycle.IsRunning() } // NewAsyncTask creates a new task and returns a pointer to it func NewAsyncTask( name string, task func(l logging.LoggerInterface) error, period int, onInit func(l logging.LoggerInterface) error, onStop func(l logging.LoggerInterface), logger logging.LoggerInterface, ) *AsyncTask { t := AsyncTask{ name: name, task: task, period: period, onInit: onInit, onStop: onStop, logger: logger, incoming: make(chan int, 10), } t.lifecycle.Setup() return &t }