## Does this PR need a docs update or release note? - [x] ⛔ No ## Type of change - [x] 🧹 Tech Debt/Cleanup ## Issue(s) * #1970 ## Test Plan - [x] ⚡ Unit test
292 lines
8.2 KiB
Go
292 lines
8.2 KiB
Go
package fault_test
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/alcionai/corso/src/pkg/fault"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// mock helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
var (
|
|
ctrl any
|
|
items = []string{}
|
|
)
|
|
|
|
type mockController struct {
|
|
errors any
|
|
}
|
|
|
|
func connectClient() error { return nil }
|
|
func dependencyCall() error { return nil }
|
|
func getIthItem(i string) error { return nil }
|
|
func getData() ([]string, error) { return nil, nil }
|
|
func storeData([]string, *fault.Errors) {}
|
|
|
|
type mockOper struct {
|
|
Errors *fault.Errors
|
|
}
|
|
|
|
func newOperation() mockOper { return mockOper{fault.New(true)} }
|
|
func (m mockOper) Run() *fault.Errors { return m.Errors }
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// examples
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// ExampleNewErrors highlights assumptions and best practices
|
|
// for generating Errors structs.
|
|
func Example_new() {
|
|
// Errors should only be generated during the construction of
|
|
// another controller, such as a new Backup or Restore Operations.
|
|
// Configurations like failFast are set during construction.
|
|
//
|
|
// Generating new fault.Errors structs outside of an operation
|
|
// controller is a smell, and should be avoided. If you need
|
|
// to aggregate errors, you should accept an interface and pass
|
|
// an Errors instance into it.
|
|
ctrl = mockController{
|
|
errors: fault.New(false),
|
|
}
|
|
}
|
|
|
|
// ExampleErrorsFail describes the assumptions and best practices
|
|
// for setting the Failure error.
|
|
func Example_errors_Fail() {
|
|
errs := fault.New(false)
|
|
|
|
// Fail() should be used to record any error that highlights a
|
|
// non-recoverable failure in a process.
|
|
//
|
|
// Fail() should only get called in the last step before returning
|
|
// a fault.Errors from a controller. In all other cases, you
|
|
// should simply return an error and expect the upstream controller
|
|
// to call Fail() for you.
|
|
if err := connectClient(); err != nil {
|
|
// normally, you'd want to
|
|
// return errs.Fail(err)
|
|
errs.Fail(err)
|
|
}
|
|
|
|
// Only the topmost handler of the error should set the Fail() err.
|
|
// This will normally be the operation controller itself.
|
|
// IE: Fail() is not Wrap(). In lower levels, errors should get
|
|
// wrapped and returned like normal, and only handled by errors
|
|
// at the end.
|
|
lowLevelCall := func() error {
|
|
if err := dependencyCall(); err != nil {
|
|
// wrap here, deeper into the stack
|
|
return errors.Wrap(err, "dependency")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := lowLevelCall(); err != nil {
|
|
// fail here, at the top of the stack
|
|
errs.Fail(err)
|
|
}
|
|
}
|
|
|
|
// ExampleErrorsAdd describes the assumptions and best practices
|
|
// for aggregating iterable or recoverable errors.
|
|
func Example_errors_Add() {
|
|
errs := fault.New(false)
|
|
|
|
// Add() should be used to record any error in a recoverable
|
|
// part of processing.
|
|
//
|
|
// Add() should only get called in the last step in handling an
|
|
// error within a loop or stream that does not otherwise return
|
|
// an error. In all other cases, you should simply return an error
|
|
// and expect the upstream point of iteration to call Add() for you.
|
|
for _, i := range items {
|
|
if err := getIthItem(i); err != nil {
|
|
errs.Add(err)
|
|
}
|
|
}
|
|
|
|
// In case of failFast behavior, iteration should exit as soon
|
|
// as an error occurs. Errors does not expose the failFast flag
|
|
// directly. Instead, iterators should check the value of Err().
|
|
// If it is non-nil, then the loop shold break.
|
|
for _, i := range items {
|
|
if errs.Err() != nil {
|
|
break
|
|
}
|
|
|
|
errs.Add(getIthItem(i))
|
|
}
|
|
|
|
// Only the topmost handler of the error should Add() the err.
|
|
// This will normally be the iteration loop itself.
|
|
// IE: Add() is not Wrap(). In lower levels, errors should get
|
|
// wrapped and returned like normally, and only added to the
|
|
// errors at the end.
|
|
clientBasedGetter := func(s string) error {
|
|
if err := dependencyCall(); err != nil {
|
|
// wrap here, deeper into the stack
|
|
return errors.Wrap(err, "dependency")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, i := range items {
|
|
if err := clientBasedGetter(i); err != nil {
|
|
// add here, within the iteraton loop
|
|
errs.Add(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExampleErrorsErr describes retrieving the non-recoverable error.
|
|
func Example_errors_Err() {
|
|
errs := fault.New(false)
|
|
errs.Fail(errors.New("catastrophe"))
|
|
|
|
// Err() gets the primary failure error.
|
|
err := errs.Err()
|
|
fmt.Println(err)
|
|
|
|
// if multiple Failures occur, each one after the first gets
|
|
// added to the Errs slice.
|
|
errs.Fail(errors.New("another catastrophe"))
|
|
errSl := errs.Errs()
|
|
|
|
for _, e := range errSl {
|
|
fmt.Println(e)
|
|
}
|
|
|
|
// If Err() is nil, then you can assume the operation completed.
|
|
// A complete operation is not necessarily an error-free operation.
|
|
//
|
|
// Even if Err() is nil, Errs() can be non-empty.
|
|
// Make sure you check both.
|
|
|
|
errs = fault.New(true)
|
|
|
|
// If failFast is set to true, then the first error Add()ed gets
|
|
// promoted to the Err() position.
|
|
|
|
errs.Add(errors.New("not catastrophic, but still becomes the Err()"))
|
|
err = errs.Err()
|
|
fmt.Println(err)
|
|
|
|
// Output: catastrophe
|
|
// another catastrophe
|
|
// not catastrophic, but still becomes the Err()
|
|
}
|
|
|
|
// ExampleErrorsErrs describes retrieving individual errors.
|
|
func Example_errors_Errs() {
|
|
errs := fault.New(false)
|
|
errs.Add(errors.New("not catastrophic"))
|
|
errs.Add(errors.New("something unwanted"))
|
|
|
|
// Errs() gets the slice errors that were recorded, but were
|
|
// considered recoverable.
|
|
errSl := errs.Errs()
|
|
for _, err := range errSl {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
// Errs() only needs to be investigated by the end user at the
|
|
// conclusion of an operation. Checking Errs() within lower-
|
|
// layer code is a smell. Funcs should return an error if they
|
|
// need upstream handlers to recognize failure states.
|
|
//
|
|
// If Errs() is nil, then you can assume that no recoverable or
|
|
// iteration-based errors occurred. But that does not necessarily
|
|
// mean the operation was able to complete.
|
|
//
|
|
// Even if Errs() contains zero items, Err() can be non-nil.
|
|
// Make sure you check both.
|
|
|
|
// Output: not catastrophic
|
|
// something unwanted
|
|
}
|
|
|
|
// ExampleErrorsE2e showcases a more complex integration.
|
|
func Example_errors_e2e() {
|
|
oper := newOperation()
|
|
|
|
// imagine that we're a user, calling into corso SDK.
|
|
// (fake funcs used here to minimize example bloat)
|
|
//
|
|
// The operation is our controller, we expect it to
|
|
// generate a new fault.Errors when constructed, and
|
|
// to return that struct when we call Run()
|
|
errs := oper.Run()
|
|
|
|
// Let's investigate what went on inside. Since we're at
|
|
// the top of our controller, and returning a fault.Errors,
|
|
// all the error handlers set the Fail() case.
|
|
/* Run() */
|
|
func() *fault.Errors {
|
|
if err := connectClient(); err != nil {
|
|
// Fail() here; we're top level in the controller
|
|
// and this is a non-recoverable issue
|
|
return oper.Errors.Fail(err)
|
|
}
|
|
|
|
data, err := getData()
|
|
if err != nil {
|
|
return oper.Errors.Fail(err)
|
|
}
|
|
|
|
// storeData will aggregate iterated errors into
|
|
// oper.Errors.
|
|
storeData(data, oper.Errors)
|
|
|
|
// return oper.Errors here, in part to ensure it's
|
|
// non-nil, and because we don't know if we've
|
|
// aggregated any iterated errors yet.
|
|
return oper.Errors
|
|
}()
|
|
|
|
// What about the lower level handling? storeData didn't
|
|
// return an error, so what's happening there?
|
|
/* storeData */
|
|
func(data []any, errs *fault.Errors) {
|
|
// this is downstream in our code somewhere
|
|
storer := func(a any) error {
|
|
if err := dependencyCall(); err != nil {
|
|
// we're not passing in or calling fault.Errors here,
|
|
// because this isn't the iteration handler, it's just
|
|
// a regular error.
|
|
return errors.Wrap(err, "dependency")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, d := range data {
|
|
if errs.Err() != nil {
|
|
break
|
|
}
|
|
|
|
if err := storer(d); err != nil {
|
|
// Since we're at the top of the iteration, we need
|
|
// to add each error to the fault.Errors struct.
|
|
errs.Add(err)
|
|
}
|
|
}
|
|
}(nil, nil)
|
|
|
|
// then at the end of the oper.Run, we investigate the results.
|
|
if errs.Err() != nil {
|
|
// handle the primary error
|
|
fmt.Println("err occurred", errs.Err())
|
|
}
|
|
|
|
for _, err := range errs.Errs() {
|
|
// handle each recoverable error
|
|
fmt.Println("recoverable err occurred", err)
|
|
}
|
|
}
|