github.com/Aoi-hosizora/ahlib@v1.5.1-0.20230404072829-241b93cf91c7/xerror/xerror.go (about)

     1  package xerror
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"strings"
     7  	"sync"
     8  )
     9  
    10  // ==========
    11  // interfaces
    12  // ==========
    13  
    14  // Wrapper is an interface used to identify errors which has Unwrap method, can be used in errors.Unwrap function.
    15  type Wrapper interface {
    16  	Unwrap() error
    17  }
    18  
    19  // Matcher is an interface used to identify errors which has Is method, can be used in errors.Is function.
    20  type Matcher interface {
    21  	Is(error) bool
    22  }
    23  
    24  // Assigner is an interface used to identify errors which has As method, can be used in errors.As function.
    25  type Assigner interface {
    26  	As(interface{}) bool
    27  }
    28  
    29  // ===========
    30  // multi error
    31  // ===========
    32  
    33  // MultiError is an interface representing error groups, types implement this interface can be returned by xerror.Combine or
    34  // several methods in github.com/uber-go/multierr package.
    35  type MultiError interface {
    36  	Errors() []error
    37  }
    38  
    39  // multiError is an unexported error type implements MultiError interface, can be returned by xerror.Combine.
    40  type multiError struct {
    41  	errs []error
    42  }
    43  
    44  var (
    45  	_ MultiError = (*multiError)(nil)
    46  )
    47  
    48  // Errors implements MultiError interface.
    49  func (m *multiError) Errors() []error {
    50  	return m.errs // items are all non-nillable, if used in a safe manner
    51  }
    52  
    53  // Is implements Matcher interface.
    54  func (m *multiError) Is(target error) bool {
    55  	for _, err := range m.errs {
    56  		if errors.Is(err, target) {
    57  			return true
    58  		}
    59  	}
    60  	return false
    61  }
    62  
    63  // As implements Assigner interface.
    64  func (m *multiError) As(target interface{}) bool {
    65  	for _, err := range m.errs {
    66  		if errors.As(err, target) {
    67  			return true
    68  		}
    69  	}
    70  	return false
    71  }
    72  
    73  // Error implements error interface.
    74  func (m *multiError) Error() string {
    75  	switch len(m.errs) {
    76  	case 0:
    77  		return ""
    78  	case 1:
    79  		return m.errs[0].Error() // non-nillable
    80  	}
    81  	sb := strings.Builder{}
    82  	for _, err := range m.errs {
    83  		if sb.Len() > 0 {
    84  			sb.WriteString("; ")
    85  		}
    86  		sb.WriteString(err.Error()) // non-nillable
    87  	}
    88  	return sb.String()
    89  }
    90  
    91  // Combine combines given errors to a single error, there are some situations:
    92  // 1. If pass empty errors, or all errors passed are nil, it will return a nil error.
    93  // 2. If pass a single non-nil error, it will return this single error directly.
    94  // 3. If more than one error passed are non-nil, it returns a MultiError containing all these non-nil errors.
    95  // 4. If some errors are MultiError, the internal errors contained will be flatted.
    96  func Combine(errs ...error) error {
    97  	switch len(errs) {
    98  	case 0:
    99  		return nil
   100  	case 1:
   101  		return errs[0] // maybe nil
   102  	}
   103  	notnil := make([]error, 0)
   104  	for _, err := range errs {
   105  		if err == nil {
   106  			continue
   107  		}
   108  		if me, ok := err.(MultiError); ok {
   109  			notnil = append(notnil, me.Errors()...)
   110  		} else {
   111  			notnil = append(notnil, err)
   112  		}
   113  	}
   114  	switch len(notnil) {
   115  	case 0:
   116  		return nil
   117  	case 1:
   118  		return notnil[0] // single error (non-nil)
   119  	default:
   120  		return &multiError{errs: notnil} // multiple errors (all non-nil)
   121  	}
   122  }
   123  
   124  // Separate separates given error to multiple errors that given error is composed of (that is MultiError). If given error is
   125  // nil, a nil slice is returned.
   126  func Separate(err error) []error {
   127  	if err == nil {
   128  		return nil
   129  	}
   130  	me, ok := err.(MultiError)
   131  	if !ok {
   132  		return []error{err}
   133  	}
   134  	errs := me.Errors()
   135  	out := make([]error, len(errs))
   136  	copy(out, errs)
   137  	return out
   138  }
   139  
   140  // ===========
   141  // error group
   142  // ===========
   143  
   144  // ErrorGroup is a sync.WaitGroup wrapper that can used to synchronization, error propagation, and context cancellation for
   145  // groups of goroutines, refers to https://pkg.go.dev/golang.org/x/sync/errgroup for more details.
   146  //
   147  // A zero ErrorGroup is also valid, which will create a cancelable context automatically for context cancellation when error.
   148  type ErrorGroup struct {
   149  	ctx    context.Context
   150  	cancel context.CancelFunc
   151  
   152  	wg       sync.WaitGroup
   153  	err      error
   154  	errMutex sync.RWMutex
   155  	errOnce  sync.Once
   156  
   157  	mu         sync.RWMutex
   158  	goExecutor func(f func())
   159  }
   160  
   161  // NewErrorGroup returns a new ErrorGroup with cancelable context derived from given context, and the default goroutine executor.
   162  func NewErrorGroup(ctx context.Context) *ErrorGroup {
   163  	ctx, cancel := context.WithCancel(ctx)
   164  	return &ErrorGroup{ctx: ctx, cancel: cancel, goExecutor: defaultExecutor}
   165  }
   166  
   167  // defaultExecutor is the default goroutine executor for ErrorGroup, including create goroutine by `go` keyword and panic
   168  // recovery with no logging.
   169  var defaultExecutor = func(f func()) {
   170  	go func() {
   171  		defer func() {
   172  			_ = recover()
   173  		}()
   174  		f()
   175  	}()
   176  }
   177  
   178  // SetGoExecutor sets goroutine executor, can be used to change the behavior of `go` keyword, you can use this executor to
   179  // add recover behavior for goroutine.
   180  //
   181  // Example:
   182  // 	// custom recover behavior
   183  // 	eg := NewErrorGroup(context.Background())
   184  // 	eg.SetGoExecutor(func(f func()) {
   185  // 		go func() {
   186  // 			defer func() {
   187  // 				if v := recover(); v != nil {
   188  // 					log.Printf("Warning: Panic with %v", v)
   189  // 				}
   190  // 			}()
   191  // 			f()
   192  // 		}()
   193  // 	})
   194  //
   195  // 	// use xgopool goroutine pool
   196  // 	eg := NewErrorGroup(context.Background())
   197  // 	gp := xgopool.New(int32(runtime.NumCPU() * 10))
   198  // 	gp.SetPanicHandler(func(_ context.Context, v interface{}) {
   199  // 		log.Printf("Warning: Panic with %v", v)
   200  // 	})
   201  // 	eg.SetGoExecutor(gp.Go)
   202  func (eg *ErrorGroup) SetGoExecutor(executor func(f func())) {
   203  	if executor != nil {
   204  		eg.mu.Lock()
   205  		eg.goExecutor = executor
   206  		eg.mu.Unlock()
   207  	}
   208  }
   209  
   210  // Go calls given function in a new goroutine using specific executor. The first call to return a non-nil error cancels the
   211  // group, its error will be returned by Wait.
   212  //
   213  // If using a zero ErrorGroup, ctx will be Background, otherwise it will be the context derived from given context passed
   214  // to NewErrorGroup.
   215  //
   216  // Example:
   217  // 	eg := NewErrorGroup(context.Background())
   218  //
   219  // 	// in select statement
   220  // 	eg.Go(func(ctx context.Context) error {
   221  // 		select {
   222  // 		case ...:
   223  // 		case <-ctx.Done():
   224  // 		}
   225  // 		return nil
   226  // 	})
   227  //
   228  // 	// in cancelable http requesting
   229  // 	eg.Go(func(ctx context.Context) error {
   230  // 		req, _ := http.NewRequestWithContext(ctx, "GET", "...", nil)
   231  // 		// ...
   232  // 		return nil
   233  // 	})
   234  func (eg *ErrorGroup) Go(f func(ctx context.Context) error) {
   235  	if f == nil {
   236  		return
   237  	}
   238  
   239  	// get executor and context
   240  	eg.mu.RLock()
   241  	executor := eg.goExecutor
   242  	ctx := eg.ctx
   243  	eg.mu.RUnlock()
   244  	if executor == nil {
   245  		eg.mu.Lock()
   246  		if eg.goExecutor == nil {
   247  			eg.goExecutor = defaultExecutor
   248  		}
   249  		executor = eg.goExecutor
   250  		eg.mu.Unlock()
   251  	}
   252  	if ctx == nil {
   253  		eg.mu.Lock()
   254  		if eg.ctx == nil {
   255  			ctx_ := context.Background()
   256  			ctx_, cancel := context.WithCancel(ctx_)
   257  			eg.ctx = ctx_
   258  			eg.cancel = cancel
   259  		}
   260  		ctx = eg.ctx
   261  		eg.mu.Unlock()
   262  	}
   263  
   264  	// execute with goroutine
   265  	eg.wg.Add(1)
   266  	executor(func() {
   267  		defer eg.wg.Done()
   268  
   269  		eg.errMutex.RLock()
   270  		can := eg.err == nil // check whether error is nil
   271  		eg.errMutex.RUnlock()
   272  		if !can {
   273  			return // err has already been recorded, reject to call function
   274  		}
   275  
   276  		err := f(ctx) // call given function
   277  		if err != nil {
   278  			eg.errOnce.Do(func() {
   279  				eg.errMutex.Lock()
   280  				eg.err = err // record the first error
   281  				eg.errMutex.Unlock()
   282  				if eg.cancel != nil {
   283  					eg.cancel() // also to cancel the context
   284  				}
   285  			})
   286  		}
   287  	})
   288  }
   289  
   290  // Reset resets states of ErrorGroup, including context and error. This method must be called if you want to reuse ErrorGroup
   291  // after Wait, when non-nil error is returned by Wait.
   292  func (eg *ErrorGroup) Reset(ctx context.Context) {
   293  	eg.mu.Lock()
   294  	defer eg.mu.Unlock()
   295  
   296  	ctx, cancel := context.WithCancel(ctx)
   297  	eg.ctx = ctx // reset
   298  	eg.cancel = cancel
   299  	eg.err = nil
   300  	eg.errOnce = sync.Once{}
   301  }
   302  
   303  // Wait blocks until all function calls from the Go method have returned, then returns the first non-nil error (if any)
   304  // from them. Note that if any error is returned, the current ErrorGroup can not be used again before calling Reset.
   305  func (eg *ErrorGroup) Wait() error {
   306  	eg.wg.Wait()
   307  	if eg.cancel != nil {
   308  		eg.cancel()
   309  	}
   310  	return eg.err
   311  }