From ce2cc6453501cd4e22f8586e7959a07ba57111bf Mon Sep 17 00:00:00 2001 From: sergerad Date: Thu, 21 Mar 2024 20:45:08 +1300 Subject: [PATCH] Add routine.Release --- errors.go | 11 ++++++++ examples/routine/main.go | 11 +++----- group.go | 11 ++------ routine.go | 17 ++++++++++-- routine_test.go | 59 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 errors.go diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..434682f --- /dev/null +++ b/errors.go @@ -0,0 +1,11 @@ +package relax + +import ( + "fmt" +) + +var ( + // PanicError is returned when a panic is recovered + // during the execution of a goroutine + PanicError = fmt.Errorf("recovered from panic") +) diff --git a/examples/routine/main.go b/examples/routine/main.go index 1c621e8..6947cb5 100644 --- a/examples/routine/main.go +++ b/examples/routine/main.go @@ -15,14 +15,11 @@ func main() { fmt.Println(err) } - // If we don't want to wait for the routine, we can use the - // following pattern: + // If we don't want to wait for the routine, release it routine = relax.Go(func() error { panic(2) }) - go func() { - if err := routine.Wait(); err != nil { - fmt.Println(err) - } - }() + routine.Release(func(err error) { + fmt.Println(err) + }) } diff --git a/group.go b/group.go index 11d20f1..8aac8f9 100644 --- a/group.go +++ b/group.go @@ -2,17 +2,10 @@ package relax import ( "context" - "fmt" "golang.org/x/sync/errgroup" ) -var ( - // PanicError is returned when a panic is recovered - // during the execution of a goroutine - PanicError = fmt.Errorf("recovered from panic") -) - // RoutineGroup is a wrapper around golang.org/x/sync/errgroup.Group that // recovers from panics and returns them as errors type RoutineGroup struct { @@ -31,8 +24,8 @@ func NewGroup(ctx context.Context) (*RoutineGroup, context.Context) { // Go runs a provided func in a goroutine while ensuring that // any panic is recovered and returned as an error -func (g *RoutineGroup) Go(f func() error) { - g.Group.Go(func() (err error) { +func (rg *RoutineGroup) Go(f func() error) { + rg.Group.Go(func() (err error) { // Handle panics defer func() { if r := recover(); r != nil { diff --git a/routine.go b/routine.go index 546e23c..94aab25 100644 --- a/routine.go +++ b/routine.go @@ -6,17 +6,28 @@ import ( "errors" ) -// Routine is a handle to a goroutine's error response +// Routine is a handle to a goroutine's error response. +// Panics that occur in this routine are converted into errors. type Routine struct { errChan chan error } -// Wait blocks until the goroutine corresponding to the Routine instance returns an error +// Wait blocks until the goroutine corresponding to the Routine instance returns an error. +// Once an error is returned, all subsequent calls to Wait will return nil. func (r *Routine) Wait() error { return <-r.errChan } -// Go launches a goroutine that will return an error if the provided func panics +// Release will call the handler against an error returned by this routine. +// Once a routine is released, waiting on it will return nil. +func (r *Routine) Release(handler func(error)) { + go func() { + handler(r.Wait()) + }() +} + +// Go launches a goroutine that will return an error if the provided func panics or +// an error is returned by the provided func. func Go(f func() error) *Routine { routine := &Routine{ errChan: make(chan error, 1), diff --git a/routine_test.go b/routine_test.go index e2c4dfa..605048d 100644 --- a/routine_test.go +++ b/routine_test.go @@ -8,6 +8,10 @@ import ( "github.com/stretchr/testify/require" ) +var ( + errTestSentinel = errors.New("test sentinel") +) + func TestRoutine_NilError(t *testing.T) { r := Go(func() error { return nil @@ -15,6 +19,59 @@ func TestRoutine_NilError(t *testing.T) { assert.NoError(t, r.Wait()) } +func TestRoutine_Multicall(t *testing.T) { + var tests = []struct { + name string + f func() error + expectedErr error + }{ + {"nil", func() error { return nil }, nil}, + {"err", func() error { return errTestSentinel }, errTestSentinel}, + {"panic", func() error { panic("test panic") }, PanicError}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := Go(func() error { + return test.f() + }) + for i := 0; i < 5; i++ { + err := r.Wait() + if i == 0 { + if !errors.Is(err, test.expectedErr) { + t.Errorf("expected %v, got %v. iteration %d", test.expectedErr, err, i) + } + } else { + if err != nil { + t.Errorf("expected nil, got %v. iteration %d", err, i) + } + } + } + }) + } +} + +func TestRoutine_Release(t *testing.T) { + var tests = []struct { + name string + err error + }{ + {"nil", nil}, + {"error", errTestSentinel}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + Go(func() error { + return test.err + }).Release(func(err error) { + if !errors.Is(err, test.err) { + t.Errorf("expected %v, got %v", test.err, err) + } + }) + + }) + } +} + func TestRoutine_Panic_NotNilError(t *testing.T) { var tests = []struct { name string @@ -23,7 +80,7 @@ func TestRoutine_Panic_NotNilError(t *testing.T) { }{ {"empty", "", ""}, {"non-empty", "test panic", "test panic"}, - {"error", errors.New("fail"), "fail"}, + {"error", errTestSentinel, errTestSentinel.Error()}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) {