mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-09 14:16:14 +02:00
Add a new function to the util/multierr package that allows deduplicating the `context.Canceled` and `context.DeadlineExceeded` errors from a slice, and then use that to reduce log spam in controlhttp. Updates #14233 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: I5a121124fe27c4449705ba35de6faf83665db5fe
175 lines
3.9 KiB
Go
175 lines
3.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package multierr provides a simple multiple-error type.
|
|
// It was inspired by github.com/go-multierror/multierror.
|
|
package multierr
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// An Error represents multiple errors.
|
|
type Error struct {
|
|
errs []error
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e Error) Error() string {
|
|
s := new(strings.Builder)
|
|
s.WriteString("multiple errors:")
|
|
for _, err := range e.errs {
|
|
s.WriteString("\n\t")
|
|
s.WriteString(err.Error())
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
// Errors returns a slice containing all errors in e.
|
|
func (e Error) Errors() []error {
|
|
return slices.Clone(e.errs)
|
|
}
|
|
|
|
// Unwrap returns the underlying errors as-is.
|
|
func (e Error) Unwrap() []error {
|
|
// Do not clone since Unwrap requires callers to not mutate the slice.
|
|
// See the documentation in the Go "errors" package.
|
|
return e.errs
|
|
}
|
|
|
|
// New returns an error composed from errs.
|
|
// Some errors in errs get special treatment:
|
|
// - nil errors are discarded
|
|
// - errors of type Error are expanded into the top level
|
|
//
|
|
// If the resulting slice has length 0, New returns nil.
|
|
// If the resulting slice has length 1, New returns that error.
|
|
// If the resulting slice has length > 1, New returns that slice as an Error.
|
|
func New(errs ...error) error {
|
|
// First count the number of errors to avoid allocating.
|
|
var n int
|
|
var errFirst error
|
|
for _, e := range errs {
|
|
switch e := e.(type) {
|
|
case nil:
|
|
continue
|
|
case Error:
|
|
n += len(e.errs)
|
|
if errFirst == nil && len(e.errs) > 0 {
|
|
errFirst = e.errs[0]
|
|
}
|
|
default:
|
|
n++
|
|
if errFirst == nil {
|
|
errFirst = e
|
|
}
|
|
}
|
|
}
|
|
if n <= 1 {
|
|
return errFirst // nil if n == 0
|
|
}
|
|
|
|
// More than one error, allocate slice and construct the multi-error.
|
|
dst := make([]error, 0, n)
|
|
for _, e := range errs {
|
|
switch e := e.(type) {
|
|
case nil:
|
|
continue
|
|
case Error:
|
|
dst = append(dst, e.errs...)
|
|
default:
|
|
dst = append(dst, e)
|
|
}
|
|
}
|
|
return Error{errs: dst}
|
|
}
|
|
|
|
// Is reports whether any error in e matches target.
|
|
func (e Error) Is(target error) bool {
|
|
for _, err := range e.errs {
|
|
if errors.Is(err, target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// As finds the first error in e that matches target, and if any is found,
|
|
// sets target to that error value and returns true. Otherwise, it returns false.
|
|
func (e Error) As(target any) bool {
|
|
for _, err := range e.errs {
|
|
if ok := errors.As(err, target); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Range performs a pre-order, depth-first iteration of the error tree
|
|
// by successively unwrapping all error values.
|
|
// For each iteration it calls fn with the current error value and
|
|
// stops iteration if it ever reports false.
|
|
func Range(err error, fn func(error) bool) bool {
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if !fn(err) {
|
|
return false
|
|
}
|
|
switch err := err.(type) {
|
|
case interface{ Unwrap() error }:
|
|
if err := err.Unwrap(); err != nil {
|
|
if !Range(err, fn) {
|
|
return false
|
|
}
|
|
}
|
|
case interface{ Unwrap() []error }:
|
|
for _, err := range err.Unwrap() {
|
|
if !Range(err, fn) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// DeduplicateContextErrors returns a new slice of errors with at most one
|
|
// occurrence of each [context.Canceled] or [context.DeadlineExceeded], if one
|
|
// or more of them are present in the input slice.
|
|
//
|
|
// All other non-nil errors are returned as-is; nil errors are skipped.
|
|
func DeduplicateContextErrors(errs []error) []error {
|
|
// preserve nil/non-nil distinction
|
|
if errs == nil {
|
|
return nil
|
|
} else if len(errs) == 0 {
|
|
return []error{}
|
|
}
|
|
|
|
var (
|
|
ret []error
|
|
sawCanceled, sawDeadline bool
|
|
)
|
|
for _, err := range errs {
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if errors.Is(err, context.Canceled) {
|
|
if sawCanceled {
|
|
continue
|
|
}
|
|
sawCanceled = true
|
|
} else if errors.Is(err, context.DeadlineExceeded) {
|
|
if sawDeadline {
|
|
continue
|
|
}
|
|
sawDeadline = true
|
|
}
|
|
ret = append(ret, err)
|
|
}
|
|
return ret
|
|
}
|