runner
provides an idiomatic wrapper for managing long-running tasks with the
requirement of single atomic runs and graceful atomic shutdowns.
Runner can be made by wrapping any runnable
into the Runner
type. runnable
can be any type satisfying the interface.
type runnable interface {
Run(ctx context.Context)
}
The runnable
must respect the ctx
and should terminate when ctx
completes.
Runner simply has 2 API methods Run()
and Close()
.
type job struct {
}
func (j job) Run(ctx context.Context) {
select {
case <-ctx.Done():
time.Sleep(10 * time.Second)
}
}
func main() {
// Create a runner wrapping the job you want to execute.
jobRunner := runner.Runner{Runnable: job{}}
// Run the runner in a go routine.
go func() {
if err := jobRunner.Run(); err != nil && !errors.Is(err, runner.ErrClosed) {
_, _ = fmt.Fprintf(os.Stderr, "error running runner: %v\n", err)
}
}()
// Wait till the job is to be stopped.
time.Sleep(5 * time.Second)
// Then Close the runner, which will block till the runner completely stops.
if err := jobRunner.Close(); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error stopping runner: %v\n", err)
}
}
Run()
starts the runnable job with a ctx
provided, whereas Close()
cancels
the ctx
thereby shutting down the job.
Yes, Run()
and Close()
can be called concurrently, but the actual run and
close of the job will be atomic (the main goal of the package). If called
concurrently, only one of the calls to Run()
and Close()
will actually have
impact, rest all the other calls will exit with ErrRunning
and ErrStopping
respectively.