pax_global_header00006660000000000000000000000064142114250060014505gustar00rootroot0000000000000052 comment=de404588ab3d4b4ca6f8afba200b5b74994879f8 semgroup-1.2.0/000077500000000000000000000000001421142500600133465ustar00rootroot00000000000000semgroup-1.2.0/.github/000077500000000000000000000000001421142500600147065ustar00rootroot00000000000000semgroup-1.2.0/.github/workflows/000077500000000000000000000000001421142500600167435ustar00rootroot00000000000000semgroup-1.2.0/.github/workflows/go.yml000066400000000000000000000011341421142500600200720ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: ci: name: "Go build" runs-on: ubuntu-latest steps: - name: Set up Go 1.17 uses: actions/setup-go@v2 with: go-version: 1.17 id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Test run: | go mod tidy -v go test -race ./... - run: "go vet ./..." - name: Staticcheck uses: dominikh/staticcheck-action@v1.1.0 with: version: "2021.1.1" install-go: false - name: Build run: go build ./... semgroup-1.2.0/LICENSE000066400000000000000000000027061421142500600143600ustar00rootroot00000000000000Copyright (c) 2022, Fatih Arslan All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of semgroup nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. semgroup-1.2.0/README.md000066400000000000000000000037641421142500600146370ustar00rootroot00000000000000# semgroup [![](https://github.com/fatih/semgroup/workflows/build/badge.svg)](https://github.com/fatih/semgroup/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/semgroup)](https://pkg.go.dev/github.com/fatih/semgroup) semgroup provides synchronization and error propagation, for groups of goroutines working on subtasks of a common task. It uses a weighted semaphore implementation to make sure that only a number of maximum tasks can be run at any time. Unlike [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup), it doesn't return the first non-nil error, rather it accumulates all errors and returns a set of errors, allowing each task to fullfil their task. # Install ```bash go get github.com/fatih/semgroup ``` # Example With no errors: ```go package main import ( "context" "fmt" "github.com/fatih/semgroup" ) func main() { const maxWorkers = 2 s := semgroup.NewGroup(context.Background(), maxWorkers) visitors := []int{5, 2, 10, 8, 9, 3, 1} for _, v := range visitors { v := v s.Go(func() error { fmt.Println("Visits: ", v) return nil }) } // Wait for all visits to complete. Any errors are accumulated. if err := s.Wait(); err != nil { fmt.Println(err) } // Output: // Visits: 2 // Visits: 10 // Visits: 8 // Visits: 9 // Visits: 3 // Visits: 1 // Visits: 5 } ``` With errors: ```go package main import ( "context" "errors" "fmt" "github.com/fatih/semgroup" ) func main() { const maxWorkers = 2 s := semgroup.NewGroup(context.Background(), maxWorkers) visitors := []int{1, 1, 1, 1, 2, 2, 1, 1, 2} for _, v := range visitors { v := v s.Go(func() error { if v != 1 { return errors.New("only one visitor is allowed") } return nil }) } // Wait for all visits to complete. Any errors are accumulated. if err := s.Wait(); err != nil { fmt.Println(err) } // Output: // 3 error(s) occurred: // * only one visitor is allowed // * only one visitor is allowed // * only one visitor is allowed } ``` semgroup-1.2.0/example_test.go000066400000000000000000000025561421142500600163770ustar00rootroot00000000000000package semgroup_test import ( "context" "errors" "fmt" "sync" "github.com/fatih/semgroup" ) // This example increases a counter for each visit concurrently, using a // SemGroup to block until all the visitors have finished. It only runs 2 tasks // at any time. func ExampleGroup_parallel() { const maxWorkers = 2 s := semgroup.NewGroup(context.Background(), maxWorkers) var ( counter int mu sync.Mutex // protects visits ) visitors := []int{5, 2, 10, 8, 9, 3, 1} for _, v := range visitors { v := v s.Go(func() error { mu.Lock() counter += v mu.Unlock() return nil }) } // Wait for all visits to complete. Any errors are accumulated. if err := s.Wait(); err != nil { fmt.Println(err) } fmt.Printf("Counter: %d", counter) // Output: // Counter: 38 } func ExampleGroup_withErrors() { const maxWorkers = 2 s := semgroup.NewGroup(context.Background(), maxWorkers) visitors := []int{1, 1, 1, 1, 2, 2, 1, 1, 2} for _, v := range visitors { v := v s.Go(func() error { if v != 1 { return errors.New("only one visitor is allowed") } return nil }) } // Wait for all visits to complete. Any errors are accumulated. if err := s.Wait(); err != nil { fmt.Println(err) } // Output: // 3 error(s) occurred: // * only one visitor is allowed // * only one visitor is allowed // * only one visitor is allowed } semgroup-1.2.0/go.mod000066400000000000000000000001501421142500600144500ustar00rootroot00000000000000module github.com/fatih/semgroup go 1.17 require golang.org/x/sync v0.0.0-20210220032951-036812b2e83c semgroup-1.2.0/go.sum000066400000000000000000000003211421142500600144750ustar00rootroot00000000000000golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= semgroup-1.2.0/semgroup.go000066400000000000000000000051621421142500600155420ustar00rootroot00000000000000// Package semgroup provides synchronization and error propagation, for groups // of goroutines working on subtasks of a common task. It uses a weighted // semaphore implementation to make sure that only a number of maximum tasks // can be run at any time. // // Unlike golang.org/x/sync/errgroup, it doesn't return the first non-nil // error, rather it accumulates all errors and returns a set of errors, // allowing each task to fullfil their task. package semgroup import ( "context" "errors" "fmt" "strings" "sync" "golang.org/x/sync/semaphore" ) // A Group is a collection of goroutines working on subtasks that are part of // the same overall task. type Group struct { sem *semaphore.Weighted wg sync.WaitGroup ctx context.Context errs multiError mu sync.Mutex // protects errs } // NewGroup returns a new Group with the given maximum combined weight for // concurrent access. func NewGroup(ctx context.Context, maxWorkers int64) *Group { return &Group{ ctx: ctx, sem: semaphore.NewWeighted(maxWorkers), } } // Go calls the given function in a new goroutine. It also acquires the // semaphore with a weight of 1, blocking until resources are available or ctx // is done. // On success, returns nil. On failure, returns ctx.Err() and leaves the // semaphore unchanged. Any function call to return a non-nil error is // accumulated; the accumulated errors will be returned by Wait. func (g *Group) Go(f func() error) { g.wg.Add(1) err := g.sem.Acquire(g.ctx, 1) if err != nil { g.wg.Done() g.mu.Lock() g.errs = append(g.errs, fmt.Errorf("couldn't acquire semaphore: %s", err)) g.mu.Unlock() return } go func() { defer g.sem.Release(1) defer g.wg.Done() if err := f(); err != nil { g.mu.Lock() g.errs = append(g.errs, err) g.mu.Unlock() } }() } // Wait blocks until all function calls from the Go method have returned, then // returns all accumulated non-nil error (if any) from them. func (g *Group) Wait() error { g.wg.Wait() return g.errs.ErrorOrNil() } type multiError []error func (e multiError) Error() string { var b strings.Builder fmt.Fprintf(&b, "%d error(s) occurred:\n", len(e)) for i, err := range e { fmt.Fprintf(&b, "* %s", err.Error()) if i != len(e)-1 { fmt.Fprintln(&b, "") } } return b.String() } func (e multiError) ErrorOrNil() error { if len(e) == 0 { return nil } return e } func (e multiError) Is(target error) bool { for _, err := range e { if errors.Is(err, target) { return true } } return false } func (e multiError) As(target interface{}) bool { for _, err := range e { if errors.As(err, target) { return true } } return false } semgroup-1.2.0/semgroup_test.go000066400000000000000000000060361421142500600166020ustar00rootroot00000000000000package semgroup import ( "context" "errors" "os" "sync" "testing" ) func TestGroup_single_task(t *testing.T) { ctx := context.Background() g := NewGroup(ctx, 1) g.Go(func() error { return nil }) err := g.Wait() if err != nil { t.Errorf("g.Wait() should not return an error") } } func TestGroup_multiple_tasks(t *testing.T) { ctx := context.Background() g := NewGroup(ctx, 1) count := 0 var mu sync.Mutex inc := func() error { mu.Lock() count++ mu.Unlock() return nil } g.Go(func() error { return inc() }) g.Go(func() error { return inc() }) g.Go(func() error { return inc() }) g.Go(func() error { return inc() }) err := g.Wait() if err != nil { t.Errorf("g.Wait() should not return an error") } if count != 4 { t.Errorf("count should be %d, got: %d", 4, count) } } func TestGroup_multiple_tasks_errors(t *testing.T) { ctx := context.Background() g := NewGroup(ctx, 1) g.Go(func() error { return errors.New("foo") }) g.Go(func() error { return nil }) g.Go(func() error { return errors.New("bar") }) g.Go(func() error { return nil }) err := g.Wait() if err == nil { t.Fatalf("g.Wait() should return an error") } wantErr := `2 error(s) occurred: * foo * bar` if wantErr != err.Error() { t.Errorf("error should be:\n%s\ngot:\n%s\n", wantErr, err.Error()) } } func TestGroup_deadlock(t *testing.T) { canceledCtx, cancel := context.WithCancel(context.Background()) cancel() g := NewGroup(canceledCtx, 1) g.Go(func() error { return nil }) g.Go(func() error { return nil }) err := g.Wait() if err == nil { t.Fatalf("g.Wait() should return an error") } wantErr := `1 error(s) occurred: * couldn't acquire semaphore: context canceled` if wantErr != err.Error() { t.Errorf("error should be:\n%s\ngot:\n%s\n", wantErr, err.Error()) } } func TestGroup_multiple_tasks_errors_Is(t *testing.T) { ctx := context.Background() g := NewGroup(ctx, 1) var ( fooErr = errors.New("foo") barErr = errors.New("bar") bazErr = errors.New("baz") ) g.Go(func() error { return fooErr }) g.Go(func() error { return nil }) g.Go(func() error { return barErr }) g.Go(func() error { return nil }) err := g.Wait() if err == nil { t.Fatalf("g.Wait() should return an error") } if !errors.Is(err, fooErr) { t.Errorf("error should be contained %v\n", fooErr) } if !errors.Is(err, barErr) { t.Errorf("error should be contained %v\n", barErr) } if errors.Is(err, bazErr) { t.Errorf("error should not be contained %v\n", bazErr) } } type foobarErr struct{ str string } func (e foobarErr) Error() string { return "foobar" } func TestGroup_multiple_tasks_errors_As(t *testing.T) { ctx := context.Background() g := NewGroup(ctx, 1) g.Go(func() error { return foobarErr{"baz"} }) g.Go(func() error { return nil }) err := g.Wait() if err == nil { t.Fatalf("g.Wait() should return an error") } var ( fbe foobarErr pe *os.PathError ) if !errors.As(err, &fbe) { t.Error("error should be matched foobarErr") } if errors.As(err, &pe) { t.Error("error should not be matched os.PathError") } }