diff --git a/README.md b/README.md index 163e256..304244e 100644 --- a/README.md +++ b/README.md @@ -29,16 +29,15 @@ func main() { This ensures that SIGINT/SIGTERM will cause all contexts used in the application to be `Done()`. -If you have multiple, long running processes to run in your program, you can use ErrorGroup to launch them. +If you have multiple, long running processes to run in your program, you can use `RoutineGroup` to launch them. ```Go - g, ctx := relax.NewErrorGroup(mainCtx) + group, ctx := relax.NewGroup(mainCtx) ``` -You can use the `ErrorGroup` to launch goroutines which will return an error if they encounter a panic. +You can use the `RoutineGroup` to launch goroutines which will return an error if they encounter a panic. ```Go - g.Go(func() error { - // An error containing "failed" will be returned + group.Go(func() error { panic("failed") }) ``` @@ -47,7 +46,18 @@ Finally, in the main goroutine, make sure to wait for the error group: ```Go if err := g.Wait(); err != nil { - fmt.Println(err) + log.Fatal(err) + } +``` + +When you only have a single goroutine to run, you can use `Routine` instead of `RoutineGroup`: + +```Go + routine := relax.Go(func() error { + panic("failed") + }) + if err := routine.Wait(); err != nil { + log.Fatal(err) } ``` diff --git a/examples/errgroup/main.go b/examples/errgroup/main.go index eac0499..665c08c 100644 --- a/examples/errgroup/main.go +++ b/examples/errgroup/main.go @@ -9,22 +9,22 @@ import ( func main() { // Instantiate the main context and error group - g, ctx := relax.NewErrorGroup(relax.Context()) + group, ctx := relax.NewGroup(relax.Context()) // Launch goroutine that blocks on context - g.Go(func() error { + group.Go(func() error { <-ctx.Done() fmt.Println("blocking routine done") return nil }) // Launch goroutine that resembles a long running processor - g.Go(func() error { + group.Go(func() error { return exampleProcessor(ctx) }) // Wait for errgroup - if err := g.Wait(); err != nil { + if err := group.Wait(); err != nil { fmt.Println(err) } fmt.Println("shutting down") diff --git a/errgroup.go b/group.go similarity index 75% rename from errgroup.go rename to group.go index 080f02a..a0a8492 100644 --- a/errgroup.go +++ b/group.go @@ -13,25 +13,25 @@ var ( PanicError = fmt.Errorf("recovered from panic") ) -// ErrorGroup is a wrapper around golang.org/x/sync/errgroup.Group that +// RoutineGroup is a wrapper around golang.org/x/sync/errgroup.Group that // recovers from panics and returns them as errors -type ErrorGroup struct { +type RoutineGroup struct { *errgroup.Group } -// NewErrorGroup instantiates an ErrorGroup and corresponding context. +// NewGroup instantiates an RoutineGroup and corresponding context. // This function should be used the same way as errgroup.WithContext() from // golang.org/x/sync/errgroup -func NewErrorGroup(ctx context.Context) (*ErrorGroup, context.Context) { +func NewGroup(ctx context.Context) (*RoutineGroup, context.Context) { errgroup, groupCtx := errgroup.WithContext(ctx) - return &ErrorGroup{ + return &RoutineGroup{ Group: errgroup, }, groupCtx } // Go runs a provided func in a goroutine while ensuring that // any panic is recovered and returned as an error -func (g *ErrorGroup) Go(f func() error) { +func (g *RoutineGroup) Go(f func() error) { g.Group.Go(func() (err error) { // Define a recover func that converts a panic to an error recoverFunc := func() { diff --git a/errgroup_test.go b/group_test.go similarity index 69% rename from errgroup_test.go rename to group_test.go index af9ef2f..3a71603 100644 --- a/errgroup_test.go +++ b/group_test.go @@ -6,14 +6,15 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestErrGroup_CancelParentContext_ChildContextDone(t *testing.T) { +func TestGroup_CancelParentContext_ChildContextDone(t *testing.T) { // Root context to cancel ctx, cancel := context.WithCancel(context.Background()) // Group to wait on root and group context Done() - e, gCtx := NewErrorGroup(ctx) + e, gCtx := NewGroup(ctx) e.Go(func() error { <-ctx.Done() <-gCtx.Done() @@ -27,12 +28,12 @@ func TestErrGroup_CancelParentContext_ChildContextDone(t *testing.T) { assert.NoError(t, e.Wait()) } -func TestErrGroup_Panic_Error(t *testing.T) { +func TestGroup_Panic_Error(t *testing.T) { // Group - e, ctx := NewErrorGroup(context.Background()) + e, ctx := NewGroup(context.Background()) // Routine that panics - panicMsg := "testing content is returned as error" + panicMsg := "test panic" e.Go(func() error { panic(panicMsg) }) @@ -45,14 +46,14 @@ func TestErrGroup_Panic_Error(t *testing.T) { // Wait for all goroutines err := e.Wait() - assert.Error(t, err) + require.Error(t, err) // Verify panic message/error is returned assert.Contains(t, err.Error(), panicMsg) assert.True(t, errors.Is(err, PanicError)) } -func TestErrGroup_NoPanic_NoError(t *testing.T) { - e, _ := NewErrorGroup(context.Background()) +func TestGroup_NoPanic_NoError(t *testing.T) { + e, _ := NewGroup(context.Background()) e.Go(func() error { return nil }) diff --git a/routine.go b/routine.go new file mode 100644 index 0000000..7c9506b --- /dev/null +++ b/routine.go @@ -0,0 +1,38 @@ +package relax + +import ( + "fmt" +) + +// Routine is a handle to a goroutine's error response +type Routine struct { + errChan chan error +} + +// Wait blocks until the goroutine corresponding to the Routine instance returns an error +func (r *Routine) Wait() error { + return <-r.errChan +} + +// Go launches a goroutine that will return an error if the provided func panics +func Go(f func() error) *Routine { + routine := &Routine{ + errChan: make(chan error, 1), + } + go func() { + // Define a recover func that converts a panic to an error + recoverFunc := func() { + if r := recover(); r != nil { + // Assign the panic content to returned error + routine.errChan <- fmt.Errorf("%w: %v", PanicError, r) + } + } + // Always close + defer close(routine.errChan) + // Handle panics + defer recoverFunc() + // Call the provided func + routine.errChan <- f() + }() + return routine +} diff --git a/routine_test.go b/routine_test.go new file mode 100644 index 0000000..372c0f3 --- /dev/null +++ b/routine_test.go @@ -0,0 +1,27 @@ +package relax + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRoutine_NilError(t *testing.T) { + r := Go(func() error { + return nil + }) + assert.NoError(t, r.Wait()) +} + +func TestRoutine_Panic_NotNilError(t *testing.T) { + panicMsg := "test panic" + r := Go(func() error { + panic("test panic") + }) + err := r.Wait() + require.Error(t, err) + assert.Contains(t, err.Error(), panicMsg) + assert.True(t, errors.Is(err, PanicError)) +}