gotosocial/vendor/codeberg.org/gruf/go-runners/run.go
2022-01-03 17:37:09 +01:00

130 lines
3.1 KiB
Go

package runners
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
// 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 error // last-set error
mu sync.Mutex // protects err
}
// 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 {
done := make(chan struct{})
go func() {
var cancelled bool
has := r.svc.Run(func(ctx context.Context) {
// reset error
r.mu.Lock()
r.err = nil
r.mu.Unlock()
// Run supplied func and set errror if returned
if err := Run(func() error { return fn(ctx) }); err != nil {
r.mu.Lock()
r.err = err
r.mu.Unlock()
}
// signal done
close(done)
// Check if cancelled
select {
case <-ctx.Done():
cancelled = true
default:
cancelled = false
}
})
switch has {
// returned after starting
case true:
r.mu.Lock()
// filter out errors due FuncRunner.Stop() being called
if cancelled && errors.Is(r.err, context.Canceled) {
// filter out errors from FuncRunner.Stop() being called
r.err = nil
} else if r.err != nil && r.ErrorHandler != nil {
// pass any non-nil error to set handler
r.err = r.ErrorHandler(r.err)
}
r.mu.Unlock()
// 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 (r.Err() == nil)
}
}
// 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 {
r.mu.Lock()
err := r.err
r.mu.Unlock()
return err
}
// 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
}