-
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.
- Loading branch information
1 parent
5a60731
commit f981188
Showing
5 changed files
with
257 additions
and
198 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,26 @@ | ||
name: 'ci' | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
jobs: | ||
ci: | ||
runs-on: 'ubuntu-22.04' | ||
strategy: | ||
matrix: | ||
version: [ '1.18', '1.19', '1.20', '1.21', '1.22' ] | ||
|
||
steps: | ||
- name: 'Checkout source code' | ||
uses: 'actions/checkout@v3' | ||
|
||
- name: 'Set up Go' | ||
uses: 'actions/setup-go@v4' | ||
with: | ||
go-version: ${{ matrix.version }} | ||
|
||
- name: 'Test' | ||
run: 'go test -v ./...' |
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 |
---|---|---|
@@ -1,11 +1,14 @@ | ||
# safegroup | ||
[![Go Reference](https://pkg.go.dev/badge/github.com/kucherenkovova/safegroup.svg)](https://pkg.go.dev/github.com/kucherenkovova/safegroup) | ||
[![Go Report Card](https://goreportcard.com/badge/github.com/kucherenkovova/safegroup)](https://goreportcard.com/report/github.com/kucherenkovova/safegroup) | ||
|
||
Panic-safe drop-in replacement for [x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup). | ||
|
||
Don't let your whole server crash because of a single goroutine. | ||
|
||
### Installation | ||
```bash | ||
go get github.com/kucherenkovova/[email protected].1 | ||
go get github.com/kucherenkovova/[email protected].2 | ||
``` | ||
|
||
### Usage | ||
|
@@ -14,19 +17,18 @@ go get github.com/kucherenkovova/[email protected] | |
func handleStuff(w http.ResponseWriter, req *http.Request) { | ||
g, ctx := safegroup.WithContext(req.Context()) | ||
|
||
g.Go(queryDB) | ||
g.Go(sendEmail) | ||
g.Go(func() error { | ||
// query database | ||
panic("oops, buggy code") | ||
return nil | ||
}) | ||
|
||
for _, api := range apis { | ||
api := api | ||
g.Go(func() error { | ||
// call api | ||
}) | ||
} | ||
|
||
// Wait for all tasks to complete. | ||
if err := g.Wait(); err != nil { | ||
if errors.Is(err, safegroup.ErrPanic) { | ||
sendAlert(ctx, err) | ||
} | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
This file was deleted.
Oops, something went wrong.
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,219 @@ | ||
// Copyright 2016 The Go Authors. All rights reserved. | ||
// Copyright 2024 Volodymyr Kucherenko <[email protected]>. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package safegroup_test | ||
|
||
import ( | ||
"context" | ||
"crypto/md5" | ||
"errors" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/kucherenkovova/safegroup" | ||
) | ||
|
||
// Pipeline demonstrates the use of a Group to implement a multi-stage | ||
// pipeline: a version of the MD5All function with bounded parallelism from | ||
// https://blog.golang.org/pipelines. | ||
func ExampleGroup_pipeline() { | ||
m, err := MD5All(context.Background(), ".") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
for k, sum := range m { | ||
fmt.Printf("%s:\t%x\n", k, sum) | ||
} | ||
} | ||
|
||
type result struct { | ||
path string | ||
sum [md5.Size]byte | ||
} | ||
|
||
// MD5All reads all the files in the file tree rooted at root and returns a map | ||
// from file path to the MD5 sum of the file's contents. If the directory walk | ||
// fails or any read operation fails, MD5All returns an error. | ||
func MD5All(ctx context.Context, root string) (map[string][md5.Size]byte, error) { | ||
// ctx is canceled when g.Wait() returns. When this version of MD5All returns | ||
// - even in case of error! - we know that all the goroutines have finished | ||
// and the memory they were using can be garbage-collected. | ||
g, ctx := safegroup.WithContext(ctx) | ||
paths := make(chan string) | ||
|
||
g.Go(func() error { | ||
defer close(paths) | ||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
if !info.Mode().IsRegular() { | ||
return nil | ||
} | ||
select { | ||
case paths <- path: | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
} | ||
return nil | ||
}) | ||
}) | ||
|
||
// Start a fixed number of goroutines to read and digest files. | ||
c := make(chan result) | ||
const numDigesters = 20 | ||
for i := 0; i < numDigesters; i++ { | ||
g.Go(func() error { | ||
for path := range paths { | ||
data, err := ioutil.ReadFile(path) | ||
if err != nil { | ||
return err | ||
} | ||
select { | ||
case c <- result{path, md5.Sum(data)}: | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
} | ||
} | ||
return nil | ||
}) | ||
} | ||
go func() { | ||
g.Wait() | ||
close(c) | ||
}() | ||
|
||
m := make(map[string][md5.Size]byte) | ||
for r := range c { | ||
m[r.path] = r.sum | ||
} | ||
// Check whether any of the goroutines failed. Since g is accumulating the | ||
// errors, we don't need to send them (or check for them) in the individual | ||
// results sent on the channel. | ||
if err := g.Wait(); err != nil { | ||
return nil, err | ||
} | ||
return m, nil | ||
} | ||
|
||
// Parallel illustrates the use of a Group for synchronizing a simple parallel | ||
// task: the "Google Search 2.0" function from | ||
// https://talks.golang.org/2012/concurrency.slide#46, augmented with a Context | ||
// and error-handling. | ||
func ExampleGroup_parallel() { | ||
Google := func(ctx context.Context, query string) ([]Result, error) { | ||
g, ctx := safegroup.WithContext(ctx) | ||
|
||
searches := []Search{Web, Image, Video} | ||
results := make([]Result, len(searches)) | ||
for i, search := range searches { | ||
i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines | ||
g.Go(func() error { | ||
result, err := search(ctx, query) | ||
if err == nil { | ||
results[i] = result | ||
} | ||
return err | ||
}) | ||
} | ||
if err := g.Wait(); err != nil { | ||
return nil, err | ||
} | ||
return results, nil | ||
} | ||
|
||
results, err := Google(context.Background(), "golang") | ||
if err != nil { | ||
fmt.Fprintln(os.Stderr, err) | ||
return | ||
} | ||
for _, result := range results { | ||
fmt.Println(result) | ||
} | ||
|
||
// Output: | ||
// web result for "golang" | ||
// image result for "golang" | ||
// video result for "golang" | ||
} | ||
|
||
var ( | ||
Web = fakeSearch("web") | ||
Image = fakeSearch("image") | ||
Video = fakeSearch("video") | ||
) | ||
|
||
type Result string | ||
type Search func(ctx context.Context, query string) (Result, error) | ||
|
||
func fakeSearch(kind string) Search { | ||
return func(_ context.Context, query string) (Result, error) { | ||
return Result(fmt.Sprintf("%s result for %q", kind, query)), nil | ||
} | ||
} | ||
|
||
// JustErrors illustrates the use of a Group in place of a sync.WaitGroup to | ||
// simplify goroutine counting and error handling. This example is derived from | ||
// the sync.WaitGroup example at https://golang.org/pkg/sync/#example_WaitGroup. | ||
func ExampleGroup_justErrors() { | ||
g := new(safegroup.Group) | ||
var urls = []string{ | ||
"http://www.golang.org/", | ||
"http://www.google.com/", | ||
"http://www.somestupidname.com/", | ||
} | ||
for _, url := range urls { | ||
// Launch a goroutine to fetch the URL. | ||
url := url // https://golang.org/doc/faq#closures_and_goroutines | ||
g.Go(func() error { | ||
// Fetch the URL. | ||
resp, err := http.Get(url) | ||
if err == nil { | ||
resp.Body.Close() | ||
} | ||
return err | ||
}) | ||
} | ||
// Wait for all HTTP fetches to complete. | ||
if err := g.Wait(); err == nil { | ||
fmt.Println("Successfully fetched all URLs.") | ||
} | ||
} | ||
|
||
func notifySupport(err error) { | ||
fmt.Println("there is a critical error:", err) | ||
} | ||
|
||
// GoroutinePanic illustrates the use of a Group as a drop-in replacement for errgroup.Group. | ||
// If any of the goroutines panics, the error is returned by g.Wait() and the rest of the goroutines are canceled. | ||
// The service remains operational. You can use errors.Is(err, safegroup.ErrPanic) to check if the error is a panic | ||
// and react accordingly. | ||
func ExampleGroup_goroutinePanic() { | ||
lookup := []string{"a", "b", "c"} | ||
g := &safegroup.Group{} | ||
for i := 0; i < 5; i++ { | ||
i := i | ||
g.Go(func() error { | ||
log.Println("working with ", lookup[i]) | ||
return nil | ||
}) | ||
} | ||
|
||
if err := g.Wait(); err != nil { | ||
if errors.Is(err, safegroup.ErrPanic) { | ||
notifySupport(err) | ||
log.Println("panic occurred, but the service is still running") | ||
} | ||
log.Println(err) | ||
return | ||
} | ||
|
||
log.Println("all goroutines finished successfully") | ||
} |
Oops, something went wrong.