Skip to content

Commit

Permalink
Rename ErrorGroup and add Routine
Browse files Browse the repository at this point in the history
  • Loading branch information
sergerad committed Nov 22, 2023
1 parent ec143ee commit 5d2c01a
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 24 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
```
Expand All @@ -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)
}
```
Expand Down
8 changes: 4 additions & 4 deletions examples/errgroup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions errgroup.go → group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
17 changes: 9 additions & 8 deletions errgroup_test.go → group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
})
Expand All @@ -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
})
Expand Down
38 changes: 38 additions & 0 deletions routine.go
Original file line number Diff line number Diff line change
@@ -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
}
27 changes: 27 additions & 0 deletions routine_test.go
Original file line number Diff line number Diff line change
@@ -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))
}

0 comments on commit 5d2c01a

Please sign in to comment.