diff --git a/secondary/secondary.go b/secondary/secondary.go index a680202..47d81f3 100644 --- a/secondary/secondary.go +++ b/secondary/secondary.go @@ -14,6 +14,8 @@ package secondary +import "github.com/cockroachdb/errors/errbase" + // WithSecondaryError enhances the error given as first argument with // an annotation that carries the error given as second argument. The // second error does not participate in cause analysis (Is, etc) and @@ -44,3 +46,40 @@ func CombineErrors(err error, otherErr error) error { } return WithSecondaryError(err, otherErr) } + +// SummarizeErrors reduces a collection of errors to a single +// error with the rest as secondary errors, making an effort +// at deduplication. Use when it's not clear, or not deterministic, +// which of many errors will be the root cause. +func SummarizeErrors(errs ...error) error { + if len(errs) == 0 { + return nil + } + uniqArgsInOrder := make([]error, 0, len(errs)) + uniqArgsMap := make(map[error]struct{}, len(errs)) + refCount := make(map[error]int) + for _, e := range errs { + if _, dup := uniqArgsMap[e]; !dup { + uniqArgsMap[e] = struct{}{} + uniqArgsInOrder = append(uniqArgsInOrder, e) + walk(e, func(w error) { refCount[w] = refCount[w] + 1 }) + } + } + var retVal error + for _, e := range uniqArgsInOrder { + if refCount[e] == 1 { + retVal = CombineErrors(retVal, e) + } + } + return retVal +} + +func walk(err error, fn func(error)) { + if err != nil { + fn(err) + walk(errbase.UnwrapOnce(err), fn) + if se, ok := err.(*withSecondaryError); ok { + walk(se.secondaryError, fn) + } + } +} diff --git a/secondary/secondary_test.go b/secondary/secondary_test.go index e65b8f0..988060e 100644 --- a/secondary/secondary_test.go +++ b/secondary/secondary_test.go @@ -102,6 +102,33 @@ func TestCombineErrors(t *testing.T) { } } +// This test asserts SummarizeErrors output in terms of CombineErrors output. +func TestSummarizeErrors(t *testing.T) { + tt := testutils.T{T: t} + err1 := errors.New("err1") + err2 := errors.New("err2") + chainWith2 := &werrFmt{err2, "chainWith2"} + chainWith1And2 := secondary.CombineErrors(chainWith2, err1) + + testData := []struct { + args []error + summary error + }{ + {[]error{err1, err2}, secondary.CombineErrors(err1, err2)}, + {[]error{err2, err1}, secondary.CombineErrors(err2, err1)}, + {[]error{err1, err2, err1}, secondary.CombineErrors(err1, err2)}, + {[]error{chainWith2, err2}, chainWith2}, + {[]error{err2, chainWith2}, chainWith2}, + {[]error{chainWith2, err1, err2}, secondary.CombineErrors(chainWith2, err1)}, + {[]error{chainWith2, chainWith1And2, err1, err2}, chainWith1And2}, + } + + for _, test := range testData { + err := secondary.SummarizeErrors(test.args...) + tt.CheckDeepEqual(err, test.summary) + } +} + func TestFormat(t *testing.T) { tt := testutils.T{t} diff --git a/secondary_api.go b/secondary_api.go index 2f5e34f..03546cd 100644 --- a/secondary_api.go +++ b/secondary_api.go @@ -40,3 +40,11 @@ func WithSecondaryError(err error, additionalErr error) error { func CombineErrors(err, otherErr error) error { return secondary.CombineErrors(err, otherErr) } + +// SummarizeErrors reduces a collection of errors to a single +// error with the rest as secondary errors, making an effort +// at deduplication. Use when it's not clear, or not deterministic, +// which of many errors will be the root cause. +func SummarizeErrors(errs ...error) error { + return secondary.SummarizeErrors(errs...) +}