-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement ErrorGroup and update tests and examples
- Loading branch information
Showing
10 changed files
with
221 additions
and
133 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# CHANGELOG | ||
|
||
## v2.0.0 | ||
|
||
* | ||
|
||
## v1.0.0 | ||
|
||
* Initial implementation |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package relax | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"os/signal" | ||
"syscall" | ||
) | ||
|
||
// Context instantiates a context that is cancelled only when | ||
// the SIGINT/SIGTERM signals are received. | ||
// This context should be set up in main() of your application. | ||
func Context() context.Context { | ||
// Instantiate cancellable context | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
go func() { | ||
// Cancel context on signals | ||
signalChan := make(chan os.Signal, 1) | ||
signal.Notify(signalChan, Signals()...) | ||
// Wait for signal | ||
<-signalChan | ||
// Cancel context | ||
cancel() | ||
}() | ||
return ctx | ||
} | ||
|
||
// Signals returns the signals that will cause the context to be cancelled. | ||
func Signals() []os.Signal { | ||
return []os.Signal{syscall.SIGINT, syscall.SIGTERM} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package relax | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestContext_Signals_CancelContext(t *testing.T) { | ||
for _, signal := range Signals() { | ||
t.Run(signal.String(), func(t *testing.T) { | ||
ctx := Context() | ||
go func() { | ||
cmd := exec.Command("kill", "-SIGINT", fmt.Sprint(os.Getpid())) | ||
cmd.Stderr = os.Stderr | ||
cmd.Stdout = os.Stdout | ||
require.NoError(t, cmd.Run()) | ||
}() | ||
// Exit only if signal causes cancel | ||
<-ctx.Done() | ||
t.Log("Exiting gracefully") | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
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") | ||
) | ||
|
||
// ErrorGroup is a wrapper around errgroup.Group that | ||
// recovers from panics and returns them as errors | ||
type ErrorGroup struct { | ||
*errgroup.Group | ||
} | ||
|
||
// NewErrorGroup instantiates an ErrorGroup 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) { | ||
errgroup, groupCtx := errgroup.WithContext(ctx) | ||
return &ErrorGroup{ | ||
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) { | ||
g.Group.Go(func() (err error) { | ||
// 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 | ||
err = fmt.Errorf("%w: %v", PanicError, r) | ||
} | ||
} | ||
// Handle panics | ||
defer recoverFunc() | ||
// Call the provided func | ||
return f() | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package relax | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestErrGroup_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.Go(func() error { | ||
<-ctx.Done() | ||
<-gCtx.Done() | ||
return nil | ||
}) | ||
|
||
// Cancel root context | ||
cancel() | ||
|
||
// Validate | ||
assert.NoError(t, e.Wait()) | ||
} | ||
|
||
func TestErrGroup_Panic_Error(t *testing.T) { | ||
// Group | ||
e, ctx := NewErrorGroup(context.Background()) | ||
|
||
// Routine that panics | ||
panicMsg := "testing content is returned as error" | ||
e.Go(func() error { | ||
panic(panicMsg) | ||
}) | ||
|
||
// Sibling routine that blocks on Group context | ||
siblingChan := make(chan interface{}, 1) | ||
e.Go(func() error { | ||
<-ctx.Done() | ||
siblingChan <- nil | ||
return nil | ||
}) | ||
|
||
// Wait for panic goroutine, verify error | ||
err := e.Wait() | ||
assert.Error(t, err) | ||
assert.Contains(t, err.Error(), panicMsg) | ||
assert.True(t, errors.Is(err, PanicError)) | ||
|
||
// Verify sibling finished | ||
assert.Nil(t, <-siblingChan) | ||
} | ||
|
||
func TestErrGroup_NoPanic_NoError(t *testing.T) { | ||
e, _ := NewErrorGroup(context.Background()) | ||
e.Go(func() error { | ||
return nil | ||
}) | ||
assert.NoError(t, e.Wait()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
|
||
"github.com/sergerad/relax" | ||
) | ||
|
||
func main() { | ||
ctx := relax.Context() | ||
// Exit only if signal causes cancel | ||
println("Either of the following commands would exit the app gracefully:") | ||
println("kill -SIGINT", os.Getpid()) | ||
println("kill -SIGTERM", os.Getpid()) | ||
go func() { | ||
cmd := exec.Command("kill", "-SIGINT", fmt.Sprint(os.Getpid())) | ||
cmd.Stderr = os.Stderr | ||
cmd.Stdout = os.Stdout | ||
if err := cmd.Run(); err != nil { | ||
panic(err) | ||
} | ||
}() | ||
<-ctx.Done() | ||
println("Exiting gracefully") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.