Compare commits
No commits in common. "8eb46f8ea7c4f072b86d333132bfbeee4404813b" and "67eb618288cb9d6391e807ef66af29e80a4d7686" have entirely different histories.
8eb46f8ea7
...
67eb618288
6 changed files with 74 additions and 254 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,18 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## v1.0.0 - 2025-11-14
|
||||
|
||||
### Added
|
||||
- Refactored WaitErr into an interface and added a New constructor.
|
||||
- Added WithContext function for context cancellation.
|
||||
- Added CONTRIBUTING.md for human contributors.
|
||||
|
||||
### Changed
|
||||
- Updated README.md and example_test.go to reflect the new interface and WithContext function.
|
||||
- Updated README.md to reference CONTRIBUTING.md.
|
||||
|
||||
|
||||
|
||||
## v0.9.0 - 2025-11-13
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
# Contributing to waiterr
|
||||
|
||||
We welcome contributions to the `waiterr` project! Please take a moment to review these guidelines before submitting your contributions.
|
||||
|
||||
## Reporting Bugs and Suggesting Features
|
||||
|
||||
If you encounter a bug or have a feature request, please report it on our [Codeberg repository](https://codeberg.org/danjones000/waiterr/issues).
|
||||
|
||||
## Git Flow Guidelines
|
||||
|
||||
We follow a Git Flow branching model.
|
||||
|
||||
* **`develop` branch**: This is our main integration branch for new features and bug fixes.
|
||||
* **`stable` branch**: This branch contains the latest production-ready code.
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. **Branching**:
|
||||
* For new features or regular bug fixes, create a new branch from `develop` (e.g., `feat/your-feature-name` or `bug/your-bug-fix`).
|
||||
* For urgent hotfixes addressing critical issues in `stable`, create a branch directly from `stable` (e.g., `hot/your-hotfix-name`).
|
||||
|
||||
2. **Pull Requests (PRs)**:
|
||||
* All new features and regular bug fixes should be submitted as Pull Requests targeting the `develop` branch.
|
||||
* Hotfixes should be submitted as Pull Requests targeting the `stable` branch directly. After a hotfix is merged into `stable`, it must also be merged back into `develop`.
|
||||
|
||||
3. **Commit Messages**:
|
||||
* It's not *required* that you follow the [Gitmoji convention](https://gitmoji.dev/) for your commit messages, but it would make me happy if you did. 😏
|
||||
* Write clear, concise, and descriptive commit messages that explain *what* changed and *why*.
|
||||
|
||||
## Code Style
|
||||
|
||||
Please ensure your code adheres to the existing Go code style and formatting conventions used in the project. Run `go fmt ./...` and `go mod tidy` before submitting your changes.
|
||||
|
||||
Thank you for contributing!
|
||||
42
README.md
42
README.md
|
|
@ -33,7 +33,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
we := waiterr.New()
|
||||
we := new(waiterr.WaitErr)
|
||||
|
||||
we.Go(func() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
|
@ -60,7 +60,7 @@ func main() {
|
|||
}
|
||||
|
||||
// You can also get the first error immediately
|
||||
we2 := waiterr.New()
|
||||
we2 := new(waiterr.WaitErr)
|
||||
we2.Go(func() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return errors.New("first error from we2")
|
||||
|
|
@ -87,45 +87,9 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
### Using WithContext
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"codeberg.org/danjones000/waiterr"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
we, ctx := waiterr.WithContext(ctx)
|
||||
|
||||
we.Go(func() error {
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
fmt.Println("Task completed")
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
fmt.Println("Task cancelled")
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
_ = we.Wait()
|
||||
// Output:
|
||||
// Task completed
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on contributing to this project, including code style, commit messages, and Git workflow.
|
||||
Please refer to the [AGENTS.md](AGENTS.md) file for guidelines on contributing to this project, including code style, commit messages, and Git workflow.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package waiterr_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
|
@ -10,7 +9,8 @@ import (
|
|||
)
|
||||
|
||||
func Example() {
|
||||
we := waiterr.New()
|
||||
we := new(waiterr.WaitErr)
|
||||
|
||||
we.Go(func() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fmt.Println("Goroutine 1 finished")
|
||||
|
|
@ -42,8 +42,8 @@ func Example() {
|
|||
// something went wrong in goroutine 2
|
||||
}
|
||||
|
||||
func Example_waitForError() {
|
||||
we := waiterr.New()
|
||||
func ExampleWaitErr_WaitForError() {
|
||||
we := new(waiterr.WaitErr)
|
||||
we.Go(func() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return errors.New("first error from we")
|
||||
|
|
@ -62,8 +62,8 @@ func Example_waitForError() {
|
|||
|
||||
}
|
||||
|
||||
func Example_unwrap() {
|
||||
we := waiterr.New()
|
||||
func ExampleWaitErr_Unwrap() {
|
||||
we := new(waiterr.WaitErr)
|
||||
we.Go(func() error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return errors.New("first error from we")
|
||||
|
|
@ -88,50 +88,3 @@ func Example_unwrap() {
|
|||
// second error from we
|
||||
// first error from we
|
||||
}
|
||||
|
||||
func ExampleWithContext() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
we, ctx := waiterr.WithContext(ctx)
|
||||
|
||||
we.Go(func() error {
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
fmt.Println("Goroutine 1 finished")
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
we.Go(func() error {
|
||||
select {
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
fmt.Println("Goroutine 2 finished with an error")
|
||||
return errors.New("something went wrong in goroutine 2")
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
we.Go(func() error {
|
||||
select {
|
||||
case <-time.After(150 * time.Millisecond):
|
||||
fmt.Println("Goroutine 3 finished")
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
})
|
||||
|
||||
if err := we.Wait(); err != nil {
|
||||
fmt.Printf("All goroutines finished. Combined error: %s\n", err)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// Goroutine 2 finished with an error
|
||||
// All goroutines finished. Combined error: something went wrong in goroutine 2
|
||||
// context canceled
|
||||
// context canceled
|
||||
}
|
||||
|
|
|
|||
70
waiterr.go
70
waiterr.go
|
|
@ -1,59 +1,27 @@
|
|||
package waiterr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func New() WaitErr {
|
||||
var we waitErr
|
||||
we.errCh = make(chan error, 1)
|
||||
we.cancel = func(error) {}
|
||||
|
||||
return &we
|
||||
}
|
||||
|
||||
func WithContext(ctx context.Context) (WaitErr, context.Context) {
|
||||
var we waitErr
|
||||
we.errCh = make(chan error, 1)
|
||||
cCtx, canc := context.WithCancelCause(ctx)
|
||||
we.cancel = canc
|
||||
|
||||
return &we, cCtx
|
||||
}
|
||||
|
||||
// WaitErr provides a way to run multiple goroutines and wait for their completion,
|
||||
// collecting any errors they return.
|
||||
type WaitErr interface {
|
||||
// Go runs f in its own goroutine. When f returns, its error is stored, and returned
|
||||
// with [WaitErr.Wait].
|
||||
Go(f func() error)
|
||||
// WaitForError waits for the first error to be returned by one of our go routines, and immediately returns
|
||||
// with that error. If all functions return successfully, a nil is returned.
|
||||
WaitForError() error
|
||||
// Wait for all current goroutines to finish. Return an error that combines all errors returned
|
||||
// in the group so far (if any).
|
||||
Wait() error
|
||||
// Unwrap returns all non-nil errors returned by our functions.
|
||||
// If no errors were returned, or all errors are nil, it returns nil.
|
||||
Unwrap() []error
|
||||
}
|
||||
|
||||
// WaitErr wraps a [sync.WaitGroup] with error handling.
|
||||
type waitErr struct {
|
||||
wg sync.WaitGroup
|
||||
errs []error
|
||||
mut sync.RWMutex
|
||||
firstErr error
|
||||
firstErrOnce sync.Once
|
||||
errCh chan error // Buffered channel of size 1
|
||||
cancel context.CancelCauseFunc
|
||||
type WaitErr struct {
|
||||
wg sync.WaitGroup
|
||||
errs []error
|
||||
mut sync.RWMutex
|
||||
firstErr error
|
||||
firstErrOnce sync.Once
|
||||
errCh chan error // Buffered channel of size 1
|
||||
initErrChOnce sync.Once
|
||||
}
|
||||
|
||||
// Go runs f in its own goroutine. When f returns, its error is stored, and returned
|
||||
// with [WaitErr.Wait].
|
||||
func (we *waitErr) Go(f func() error) {
|
||||
func (we *WaitErr) Go(f func() error) {
|
||||
we.initErrChOnce.Do(func() {
|
||||
we.errCh = make(chan error, 1)
|
||||
})
|
||||
wrap := func() {
|
||||
err := f()
|
||||
|
||||
|
|
@ -61,7 +29,6 @@ func (we *waitErr) Go(f func() error) {
|
|||
we.firstErrOnce.Do(func() {
|
||||
we.mut.Lock() // Acquire lock before writing to firstErr
|
||||
we.firstErr = err
|
||||
we.cancel(err)
|
||||
we.mut.Unlock() // Release lock after writing
|
||||
|
||||
// Non-blocking send to errCh
|
||||
|
|
@ -81,7 +48,10 @@ func (we *waitErr) Go(f func() error) {
|
|||
|
||||
// WaitForError waits for the first error to be returned by one of our go routines, and immediately returns
|
||||
// with that error. If all functions return successfully, a nil is returned. It will panic if called before Go.
|
||||
func (we *waitErr) WaitForError() error {
|
||||
func (we *WaitErr) WaitForError() error {
|
||||
if we.errCh == nil {
|
||||
panic("WaitForError called before Go")
|
||||
}
|
||||
// Check if an error has already been set
|
||||
we.mut.RLock()
|
||||
if we.firstErr != nil {
|
||||
|
|
@ -112,18 +82,16 @@ func (we *waitErr) WaitForError() error {
|
|||
|
||||
// Wait for all current goroutines to finish. Return an error that combines all errors returned
|
||||
// in the group so far (if any).
|
||||
func (we *waitErr) Wait() error {
|
||||
func (we *WaitErr) Wait() error {
|
||||
we.wg.Wait()
|
||||
we.mut.RLock()
|
||||
defer we.mut.RUnlock()
|
||||
ret := errors.Join(we.errs...)
|
||||
we.cancel(ret)
|
||||
return ret
|
||||
return errors.Join(we.errs...)
|
||||
}
|
||||
|
||||
// Unwrap returns all non-nil errors returned by our functions.
|
||||
// If no errors were returned, or all errors are nil, it returns nil.
|
||||
func (we *waitErr) Unwrap() []error {
|
||||
func (we *WaitErr) Unwrap() []error {
|
||||
errs := make([]error, 0, len(we.errs))
|
||||
for _, e := range we.errs {
|
||||
if e != nil {
|
||||
|
|
|
|||
110
waiterr_test.go
110
waiterr_test.go
|
|
@ -1,7 +1,6 @@
|
|||
package waiterr_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
|
|
@ -12,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestGo(t *testing.T) {
|
||||
we := waiterr.New()
|
||||
we := new(waiterr.WaitErr)
|
||||
err := errors.New("uh-oh")
|
||||
var run bool
|
||||
we.Go(func() error {
|
||||
|
|
@ -25,7 +24,7 @@ func TestGo(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWait(t *testing.T) {
|
||||
we := waiterr.New()
|
||||
we := new(waiterr.WaitErr)
|
||||
er1 := errors.New("uh-oh")
|
||||
er2 := errors.New("oops")
|
||||
we.Go(func() error { return er1 })
|
||||
|
|
@ -44,53 +43,32 @@ func TestWait(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWaitForError(tt *testing.T) {
|
||||
tt.Run("first error", func(t *testing.T) {
|
||||
we := waiterr.New()
|
||||
er1 := errors.New("uh-oh")
|
||||
er2 := errors.New("oops")
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return er1 })
|
||||
we.Go(func() error { return er2 })
|
||||
func TestWaitForError(t *testing.T) {
|
||||
we := new(waiterr.WaitErr)
|
||||
er1 := errors.New("uh-oh")
|
||||
er2 := errors.New("oops")
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return er1 })
|
||||
we.Go(func() error { return er2 })
|
||||
|
||||
err := we.WaitForError()
|
||||
// Due to how goroutines run, it is possible that either of those return first. This is an acceptable limitation
|
||||
be.True(t, err == er1 || err == er2)
|
||||
})
|
||||
err := we.WaitForError()
|
||||
// Due to how goroutines run, it is possible that either of those return first. This is an acceptable limitation
|
||||
be.True(t, err == er1 || err == er2)
|
||||
}
|
||||
|
||||
tt.Run("no error", func(t *testing.T) {
|
||||
we := waiterr.New()
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return nil })
|
||||
func TestWaitForErrorNoErr(t *testing.T) {
|
||||
we := new(waiterr.WaitErr)
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return nil })
|
||||
|
||||
err := we.WaitForError()
|
||||
be.Err(t, err, nil)
|
||||
})
|
||||
|
||||
tt.Run("first error set", func(tt2 *testing.T) {
|
||||
we := waiterr.New()
|
||||
expectedErr := errors.New("pre-set error")
|
||||
|
||||
synctest.Test(tt2, func(t *testing.T) {
|
||||
we.Go(func() error { return expectedErr })
|
||||
// synctest.Wait ensures that the gorouting has finished before anything else.
|
||||
synctest.Wait()
|
||||
|
||||
we.Go(func() error { return errors.New("another error") })
|
||||
synctest.Wait()
|
||||
|
||||
we.Go(func() error { return nil })
|
||||
|
||||
actualErr := we.WaitForError()
|
||||
be.Err(t, actualErr, expectedErr)
|
||||
})
|
||||
})
|
||||
err := we.WaitForError()
|
||||
be.Err(t, err, nil)
|
||||
}
|
||||
|
||||
func TestUnwrap(tt *testing.T) {
|
||||
tt.Run("two errors", func(t *testing.T) {
|
||||
we := waiterr.New()
|
||||
we := new(waiterr.WaitErr)
|
||||
er1 := errors.New("error one")
|
||||
er2 := errors.New("error two")
|
||||
|
||||
|
|
@ -108,7 +86,7 @@ func TestUnwrap(tt *testing.T) {
|
|||
})
|
||||
|
||||
tt.Run("no errors", func(t *testing.T) {
|
||||
weNoErr := waiterr.New()
|
||||
weNoErr := new(waiterr.WaitErr)
|
||||
weNoErr.Go(func() error { return nil })
|
||||
weNoErr.Go(func() error { return nil })
|
||||
_ = weNoErr.Wait()
|
||||
|
|
@ -116,28 +94,32 @@ func TestUnwrap(tt *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestWithContext(tt *testing.T) {
|
||||
tt.Run("with error", func(tt2 *testing.T) {
|
||||
er1 := errors.New("uh-oh")
|
||||
er2 := errors.New("oops")
|
||||
synctest.Test(tt2, func(t *testing.T) {
|
||||
we, ctx := waiterr.WithContext(t.Context())
|
||||
we.Go(func() error { return er1 })
|
||||
synctest.Wait() // Ensure it finishes first
|
||||
we.Go(func() error { return er2 })
|
||||
func TestWaitForErrorPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("The code did not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
er := context.Cause(ctx)
|
||||
be.Err(t, er, er1)
|
||||
be.True(t, !errors.Is(er, er2))
|
||||
})
|
||||
})
|
||||
we := new(waiterr.WaitErr)
|
||||
_ = we.WaitForError()
|
||||
}
|
||||
|
||||
func TestWaitForErrorFirstErrSet(tt *testing.T) {
|
||||
we := new(waiterr.WaitErr)
|
||||
expectedErr := errors.New("pre-set error")
|
||||
|
||||
synctest.Test(tt, func(t *testing.T) {
|
||||
we.Go(func() error { return expectedErr })
|
||||
// synctest.Wait ensures that the gorouting has finished before anything else.
|
||||
synctest.Wait()
|
||||
|
||||
we.Go(func() error { return errors.New("another error") })
|
||||
synctest.Wait()
|
||||
|
||||
tt.Run("no error", func(t *testing.T) {
|
||||
we, ctx := waiterr.WithContext(t.Context())
|
||||
we.Go(func() error { return nil })
|
||||
we.Go(func() error { return nil })
|
||||
er := we.Wait()
|
||||
be.Err(t, er, nil)
|
||||
be.Err(t, context.Cause(ctx), context.Canceled)
|
||||
|
||||
actualErr := we.WaitForError()
|
||||
be.Err(t, actualErr, expectedErr)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue