-
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
227 additions
and
135 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 golang.org/x/sync/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,60 @@ | ||
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 | ||
e.Go(func() error { | ||
<-ctx.Done() | ||
return nil | ||
}) | ||
|
||
// Wait for all goroutines | ||
err := e.Wait() | ||
assert.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()) | ||
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,35 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"os/exec" | ||
|
||
"github.com/sergerad/relax" | ||
) | ||
|
||
func main() { | ||
// Main context that is cancelled on SIGINT/SIGTERM | ||
ctx := relax.Context() | ||
|
||
// Indicate how to exit the app gracefully | ||
println("Either of the following commands would exit the app gracefully:") | ||
println("kill -SIGINT", os.Getpid()) | ||
println("kill -SIGTERM", os.Getpid()) | ||
|
||
// Exit the app automatically. Comment this out to try manually | ||
// sending signals | ||
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 { | ||
log.Fatal(err) | ||
} | ||
}() | ||
|
||
// Wait for context to be cancelled | ||
<-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.