package runners import ( "context" "errors" "fmt" "time" "codeberg.org/gruf/go-atomics" ) // FuncRunner provides a means of managing long-running functions e.g. main logic loops. type FuncRunner struct { // HandOff is the time after which a blocking function will be considered handed off HandOff time.Duration // ErrorHandler is the function that errors are passed to when encountered by the // provided function. This can be used both for logging, and for error filtering ErrorHandler func(err error) error svc Service // underlying service to manage start/stop err atomics.Error } // Go will attempt to run 'fn' asynchronously. The provided context is used to propagate requested // cancel if FuncRunner.Stop() is called. Any returned error will be passed to FuncRunner.ErrorHandler // for filtering/logging/etc. Any blocking functions will be waited on for FuncRunner.HandOff amount of // time before considering the function as handed off. Returned bool is success state, i.e. returns true // if function is successfully handed off or returns within hand off time with nil error. func (r *FuncRunner) Go(fn func(ctx context.Context) error) bool { var has bool done := make(chan struct{}) go func() { var cancelled bool has = r.svc.Run(func(ctx context.Context) { // reset error r.err.Store(nil) // Run supplied func and set errror if returned if err := Run(func() error { return fn(ctx) }); err != nil { r.err.Store(err) } // signal done close(done) // Check if cancelled select { case <-ctx.Done(): cancelled = true default: cancelled = false } }) switch has { // returned after starting case true: // Load set error err := r.err.Load() // filter out errors due FuncRunner.Stop() being called if cancelled && errors.Is(err, context.Canceled) { // filter out errors from FuncRunner.Stop() being called r.err.Store(nil) } else if err != nil && r.ErrorHandler != nil { // pass any non-nil error to set handler r.err.Store(r.ErrorHandler(err)) } // already running case false: close(done) } }() // get valid handoff to use handoff := r.HandOff if handoff < 1 { handoff = time.Second * 5 } select { // handed off (long-run successful) case <-time.After(handoff): return true // 'fn' returned, check error case <-done: return has } } // Stop will cancel the context supplied to the running function. func (r *FuncRunner) Stop() bool { return r.svc.Stop() } // Err returns the last-set error value. func (r *FuncRunner) Err() error { return r.err.Load() } // Run will execute the supplied 'fn' catching any panics. Returns either function-returned error or formatted panic. func Run(fn func() error) (err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok { // wrap and preserve existing error err = fmt.Errorf("caught panic: %w", e) } else { // simply create new error fromt iface err = fmt.Errorf("caught panic: %v", r) } } }() // run supplied func err = fn() return }