Skip to content

Commit

Permalink
improved docs, added example (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
kucherenkovova authored Feb 18, 2024
1 parent 5a60731 commit f981188
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 198 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
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 ./...'
20 changes: 11 additions & 9 deletions README.md
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
Expand All @@ -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
}
Expand Down
102 changes: 0 additions & 102 deletions safegroup_example_md5all_test.go

This file was deleted.

219 changes: 219 additions & 0 deletions safegroup_examples_test.go
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")
}
Loading

0 comments on commit f981188

Please sign in to comment.