mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-26 05:59:02 -04:00
Bumps [github.com/olekukonko/errors](https://github.com/olekukonko/errors) from 1.2.0 to 1.3.0. - [Commits](https://github.com/olekukonko/errors/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: github.com/olekukonko/errors dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
310 lines
7.5 KiB
Go
310 lines
7.5 KiB
Go
// Channel-based error utilities and streaming error collection.
|
|
// All functions compose with the standard (chan T, chan error) idiom.
|
|
|
|
package errors
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// ErrLimitReached is returned by Collect when n errors have been gathered
|
|
// before the channel closed or the context was done.
|
|
// Callers can use errors.Is(err, ErrLimitReached) to distinguish this case.
|
|
var ErrLimitReached = Const("limit_reached", "error collection limit reached")
|
|
|
|
// Drain reads all errors from ch until it is closed and returns them as a
|
|
// *MultiError. Returns nil if every received value was nil.
|
|
// Blocks until ch is closed.
|
|
//
|
|
// Example:
|
|
//
|
|
// results, errs := processItems(ctx, items)
|
|
// if err := errors.Drain(errs); err != nil {
|
|
// log.Println(err)
|
|
// }
|
|
func Drain(ch <-chan error) error {
|
|
m := NewMultiError()
|
|
for err := range ch {
|
|
if err != nil {
|
|
m.Add(err)
|
|
}
|
|
}
|
|
return m.Single()
|
|
}
|
|
|
|
// First returns the first non-nil error received from ch, then returns.
|
|
// Uses ctx for deadline/cancellation only — it does NOT call any cancel
|
|
// function. The caller is responsible for cancelling sibling work after
|
|
// First returns.
|
|
//
|
|
// Returns nil if ch is closed before any error arrives, or ctx is done.
|
|
// Returns ctx.Err() if the context is cancelled or times out.
|
|
//
|
|
// Example:
|
|
//
|
|
// ctx, cancel := context.WithCancel(context.Background())
|
|
// results, errs := processItems(ctx, items)
|
|
// if err := errors.First(ctx, errs); err != nil {
|
|
// cancel() // caller decides to stop siblings
|
|
// log.Println(err)
|
|
// }
|
|
// defer cancel()
|
|
func First(ctx context.Context, ch <-chan error) error {
|
|
for {
|
|
select {
|
|
case err, ok := <-ch:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fan merges multiple error channels into a single output channel that closes
|
|
// when all inputs have closed or ctx is done.
|
|
//
|
|
// Callers MUST either drain the returned channel to completion OR cancel ctx —
|
|
// failing to do so leaks the internal goroutines. The select in each forwarder
|
|
// respects ctx.Done() so cancellation is always safe.
|
|
//
|
|
// Example:
|
|
//
|
|
// errs1, errs2 := stage1(ctx), stage2(ctx)
|
|
// for err := range errors.Fan(ctx, errs1, errs2) {
|
|
// if err != nil { log.Println(err) }
|
|
// }
|
|
func Fan(ctx context.Context, chans ...<-chan error) <-chan error {
|
|
bufSize := len(chans)
|
|
if bufSize < 1 {
|
|
bufSize = 1
|
|
}
|
|
|
|
out := make(chan error, bufSize)
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(chans))
|
|
|
|
for _, ch := range chans {
|
|
ch := ch
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case err, ok := <-ch:
|
|
if !ok {
|
|
return
|
|
}
|
|
select {
|
|
case out <- err:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(out)
|
|
}()
|
|
|
|
return out
|
|
}
|
|
|
|
// Collect reads up to n non-nil errors from ch (or until ch closes or ctx is
|
|
// done) and returns them as a *MultiError.
|
|
//
|
|
// If the limit n is reached before the channel closes, the returned error
|
|
// wraps ErrLimitReached as its cause so callers can distinguish the two cases:
|
|
//
|
|
// err := errors.Collect(ctx, errs, 10)
|
|
// if errors.Is(err, errors.ErrLimitReached) {
|
|
// // stopped early — more errors may exist
|
|
// }
|
|
func Collect(ctx context.Context, ch <-chan error, n int) error {
|
|
m := NewMultiError(WithLimit(n))
|
|
for {
|
|
select {
|
|
case err, ok := <-ch:
|
|
if !ok {
|
|
return m.Single()
|
|
}
|
|
if err != nil {
|
|
m.Add(err)
|
|
if m.Count() >= n {
|
|
// Wrap in an *Error so errors.Is traversal finds ErrLimitReached.
|
|
e := New(fmt.Sprintf("collected %d errors (limit reached)", n))
|
|
e.cause = ErrLimitReached
|
|
if inner := m.Single(); inner != nil {
|
|
return New(e.Error()).Wrap(inner).Wrap(ErrLimitReached)
|
|
}
|
|
return e
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
return m.Single()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stream — concurrent item processing with progressive error collection
|
|
|
|
// Stream processes a slice of items concurrently, collecting errors as they
|
|
// occur without stopping execution. Use Wait() to block until all items are
|
|
// done, or Each() to process errors as they arrive.
|
|
//
|
|
// Example — collect all:
|
|
//
|
|
// s := errors.NewStream(ctx, items, process, 8)
|
|
// if err := s.Wait(); err != nil {
|
|
// log.Println(err)
|
|
// }
|
|
//
|
|
// Example — process as they arrive:
|
|
//
|
|
// s := errors.NewStream(ctx, items, process, 8)
|
|
// s.Each(func(err error) { log.Println(err) })
|
|
type Stream[T any] struct {
|
|
ch chan error
|
|
done chan struct{}
|
|
stopCh chan struct{}
|
|
closeOnce sync.Once
|
|
stopOnce sync.Once
|
|
|
|
// consumed guards Each/Wait — only one consumer is permitted.
|
|
// 0 = available, 1 = consumed. Enforced with atomic CAS.
|
|
consumed atomic.Int32
|
|
}
|
|
|
|
// NewStream creates a Stream that applies fn to every item in items using
|
|
// up to workers concurrent goroutines.
|
|
//
|
|
// workers <= 0 defaults to len(items), running all items at once.
|
|
// Respects ctx: in-flight work completes but no new items start once ctx
|
|
// is done.
|
|
//
|
|
// Example:
|
|
//
|
|
// s := errors.NewStream(ctx, urls, func(url string) error {
|
|
// return fetch(url)
|
|
// }, 8)
|
|
func NewStream[T any](ctx context.Context, items []T, fn func(T) error, workers ...int) *Stream[T] {
|
|
w := len(items)
|
|
if len(workers) > 0 && workers[0] > 0 {
|
|
w = workers[0]
|
|
}
|
|
if w < 1 {
|
|
w = 1
|
|
}
|
|
|
|
s := &Stream[T]{
|
|
ch: make(chan error, w),
|
|
done: make(chan struct{}),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
|
|
go s.run(ctx, items, fn, w)
|
|
return s
|
|
}
|
|
|
|
func (s *Stream[T]) run(ctx context.Context, items []T, fn func(T) error, workers int) {
|
|
defer func() {
|
|
s.closeOnce.Do(func() { close(s.ch) })
|
|
close(s.done)
|
|
}()
|
|
|
|
work := make(chan T, workers)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for item := range work {
|
|
if err := fn(item); err != nil {
|
|
select {
|
|
case s.ch <- err:
|
|
case <-s.stopCh:
|
|
return
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
feed:
|
|
for _, item := range items {
|
|
select {
|
|
case work <- item:
|
|
case <-s.stopCh:
|
|
break feed
|
|
case <-ctx.Done():
|
|
break feed
|
|
}
|
|
}
|
|
close(work)
|
|
wg.Wait()
|
|
}
|
|
|
|
// acquireConsumer atomically marks the stream as consumed.
|
|
// Panics if called more than once — Each and Wait are mutually exclusive.
|
|
func (s *Stream[T]) acquireConsumer(name string) {
|
|
if !s.consumed.CompareAndSwap(0, 1) {
|
|
panic(fmt.Sprintf("errors.Stream: %s called on an already-consumed Stream; Each and Wait are mutually exclusive", name))
|
|
}
|
|
}
|
|
|
|
// Each calls fn for every error produced by the stream, in the order they
|
|
// arrive. Blocks until all items have been processed.
|
|
//
|
|
// Panics if called after Wait (or a second call to Each).
|
|
func (s *Stream[T]) Each(fn func(error)) {
|
|
s.acquireConsumer("Each")
|
|
for err := range s.ch {
|
|
fn(err)
|
|
}
|
|
<-s.done
|
|
}
|
|
|
|
// Wait blocks until all items have been processed and returns a *MultiError
|
|
// containing every error collected, or nil if all items succeeded.
|
|
//
|
|
// Panics if called after Each (or a second call to Wait).
|
|
func (s *Stream[T]) Wait() error {
|
|
s.acquireConsumer("Wait")
|
|
m := NewMultiError()
|
|
for err := range s.ch {
|
|
m.Add(err)
|
|
}
|
|
<-s.done
|
|
return m.Single()
|
|
}
|
|
|
|
// Stop signals the stream to stop processing new items and drains any
|
|
// buffered errors in the background. Safe to call multiple times.
|
|
// After Stop, Wait and Each will still return promptly but may not see
|
|
// all errors.
|
|
func (s *Stream[T]) Stop() {
|
|
s.stopOnce.Do(func() {
|
|
close(s.stopCh)
|
|
// Drain the error channel in the background so run() goroutines
|
|
// are not blocked trying to send, preventing a goroutine leak.
|
|
go func() {
|
|
for range s.ch {
|
|
}
|
|
}()
|
|
})
|
|
}
|