sq/libsq/core/errz/multi.go
Neil O'Toole db55986980
#307: Ingest cache (#354)
- Support for ingest cache, download cache, and progress bars.
2024-01-14 18:45:34 -07:00

534 lines
14 KiB
Go

package errz
// ACKNOWLEDGEMENT: This code is lifted from uber's multierr package.
import (
"bytes"
"errors"
"fmt"
"io"
"log/slog"
"strings"
"sync"
"sync/atomic"
"github.com/samber/lo"
)
// Copyright (c) 2017-2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Package multierr allows combining one or more errors together.
//
// # Overview
//
// Errors can be combined with the use of the Combine function.
//
// multierr.Combine(
// reader.Close(),
// writer.Close(),
// conn.Close(),
// )
//
// If only two errors are being combined, the Append function may be used
// instead.
//
// err = multierr.Append(reader.Close(), writer.Close())
//
// The underlying list of errors for a returned error object may be retrieved
// with the Errors function.
//
// errors := multierr.Errors(err)
// if len(errors) > 0 {
// fmt.Println("The following errors occurred:", errors)
// }
//
// # Appending from a loop
//
// You sometimes need to append into an error from a loop.
//
// var err error
// for _, item := range items {
// err = multierr.Append(err, process(item))
// }
//
// Cases like this may require knowledge of whether an individual instance
// failed. This usually requires introduction of a new variable.
//
// var err error
// for _, item := range items {
// if perr := process(item); perr != nil {
// log.Warn("skipping item", item)
// err = multierr.Append(err, perr)
// }
// }
//
// multierr includes AppendInto to simplify cases like this.
//
// var err error
// for _, item := range items {
// if multierr.AppendInto(&err, process(item)) {
// log.Warn("skipping item", item)
// }
// }
//
// This will append the error into the err variable, and return true if that
// individual error was non-nil.
//
// See [AppendInto] for more information.
//
// # Deferred Functions
//
// Go makes it possible to modify the return value of a function in a defer
// block if the function was using named returns. This makes it possible to
// record resource cleanup failures from deferred blocks.
//
// func sendRequest(req Request) (err error) {
// conn, err := openConnection()
// if err != nil {
// return err
// }
// defer func() {
// err = multierr.Append(err, conn.Close())
// }()
// // ...
// }
//
// multierr provides the Invoker type and AppendInvoke function to make cases
// like the above simpler and obviate the need for a closure. The following is
// roughly equivalent to the example above.
//
// func sendRequest(req Request) (err error) {
// conn, err := openConnection()
// if err != nil {
// return err
// }
// defer multierr.AppendInvoke(&err, multierr.Close(conn))
// // ...
// }
//
// See [AppendInvoke] and [Invoker] for more information.
//
// NOTE: If you're modifying an error from inside a defer, you MUST use a named
// return value for that function.
//
// # Advanced Usage
//
// Errors returned by Combine and Append MAY implement the following
// interface.
//
// type errorGroup interface {
// // Returns a slice containing the underlying list of errors.
// //
// // This slice MUST NOT be modified by the caller.
// Errors() []error
// }
//
// Note that if you need access to list of errors behind a multierr error, you
// should prefer using the Errors function. That said, if you need cheap
// read-only access to the underlying errors slice, you can attempt to cast
// the error to this interface. You MUST handle the failure case gracefully
// because errors returned by Combine and Append are not guaranteed to
// implement this interface.
//
// var errors []error
// group, ok := err.(errorGroup)
// if ok {
// errors = group.Errors()
// } else {
// errors = []error{err}
// }
var (
// Separator for single-line error messages.
_singlelineSeparator = []byte("; ")
// Prefix for multi-line messages.
_multilinePrefix = []byte("the following errors occurred:")
// Prefix for the first and following lines of an item in a list of
// multi-line error messages.
//
// For example, if a single item is:
//
// foo
// bar
//
// It will become,
//
// - foo
// bar
_multilineSeparator = []byte("\n - ")
_multilineIndent = []byte(" ")
)
// _bufferPool is a pool of bytes.Buffers.
var _bufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
// Errors returns a slice containing zero or more errors that the supplied
// error is composed of. If the error is nil, a nil slice is returned.
//
// err := multierr.Append(r.Close(), w.Close())
// errors := multierr.Errors(err)
//
// If the error is not composed of other errors, the returned slice contains
// just the error that was passed in.
//
// Callers of this function are free to modify the returned slice.
func Errors(err error) []error {
return extractErrors(err)
}
// multiErr is an error that holds one or more errors.
//
// An instance of this is guaranteed to be non-empty and flattened. That is,
// none of the errors inside multiErr are other multiErrors.
//
// multiErr formats to a semicolon delimited list of error messages with
// %v and with a more readable multi-line format with %+v.
type multiErr struct { //nolint:errname
copyNeeded atomic.Bool
errors []error
*stack
}
// inner implements stackTracer.
func (merr *multiErr) inner() error { return nil }
// stackTrace implements stackTracer.
func (merr *multiErr) stackTrace() *StackTrace {
if merr == nil || merr.stack == nil {
return nil
}
st := merr.stack.stackTrace()
if st != nil {
st.Error = merr
}
return st
}
// Errors returns the list of underlying errors.
//
// This slice MUST NOT be modified.
func (merr *multiErr) Errors() []error {
if merr == nil {
return nil
}
return merr.errors
}
func (merr *multiErr) Error() string {
if merr == nil {
return ""
}
buff, _ := _bufferPool.Get().(*bytes.Buffer)
buff.Reset()
merr.writeSingleline(buff)
result := buff.String()
_bufferPool.Put(buff)
return result
}
// LogValue implements [slog.LogValuer].
func (merr *multiErr) LogValue() slog.Value {
if merr == nil {
return slog.Value{}
}
attrs := make([]slog.Attr, 4)
attrs[0] = slog.String("msg", merr.Error())
attrs[1] = slog.String("type", fmt.Sprintf("%T", merr))
attrs[2] = slog.String("tree", SprintTreeTypes(merr))
errs := lo.Map(merr.Errors(), func(err error, i int) string { return err.Error() })
attrs[3] = slog.Any("errors", errs)
return slog.GroupValue(attrs...)
}
func (merr *multiErr) Format(f fmt.State, c rune) {
if c == 'v' && f.Flag('+') {
merr.writeMultiline(f)
} else {
merr.writeSingleline(f)
}
}
func (merr *multiErr) writeSingleline(w io.Writer) {
first := true
for _, item := range merr.errors {
if first {
first = false
} else {
_, _ = w.Write(_singlelineSeparator)
}
_, _ = io.WriteString(w, item.Error())
}
}
func (merr *multiErr) writeMultiline(w io.Writer) {
_, _ = w.Write(_multilinePrefix)
for _, item := range merr.errors {
_, _ = w.Write(_multilineSeparator)
writePrefixLine(w, _multilineIndent, fmt.Sprintf("%+v", item))
}
}
// Writes s to the writer with the given prefix added before each line after
// the first.
func writePrefixLine(w io.Writer, prefix []byte, s string) {
first := true
for len(s) > 0 {
if first {
first = false
} else {
_, _ = w.Write(prefix)
}
idx := strings.IndexByte(s, '\n')
if idx < 0 {
idx = len(s) - 1
}
_, _ = io.WriteString(w, s[:idx+1])
s = s[idx+1:]
}
}
type inspectResult struct {
// Number of top-level non-nil errors
Count int
// Total number of errors including multiErrors
Capacity int
// Index of the first non-nil error in the list. Value is meaningless if
// Count is zero.
FirstErrorIdx int
// Whether the list contains at least one multiErr
ContainsMultiError bool
}
// Inspects the given slice of errors so that we can efficiently allocate
// space for it.
func inspect(errs []error) (res inspectResult) {
first := true
for i, err := range errs {
if err == nil {
continue
}
res.Count++
if first {
first = false
res.FirstErrorIdx = i
}
if merr, ok := err.(*multiErr); ok { //nolint:errorlint
res.Capacity += len(merr.errors)
res.ContainsMultiError = true
} else {
res.Capacity++
}
}
return res
}
// fromSlice converts the given list of errors into a single error.
func fromSlice(errs []error) error {
// Don't pay to inspect small slices.
switch len(errs) {
case 0:
return nil
case 1:
return errs[0]
}
res := inspect(errs)
switch res.Count {
case 0:
return nil
case 1:
// only one non-nil entry
return errs[res.FirstErrorIdx]
case len(errs):
if !res.ContainsMultiError {
// Error list is flat. Make a copy of it
// Otherwise "errors" escapes to the heap
// unconditionally for all other cases.
// This lets us optimize for the "no errors" case.
out := append(([]error)(nil), errs...)
return &multiErr{errors: out, stack: callers(1)}
}
}
nonNilErrs := make([]error, 0, res.Capacity)
for _, err := range errs[res.FirstErrorIdx:] {
if err == nil {
continue
}
if nested, ok := err.(*multiErr); ok { //nolint:errorlint
nonNilErrs = append(nonNilErrs, nested.errors...)
} else {
nonNilErrs = append(nonNilErrs, err)
}
}
return &multiErr{errors: nonNilErrs, stack: callers(0)}
}
// Combine combines the passed errors into a single error.
//
// If zero arguments were passed or if all items are nil, a nil error is
// returned.
//
// Combine(nil, nil) // == nil
//
// If only a single error was passed, it is returned as-is.
//
// Combine(err) // == err
//
// Combine skips over nil arguments so this function may be used to combine
// together errors from operations that fail independently of each other.
//
// multierr.Combine(
// reader.Close(),
// writer.Close(),
// pipe.Close(),
// )
//
// If any of the passed errors is a multierr error, it will be flattened along
// with the other errors.
//
// multierr.Combine(multierr.Combine(err1, err2), err3)
// // is the same as
// multierr.Combine(err1, err2, err3)
//
// The returned error formats into a readable multi-line error message if
// formatted with %+v.
//
// fmt.Sprintf("%+v", multierr.Combine(err1, err2))
func Combine(errs ...error) error {
return fromSlice(errs)
}
// Append appends the given errors together. Either value may be nil.
//
// This function is a specialization of Combine for the common case where
// there are only two errors.
//
// err = multierr.Append(reader.Close(), writer.Close())
//
// The following pattern may also be used to record failure of deferred
// operations without losing information about the original error.
//
// func doSomething(..) (err error) {
// f := acquireResource()
// defer func() {
// err = multierr.Append(err, f.Close())
// }()
//
// Note that the variable MUST be a named return to append an error to it from
// the defer statement.
//
//nolint:errorlint
func Append(left, right error) error {
switch {
case left == nil && right == nil:
return nil
case left == nil:
switch right := right.(type) {
case *multiErr, *errz:
return right
default:
// It's not an errz, so we need to wrap it.
return &errz{stack: callers(0), error: right}
}
case right == nil:
switch left := left.(type) {
case *multiErr, *errz:
return left
default:
// It's not an errz, so we need to wrap it.
return &errz{stack: callers(0), error: left}
}
}
if _, ok := right.(*multiErr); !ok {
if l, ok := left.(*multiErr); ok && !l.copyNeeded.Swap(true) {
// Common case where the error on the left is constantly being
// appended to.
errs := append(l.errors, right) //nolint:gocritic
return &multiErr{errors: errs, stack: callers(0)}
} else if !ok {
// Both errors are single errors.
return &multiErr{errors: []error{left, right}, stack: callers(0)}
}
}
// Either right or both, left and right, are multiErrors. Rely on usual
// expensive logic.
errs := [2]error{left, right}
return fromSlice(errs[0:])
}
// Unwrap returns a list of errors wrapped by this multierr.
func (merr *multiErr) Unwrap() []error {
return merr.Errors()
}
type multipleErrorer interface {
Unwrap() []error
}
func extractErrors(err error) []error {
if err == nil {
return nil
}
// check if the given err is an unwrappable error that
// implements multipleErrorer interface.
eg, ok := err.(multipleErrorer) //nolint:errorlint
if !ok {
return []error{err}
}
return append(([]error)(nil), eg.Unwrap()...)
}
// Every compares every error in the given err against the given target error
// using [errors.Is], and returns true only if every comparison returned true.
func Every(err, target error) bool {
for _, e := range extractErrors(err) {
if !errors.Is(e, target) {
return false
}
}
return true
}