remove local bus struct (#4435)

removes the local bus from fault in favor of a single Bus that operates both within a local and global instance, and can be passed downstream independent of context.  Also includes some code separation in the fault package for readability.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-10-09 19:25:14 -06:00 committed by GitHub
parent ff6c1eaf8d
commit aa66675b83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 633 additions and 554 deletions

70
src/pkg/fault/alert.go Normal file
View File

@ -0,0 +1,70 @@
package fault
import (
"github.com/alcionai/corso/src/cli/print"
)
var _ print.Printable = &Alert{}
// Alerts are informational-only notifications. The purpose of alerts is to
// provide a means of end-user communication about important events without
// needing to generate runtime failures or recoverable errors. When generating
// an alert, no other fault feature (failure, recoverable, skip, etc) should
// be in use. IE: Errors do not also get alerts, since the error itself is a
// form of end-user communication already.
type Alert struct {
Item Item `json:"item"`
Message string `json:"message"`
}
// String complies with the stringer interface.
func (a *Alert) String() string {
msg := "<nil>"
if a != nil {
msg = a.Message
}
if len(msg) == 0 {
msg = "<missing>"
}
return "Alert: " + msg
}
func (a Alert) MinimumPrintable() any {
return a
}
// Headers returns the human-readable names of properties of a skipped Item
// for printing out to a terminal.
func (a Alert) Headers() []string {
return []string{"Action", "Message", "Container", "Name", "ID"}
}
// Values populates the printable values matching the Headers list.
func (a Alert) Values() []string {
var cn string
acn, ok := a.Item.Additional[AddtlContainerName]
if ok {
str, ok := acn.(string)
if ok {
cn = str
}
}
return []string{"Alert", a.Message, cn, a.Item.Name, a.Item.ID}
}
func NewAlert(message, namespace, itemID, name string, addtl map[string]any) *Alert {
return &Alert{
Message: message,
Item: Item{
Namespace: namespace,
ID: itemID,
Name: name,
Additional: addtl,
},
}
}

View File

@ -0,0 +1,88 @@
package fault_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/fault"
)
type AlertUnitSuite struct {
tester.Suite
}
func TestAlertUnitSuite(t *testing.T) {
suite.Run(t, &AlertUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *AlertUnitSuite) TestAlert_String() {
var (
t = suite.T()
a fault.Alert
)
assert.Contains(t, a.String(), "Alert: <missing>")
a = fault.Alert{
Item: fault.Item{},
Message: "",
}
assert.Contains(t, a.String(), "Alert: <missing>")
a = fault.Alert{
Item: fault.Item{
ID: "item_id",
},
Message: "msg",
}
assert.NotContains(t, a.String(), "item_id")
assert.Contains(t, a.String(), "Alert: msg")
}
func (suite *AlertUnitSuite) TestNewAlert() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
a := fault.NewAlert("message-to-show", "ns", "item_id", "item_name", addtl)
expect := fault.Alert{
Item: fault.Item{
Namespace: "ns",
ID: "item_id",
Name: "item_name",
Additional: addtl,
},
Message: "message-to-show",
}
assert.Equal(t, expect, *a)
}
func (suite *AlertUnitSuite) TestAlert_HeadersValues() {
addtl := map[string]any{
fault.AddtlContainerID: "cid",
fault.AddtlContainerName: "cname",
}
table := []struct {
name string
alert *fault.Alert
expect []string
}{
{
name: "new alert",
alert: fault.NewAlert("message-to-show", "ns", "id", "name", addtl),
expect: []string{"Alert", "message-to-show", "cname", "name", "id"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, []string{"Action", "Message", "Container", "Name", "ID"}, test.alert.Headers())
assert.Equal(t, test.expect, test.alert.Values())
})
}
}

View File

@ -441,3 +441,28 @@ func ExampleBus_AddSkip() {
// Output: skipped processing file: malware_detected
}
// ExampleBus_AddAlert showcases when to use AddAlert.
func ExampleBus_AddAlert() {
errs := fault.New(false)
// Some events should be communicated to the end user without recording an
// error to the operation. Logs aren't sufficient because we don't promote
// log messages to the terminal. But errors and skips are too heavy and hacky
// to use. In these cases, we can create informational Alerts.
//
// Only the message gets shown to the user. But since we're persisting this
// data along with the backup details and other fault info, we have the option
// of packing any other contextual data that we want.
errs.AddAlert(ctx, fault.NewAlert(
"something important happened!",
"deduplication-namespace",
"file-id",
"file-name",
map[string]any{"foo": "bar"}))
// later on, after processing, end users can scrutinize the alerts.
fmt.Println(errs.Alerts()[0].String())
// Alert: something important happened!
}

View File

@ -15,11 +15,24 @@ import (
"github.com/alcionai/corso/src/pkg/logger"
)
// temporary hack identifier
// see: https://github.com/alcionai/corso/pull/2510#discussion_r1113532530
// TODO: https://github.com/alcionai/corso/issues/4003
const LabelForceNoBackupCreation = "label_forces_no_backup_creations"
type Bus struct {
mu *sync.Mutex
// When creating a local bus, the parent property retains a pointer
// to the root Bus. Even in the case of multiple chained creations of
// local busses, the parent reference remains the original root bus,
// and does not create a linked list of lineage. Any errors and failures
// created by a local instance will get fielded to the parent. But only
// local errors will returned by property getter funcs.
parent *Bus
// Failure probably identifies errors that were added to the bus
// or localBus via AddRecoverable, but which were promoted
// or a local Bus via AddRecoverable, but which were promoted
// to the failure position due to failFast=true configuration.
// Alternatively, the process controller might have set failure
// by calling Fail(err).
@ -58,67 +71,61 @@ func New(failFast bool) *Bus {
}
}
// Local constructs a new bus with a local reference to handle error aggregation
// in a constrained scope. This allows the caller to review recoverable errors and
// failures within only the current codespace, as opposed to the global set of errors.
// The function that spawned the local bus should always return `bus.Failure()` to
// ensure that hard failures are propagated back upstream.
func (e *Bus) Local() *Bus {
parent := e.parent
// only use e if it is already the root instance
if parent == nil {
parent = e
}
return &Bus{
mu: &sync.Mutex{},
parent: parent,
failFast: parent.failFast,
}
}
// FailFast returns the failFast flag in the bus.
func (e *Bus) FailFast() bool {
return e.failFast
}
// Failure returns the primary error. If not nil, this
// indicates the operation exited prior to completion.
func (e *Bus) Failure() error {
return e.failure
}
// Recovered returns the slice of errors that occurred in
// recoverable points of processing. This is often during
// iteration where a single failure (ex: retrieving an item),
// doesn't require the entire process to end.
func (e *Bus) Recovered() []error {
return slices.Clone(e.recoverable)
}
// Skipped returns the slice of items that were permanently
// skipped during processing.
func (e *Bus) Skipped() []Skipped {
return slices.Clone(e.skipped)
}
// Alerts returns the slice of alerts generated during runtime.
func (e *Bus) Alerts() []Alert {
return slices.Clone(e.alerts)
}
// Fail sets the non-recoverable error (ie: bus.failure)
// in the bus. If a failure error is already present,
// the error gets added to the recoverable slice for
// purposes of tracking.
//
// TODO: Return Data, not Bus. The consumers of a failure
// should care about the state of data, not the communication
// pattern.
func (e *Bus) Fail(err error) *Bus {
if err == nil {
return e
}
e.mu.Lock()
defer e.mu.Unlock()
return e.setFailure(err)
}
// setErr handles setting bus.failure. Sync locking gets
// handled upstream of this call.
func (e *Bus) setFailure(err error) *Bus {
e.mu.Lock()
defer e.mu.Unlock()
if e.failure == nil {
e.failure = err
return e
}
} else {
// technically not a recoverable error: we're using the
// recoverable slice as an overflow container here to
// ensure everything is tracked.
e.recoverable = append(e.recoverable, err)
}
if e.parent != nil {
e.parent.setFailure(err)
}
return e
}
@ -127,17 +134,11 @@ func (e *Bus) setFailure(err error) *Bus {
// errors (ie: bus.recoverable). If failFast is true, the first
// added error will get copied to bus.failure, causing the bus
// to identify as non-recoverably failed.
//
// TODO: nil return, not Bus, since we don't want people to return
// from errors.AddRecoverable().
func (e *Bus) AddRecoverable(ctx context.Context, err error) {
if err == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.logAndAddRecoverable(ctx, err, 1)
}
@ -158,19 +159,77 @@ func (e *Bus) logAndAddRecoverable(ctx context.Context, err error, skip int) {
// gets handled upstream of this call. Returns true if the
// error is a failure, false otherwise.
func (e *Bus) addRecoverableErr(err error) bool {
e.mu.Lock()
defer e.mu.Unlock()
var isFail bool
if e.failure == nil && e.failFast {
e.setFailure(err)
if e.failure == nil {
e.failure = err
} else {
// technically not a recoverable error: we're using the
// recoverable slice as an overflow container here to
// ensure everything is tracked.
e.recoverable = append(e.recoverable, err)
}
if e.parent != nil {
e.parent.setFailure(err)
}
isFail = true
}
e.recoverable = append(e.recoverable, err)
// local bus instances must promote errors to the root bus.
if e.parent != nil {
e.parent.addRecoverableErr(err)
}
return isFail
}
// ---------------------------------------------------------------------------
// Non-error adders
// ---------------------------------------------------------------------------
// AddAlert appends a record of an Alert message to the fault bus.
// Importantly, alerts are not errors, exceptions, or skipped items.
// An alert should only be generated if no other fault functionality
// is in use, but that we still want the end user to clearly and
// plainly receive a notification about a runtime event.
func (e *Bus) AddAlert(ctx context.Context, a *Alert) {
if a == nil {
return
}
e.logAndAddAlert(ctx, a, 1)
}
// logs the error and adds an alert.
func (e *Bus) logAndAddAlert(ctx context.Context, a *Alert, trace int) {
logger.CtxStack(ctx, trace+1).
With("alert", a).
Info("alert: " + a.Message)
e.addAlert(a)
}
func (e *Bus) addAlert(a *Alert) *Bus {
e.mu.Lock()
defer e.mu.Unlock()
e.alerts = append(e.alerts, *a)
// local bus instances must promote alerts to the root bus.
if e.parent != nil {
e.parent.addAlert(a)
}
return e
}
// AddSkip appends a record of a Skipped item to the fault bus.
// Importantly, skipped items are not the same as recoverable
// errors. An item should only be skipped under the following
@ -186,9 +245,6 @@ func (e *Bus) AddSkip(ctx context.Context, s *Skipped) {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.logAndAddSkip(ctx, s, 1)
}
@ -196,44 +252,28 @@ func (e *Bus) AddSkip(ctx context.Context, s *Skipped) {
func (e *Bus) logAndAddSkip(ctx context.Context, s *Skipped, trace int) {
logger.CtxStack(ctx, trace+1).
With("skipped", s).
Info("skipped item")
Info("skipped an item")
e.addSkip(s)
}
func (e *Bus) addSkip(s *Skipped) *Bus {
e.skipped = append(e.skipped, *s)
return e
}
// AddAlert appends a record of an Alert message to the fault bus.
// Importantly, alerts are not errors, exceptions, or skipped items.
// An alert should only be generated if no other fault functionality
// is in use, but that we still want the end user to clearly and
// plainly receive a notification about a runtime event.
func (e *Bus) AddAlert(ctx context.Context, a *Alert) {
if a == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.logAndAddAlert(ctx, a, 1)
}
e.skipped = append(e.skipped, *s)
// logs the error and adds an alert.
func (e *Bus) logAndAddAlert(ctx context.Context, a *Alert, trace int) {
logger.CtxStack(ctx, trace+1).
With("alert", a).
Info("alert: " + a.Message)
e.addAlert(a)
}
// local bus instances must promote skipped items to the root bus.
if e.parent != nil {
e.parent.addSkip(s)
}
func (e *Bus) addAlert(a *Alert) *Bus {
e.alerts = append(e.alerts, *a)
return e
}
// ---------------------------------------------------------------------------
// Results
// ---------------------------------------------------------------------------
// Errors returns the plain record of errors that were aggregated
// within a fult Bus.
func (e *Bus) Errors() *Errors {
@ -249,6 +289,39 @@ func (e *Bus) Errors() *Errors {
}
}
// Failure returns the primary error. If not nil, this
// indicates the operation exited prior to completion.
// If the bus is a local instance, this only returns the
// local failure, and will not return parent data.
func (e *Bus) Failure() error {
return e.failure
}
// Recovered returns the slice of errors that occurred in
// recoverable points of processing. This is often during
// iteration where a single failure (ex: retrieving an item),
// doesn't require the entire process to end.
// If the bus is a local instance, this only returns the
// local recovered errors, and will not return parent data.
func (e *Bus) Recovered() []error {
return slices.Clone(e.recoverable)
}
// Skipped returns the slice of items that were permanently
// skipped during processing.
// If the bus is a local instance, this only returns the
// local skipped items, and will not return parent data.
func (e *Bus) Skipped() []Skipped {
return slices.Clone(e.skipped)
}
// Alerts returns the slice of alerts generated during runtime.
// If the bus is a local alerts, this only returns the
// local failure, and will not return parent data.
func (e *Bus) Alerts() []Alert {
return slices.Clone(e.alerts)
}
// ItemsAndRecovered returns the items that failed along with other
// recoverable errors
func (e *Bus) ItemsAndRecovered() ([]Item, []error) {
@ -275,10 +348,6 @@ func (e *Bus) ItemsAndRecovered() ([]Item, []error) {
return maps.Values(is), non
}
// ---------------------------------------------------------------------------
// Errors Data
// ---------------------------------------------------------------------------
// Errors provides the errors data alone, without sync controls
// or adders/setters. Expected to get called at the end of processing,
// as a way to aggregate results.
@ -360,6 +429,10 @@ func UnmarshalErrorsTo(e *Errors) func(io.ReadCloser) error {
}
}
// ---------------------------------------------------------------------------
// Print compatibility
// ---------------------------------------------------------------------------
// Print writes the DetailModel Entries to StdOut, in the format
// requested by the caller.
func (e *Errors) PrintItems(
@ -430,73 +503,3 @@ func (pec printableErrCore) Values() []string {
return []string{pec.Msg}
}
// ---------------------------------------------------------------------------
// Local aggregator
// ---------------------------------------------------------------------------
// Local constructs a new local bus to handle error aggregation in a
// constrained scope. Local busses shouldn't be passed down to other
// funcs, and the function that spawned the local bus should always
// return `local.Failure()` to ensure that hard failures are propagated
// back upstream.
func (e *Bus) Local() *LocalBus {
return &LocalBus{
mu: &sync.Mutex{},
bus: e,
}
}
type LocalBus struct {
mu *sync.Mutex
bus *Bus
current error
}
func (e *LocalBus) AddRecoverable(ctx context.Context, err error) {
if err == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
if e.current == nil && e.bus.failFast {
e.current = err
}
e.bus.logAndAddRecoverable(ctx, err, 1)
}
// AddSkip appends a record of a Skipped item to the local bus.
// Importantly, skipped items are not the same as recoverable
// errors. An item should only be skipped under the following
// conditions. All other cases should be handled as errors.
// 1. The conditions for skipping the item are well-known and
// well-documented. End users need to be able to understand
// both the conditions and identifications of skips.
// 2. Skipping avoids a permanent and consistent failure. If
// the underlying reason is transient or otherwise recoverable,
// the item should not be skipped.
func (e *LocalBus) AddSkip(ctx context.Context, s *Skipped) {
if s == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.bus.logAndAddSkip(ctx, s, 1)
}
// Failure returns the failure that happened within the local bus.
// It does not return the underlying bus.Failure(), only the failure
// that was recorded within the local bus instance. This error should
// get returned by any func which created a local bus.
func (e *LocalBus) Failure() error {
return e.current
}
// temporary hack identifier
// see: https://github.com/alcionai/corso/pull/2510#discussion_r1113532530
const LabelForceNoBackupCreation = "label_forces_no_backup_creations"

View File

@ -189,25 +189,6 @@ func (suite *FaultErrorsUnitSuite) TestAdd() {
assert.Len(t, n.Recovered(), 2)
}
func (suite *FaultErrorsUnitSuite) TestAddSkip() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
n := fault.New(true)
require.NotNil(t, n)
n.Fail(assert.AnError)
assert.Len(t, n.Skipped(), 0)
n.AddRecoverable(ctx, assert.AnError)
assert.Len(t, n.Skipped(), 0)
n.AddSkip(ctx, fault.OwnerSkip(fault.SkipMalware, "ns", "id", "name", nil))
assert.Len(t, n.Skipped(), 1)
}
func (suite *FaultErrorsUnitSuite) TestErrors() {
t := suite.T()

View File

@ -11,15 +11,15 @@ const (
AddtlMalwareDesc = "malware_description"
)
type itemType string
type ItemType string
const (
FileType itemType = "file"
ContainerType itemType = "container"
ResourceOwnerType itemType = "resource_owner"
FileType ItemType = "file"
ContainerType ItemType = "container"
ResourceOwnerType ItemType = "resource_owner"
)
func (it itemType) Printable() string {
func (it ItemType) Printable() string {
switch it {
case FileType:
return "File"
@ -62,7 +62,7 @@ type Item struct {
Name string `json:"name"`
// tracks the type of item represented by this entry.
Type itemType `json:"type"`
Type ItemType `json:"type"`
// Error() of the causal error, or a sentinel if this is the
// source of the error. In case of ID collisions, the first
@ -138,7 +138,7 @@ func OwnerErr(cause error, namespace, id, name string, addtl map[string]any) *It
}
// itemErr produces a Item of the provided type for tracking erroneous items.
func itemErr(t itemType, cause error, namespace, id, name string, addtl map[string]any) *Item {
func itemErr(t ItemType, cause error, namespace, id, name string, addtl map[string]any) *Item {
return &Item{
Namespace: namespace,
ID: id,
@ -148,183 +148,3 @@ func itemErr(t itemType, cause error, namespace, id, name string, addtl map[stri
Additional: addtl,
}
}
// ---------------------------------------------------------------------------
// Skipped Items
// ---------------------------------------------------------------------------
// skipCause identifies the well-known conditions to Skip an item. It is
// important that skip cause enumerations do not overlap with general error
// handling. Skips must be well known, well documented, and consistent.
// Transient failures, undocumented or unknown conditions, and arbitrary
// handling should never produce a skipped item. Those cases should get
// handled as normal errors.
type skipCause string
const (
// SkipMalware identifies a malware detection case. Files that graph
// api identifies as malware cannot be downloaded or uploaded, and will
// permanently fail any attempts to backup or restore.
SkipMalware skipCause = "malware_detected"
// SkipBigOneNote identifies that a file was skipped because it
// was big OneNote file and we can only download OneNote files which
// are less that 2GB in size.
//nolint:lll
// https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#onenotenotebooks
SkipBigOneNote skipCause = "big_one_note_file"
)
var _ print.Printable = &Skipped{}
// Skipped items are permanently unprocessable due to well-known conditions.
// In order to skip an item, the following conditions should be met:
// 1. The conditions for skipping the item are well-known and
// well-documented. End users need to be able to understand
// both the conditions and identifications of skips.
// 2. Skipping avoids a permanent and consistent failure. If
// the underlying reason is transient or otherwise recoverable,
// the item should not be skipped.
//
// Skipped wraps Item primarily to minimize confusion when sharing the
// fault interface. Skipped items are not errors, and Item{} errors are
// not the basis for a Skip.
type Skipped struct {
Item Item `json:"item"`
}
// String complies with the stringer interface.
func (s *Skipped) String() string {
if s == nil {
return "<nil>"
}
return "skipped " + s.Item.Error() + ": " + s.Item.Cause
}
// HasCause compares the underlying cause against the parameter.
func (s *Skipped) HasCause(c skipCause) bool {
if s == nil {
return false
}
return s.Item.Cause == string(c)
}
func (s Skipped) MinimumPrintable() any {
return s
}
// Headers returns the human-readable names of properties of a skipped Item
// for printing out to a terminal.
func (s Skipped) Headers() []string {
return []string{"Action", "Type", "Name", "Container", "Cause"}
}
// Values populates the printable values matching the Headers list.
func (s Skipped) Values() []string {
var cn string
acn, ok := s.Item.Additional[AddtlContainerName]
if ok {
str, ok := acn.(string)
if ok {
cn = str
}
}
return []string{"Skip", s.Item.Type.Printable(), s.Item.Name, cn, s.Item.Cause}
}
// ContainerSkip produces a Container-kind Item for tracking skipped items.
func ContainerSkip(cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return itemSkip(ContainerType, cause, namespace, id, name, addtl)
}
// FileSkip produces a File-kind Item for tracking skipped items.
func FileSkip(cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return itemSkip(FileType, cause, namespace, id, name, addtl)
}
// OnwerSkip produces a ResourceOwner-kind Item for tracking skipped items.
func OwnerSkip(cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return itemSkip(ResourceOwnerType, cause, namespace, id, name, addtl)
}
// itemSkip produces a Item of the provided type for tracking skipped items.
func itemSkip(t itemType, cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return &Skipped{
Item: Item{
Namespace: namespace,
ID: id,
Name: name,
Type: t,
Cause: string(cause),
Additional: addtl,
},
}
}
// ---------------------------------------------------------------------------
// Alerts
// ---------------------------------------------------------------------------
var _ print.Printable = &Alert{}
// Alerts are informational-only notifications. The purpose of alerts is to
// provide a means of end-user communication about important events without
// needing to generate runtime failures or recoverable errors. When generating
// an alert, no other fault feature (failure, recoverable, skip, etc) should
// be in use. IE: Errors do not also get alerts, since the error itself is a
// form of end-user communication already.
type Alert struct {
Item Item `json:"item"`
Message string `json:"message"`
}
// String complies with the stringer interface.
func (a Alert) String() string {
msg := a.Message
if len(msg) == 0 {
msg = "<nil>"
}
return "Alert: " + msg
}
func (a Alert) MinimumPrintable() any {
return a
}
// Headers returns the human-readable names of properties of a skipped Item
// for printing out to a terminal.
func (a Alert) Headers() []string {
return []string{"Action", "Message", "Container", "Name", "ID"}
}
// Values populates the printable values matching the Headers list.
func (a Alert) Values() []string {
var cn string
acn, ok := a.Item.Additional[AddtlContainerName]
if ok {
str, ok := acn.(string)
if ok {
cn = str
}
}
return []string{"Alert", a.Message, cn, a.Item.Name, a.Item.ID}
}
func NewAlert(message, namespace, itemID, name string, addtl map[string]any) *Alert {
return &Alert{
Message: message,
Item: Item{
Namespace: namespace,
ID: itemID,
Name: name,
Additional: addtl,
},
}
}

View File

@ -1,4 +1,4 @@
package fault
package fault_test
import (
"testing"
@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/fault"
)
type ItemUnitSuite struct {
@ -21,28 +22,28 @@ func TestItemUnitSuite(t *testing.T) {
func (suite *ItemUnitSuite) TestItem_Error() {
var (
t = suite.T()
i *Item
i *fault.Item
)
assert.Contains(t, i.Error(), "nil")
i = &Item{}
i = &fault.Item{}
assert.Contains(t, i.Error(), "unknown type")
i = &Item{Type: FileType}
assert.Contains(t, i.Error(), FileType)
i = &fault.Item{Type: fault.FileType}
assert.Contains(t, i.Error(), fault.FileType)
}
func (suite *ItemUnitSuite) TestContainerErr() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := ContainerErr(clues.New("foo"), "ns", "id", "name", addtl)
i := fault.ContainerErr(clues.New("foo"), "ns", "id", "name", addtl)
expect := Item{
expect := fault.Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: ContainerType,
Type: fault.ContainerType,
Cause: "foo",
Additional: addtl,
}
@ -53,13 +54,13 @@ func (suite *ItemUnitSuite) TestContainerErr() {
func (suite *ItemUnitSuite) TestFileErr() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := FileErr(clues.New("foo"), "ns", "id", "name", addtl)
i := fault.FileErr(clues.New("foo"), "ns", "id", "name", addtl)
expect := Item{
expect := fault.Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: FileType,
Type: fault.FileType,
Cause: "foo",
Additional: addtl,
}
@ -70,13 +71,13 @@ func (suite *ItemUnitSuite) TestFileErr() {
func (suite *ItemUnitSuite) TestOwnerErr() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := OwnerErr(clues.New("foo"), "ns", "id", "name", addtl)
i := fault.OwnerErr(clues.New("foo"), "ns", "id", "name", addtl)
expect := Item{
expect := fault.Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: ResourceOwnerType,
Type: fault.ResourceOwnerType,
Cause: "foo",
Additional: addtl,
}
@ -86,23 +87,23 @@ func (suite *ItemUnitSuite) TestOwnerErr() {
func (suite *ItemUnitSuite) TestItemType_Printable() {
table := []struct {
t itemType
t fault.ItemType
expect string
}{
{
t: FileType,
t: fault.FileType,
expect: "File",
},
{
t: ContainerType,
t: fault.ContainerType,
expect: "Container",
},
{
t: ResourceOwnerType,
t: fault.ResourceOwnerType,
expect: "Resource Owner",
},
{
t: itemType("foo"),
t: fault.ItemType("foo"),
expect: "Unknown",
},
}
@ -118,30 +119,30 @@ func (suite *ItemUnitSuite) TestItem_HeadersValues() {
err = assert.AnError
cause = err.Error()
addtl = map[string]any{
AddtlContainerID: "cid",
AddtlContainerName: "cname",
fault.AddtlContainerID: "cid",
fault.AddtlContainerName: "cname",
}
)
table := []struct {
name string
item *Item
item *fault.Item
expect []string
}{
{
name: "file",
item: FileErr(assert.AnError, "ns", "id", "name", addtl),
expect: []string{"Error", FileType.Printable(), "name", "cname", cause},
item: fault.FileErr(assert.AnError, "ns", "id", "name", addtl),
expect: []string{"Error", fault.FileType.Printable(), "name", "cname", cause},
},
{
name: "container",
item: ContainerErr(assert.AnError, "ns", "id", "name", addtl),
expect: []string{"Error", ContainerType.Printable(), "name", "cname", cause},
item: fault.ContainerErr(assert.AnError, "ns", "id", "name", addtl),
expect: []string{"Error", fault.ContainerType.Printable(), "name", "cname", cause},
},
{
name: "owner",
item: OwnerErr(assert.AnError, "ns", "id", "name", nil),
expect: []string{"Error", ResourceOwnerType.Printable(), "name", "", cause},
item: fault.OwnerErr(assert.AnError, "ns", "id", "name", nil),
expect: []string{"Error", fault.ResourceOwnerType.Printable(), "name", "", cause},
},
}
for _, test := range table {
@ -153,175 +154,3 @@ func (suite *ItemUnitSuite) TestItem_HeadersValues() {
})
}
}
func (suite *ItemUnitSuite) TestSkipped_String() {
var (
t = suite.T()
i *Skipped
)
assert.Contains(t, i.String(), "nil")
i = &Skipped{Item{}}
assert.Contains(t, i.String(), "unknown type")
i = &Skipped{Item{Type: FileType}}
assert.Contains(t, i.Item.Error(), FileType)
}
func (suite *ItemUnitSuite) TestContainerSkip() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := ContainerSkip(SkipMalware, "ns", "id", "name", addtl)
expect := Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: ContainerType,
Cause: string(SkipMalware),
Additional: addtl,
}
assert.Equal(t, Skipped{expect}, *i)
}
func (suite *ItemUnitSuite) TestFileSkip() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := FileSkip(SkipMalware, "ns", "id", "name", addtl)
expect := Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: FileType,
Cause: string(SkipMalware),
Additional: addtl,
}
assert.Equal(t, Skipped{expect}, *i)
}
func (suite *ItemUnitSuite) TestOwnerSkip() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := OwnerSkip(SkipMalware, "ns", "id", "name", addtl)
expect := Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: ResourceOwnerType,
Cause: string(SkipMalware),
Additional: addtl,
}
assert.Equal(t, Skipped{expect}, *i)
}
func (suite *ItemUnitSuite) TestSkipped_HeadersValues() {
addtl := map[string]any{
AddtlContainerID: "cid",
AddtlContainerName: "cname",
}
table := []struct {
name string
skip *Skipped
expect []string
}{
{
name: "file",
skip: FileSkip(SkipMalware, "ns", "id", "name", addtl),
expect: []string{"Skip", FileType.Printable(), "name", "cname", string(SkipMalware)},
},
{
name: "container",
skip: ContainerSkip(SkipMalware, "ns", "id", "name", addtl),
expect: []string{"Skip", ContainerType.Printable(), "name", "cname", string(SkipMalware)},
},
{
name: "owner",
skip: OwnerSkip(SkipMalware, "ns", "id", "name", nil),
expect: []string{"Skip", ResourceOwnerType.Printable(), "name", "", string(SkipMalware)},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.skip.Headers())
assert.Equal(t, test.expect, test.skip.Values())
})
}
}
func (suite *ItemUnitSuite) TestAlert_String() {
var (
t = suite.T()
a Alert
)
assert.Contains(t, a.String(), "Alert: <nil>")
a = Alert{
Item: Item{},
Message: "",
}
assert.Contains(t, a.String(), "Alert: <nil>")
a = Alert{
Item: Item{
ID: "item_id",
},
Message: "msg",
}
assert.NotContains(t, a.String(), "item_id")
assert.Contains(t, a.String(), "Alert: msg")
}
func (suite *ItemUnitSuite) TestNewAlert() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
a := NewAlert("message-to-show", "ns", "item_id", "item_name", addtl)
expect := Alert{
Item: Item{
Namespace: "ns",
ID: "item_id",
Name: "item_name",
Additional: addtl,
},
Message: "message-to-show",
}
assert.Equal(t, expect, *a)
}
func (suite *ItemUnitSuite) TestAlert_HeadersValues() {
addtl := map[string]any{
AddtlContainerID: "cid",
AddtlContainerName: "cname",
}
table := []struct {
name string
alert *Alert
expect []string
}{
{
name: "new alert",
alert: NewAlert("message-to-show", "ns", "id", "name", addtl),
expect: []string{"Alert", "message-to-show", "cname", "name", "id"},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, []string{"Action", "Message", "Container", "Name", "ID"}, test.alert.Headers())
assert.Equal(t, test.expect, test.alert.Values())
})
}
}

117
src/pkg/fault/skipped.go Normal file
View File

@ -0,0 +1,117 @@
package fault
import (
"github.com/alcionai/corso/src/cli/print"
)
// skipCause identifies the well-known conditions to Skip an item. It is
// important that skip cause enumerations do not overlap with general error
// handling. Skips must be well known, well documented, and consistent.
// Transient failures, undocumented or unknown conditions, and arbitrary
// handling should never produce a skipped item. Those cases should get
// handled as normal errors.
type skipCause string
const (
// SkipMalware identifies a malware detection case. Files that graph
// api identifies as malware cannot be downloaded or uploaded, and will
// permanently fail any attempts to backup or restore.
SkipMalware skipCause = "malware_detected"
// SkipBigOneNote identifies that a file was skipped because it
// was big OneNote file and we can only download OneNote files which
// are less that 2GB in size.
//nolint:lll
// https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#onenotenotebooks
SkipBigOneNote skipCause = "big_one_note_file"
)
var _ print.Printable = &Skipped{}
// Skipped items are permanently unprocessable due to well-known conditions.
// In order to skip an item, the following conditions should be met:
// 1. The conditions for skipping the item are well-known and
// well-documented. End users need to be able to understand
// both the conditions and identifications of skips.
// 2. Skipping avoids a permanent and consistent failure. If
// the underlying reason is transient or otherwise recoverable,
// the item should not be skipped.
//
// Skipped wraps Item primarily to minimize confusion when sharing the
// fault interface. Skipped items are not errors, and Item{} errors are
// not the basis for a Skip.
type Skipped struct {
Item Item `json:"item"`
}
// String complies with the stringer interface.
func (s *Skipped) String() string {
if s == nil {
return "<nil>"
}
return "skipped " + s.Item.Error() + ": " + s.Item.Cause
}
// HasCause compares the underlying cause against the parameter.
func (s *Skipped) HasCause(c skipCause) bool {
if s == nil {
return false
}
return s.Item.Cause == string(c)
}
func (s Skipped) MinimumPrintable() any {
return s
}
// Headers returns the human-readable names of properties of a skipped Item
// for printing out to a terminal.
func (s Skipped) Headers() []string {
return []string{"Action", "Type", "Name", "Container", "Cause"}
}
// Values populates the printable values matching the Headers list.
func (s Skipped) Values() []string {
var cn string
acn, ok := s.Item.Additional[AddtlContainerName]
if ok {
str, ok := acn.(string)
if ok {
cn = str
}
}
return []string{"Skip", s.Item.Type.Printable(), s.Item.Name, cn, s.Item.Cause}
}
// ContainerSkip produces a Container-kind Item for tracking skipped items.
func ContainerSkip(cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return itemSkip(ContainerType, cause, namespace, id, name, addtl)
}
// FileSkip produces a File-kind Item for tracking skipped items.
func FileSkip(cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return itemSkip(FileType, cause, namespace, id, name, addtl)
}
// OnwerSkip produces a ResourceOwner-kind Item for tracking skipped items.
func OwnerSkip(cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return itemSkip(ResourceOwnerType, cause, namespace, id, name, addtl)
}
// itemSkip produces a Item of the provided type for tracking skipped items.
func itemSkip(t ItemType, cause skipCause, namespace, id, name string, addtl map[string]any) *Skipped {
return &Skipped{
Item: Item{
Namespace: namespace,
ID: id,
Name: name,
Type: t,
Cause: string(cause),
Additional: addtl,
},
}
}

View File

@ -0,0 +1,146 @@
package fault_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/fault"
)
type SkippedUnitSuite struct {
tester.Suite
}
func TestSkippedUnitSuite(t *testing.T) {
suite.Run(t, &SkippedUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *SkippedUnitSuite) TestSkipped_String() {
var (
t = suite.T()
i *fault.Skipped
)
assert.Contains(t, i.String(), "nil")
i = &fault.Skipped{fault.Item{}}
assert.Contains(t, i.String(), "unknown type")
i = &fault.Skipped{
fault.Item{
Type: fault.FileType,
},
}
assert.Contains(t, i.Item.Error(), fault.FileType)
}
func (suite *SkippedUnitSuite) TestContainerSkip() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := fault.ContainerSkip(fault.SkipMalware, "ns", "id", "name", addtl)
expect := fault.Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: fault.ContainerType,
Cause: string(fault.SkipMalware),
Additional: addtl,
}
assert.Equal(t, fault.Skipped{expect}, *i)
}
func (suite *SkippedUnitSuite) TestFileSkip() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := fault.FileSkip(fault.SkipMalware, "ns", "id", "name", addtl)
expect := fault.Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: fault.FileType,
Cause: string(fault.SkipMalware),
Additional: addtl,
}
assert.Equal(t, fault.Skipped{expect}, *i)
}
func (suite *SkippedUnitSuite) TestOwnerSkip() {
t := suite.T()
addtl := map[string]any{"foo": "bar"}
i := fault.OwnerSkip(fault.SkipMalware, "ns", "id", "name", addtl)
expect := fault.Item{
Namespace: "ns",
ID: "id",
Name: "name",
Type: fault.ResourceOwnerType,
Cause: string(fault.SkipMalware),
Additional: addtl,
}
assert.Equal(t, fault.Skipped{expect}, *i)
}
func (suite *SkippedUnitSuite) TestSkipped_HeadersValues() {
addtl := map[string]any{
fault.AddtlContainerID: "cid",
fault.AddtlContainerName: "cname",
}
table := []struct {
name string
skip *fault.Skipped
expect []string
}{
{
name: "file",
skip: fault.FileSkip(fault.SkipMalware, "ns", "id", "name", addtl),
expect: []string{"Skip", fault.FileType.Printable(), "name", "cname", string(fault.SkipMalware)},
},
{
name: "container",
skip: fault.ContainerSkip(fault.SkipMalware, "ns", "id", "name", addtl),
expect: []string{"Skip", fault.ContainerType.Printable(), "name", "cname", string(fault.SkipMalware)},
},
{
name: "owner",
skip: fault.OwnerSkip(fault.SkipMalware, "ns", "id", "name", nil),
expect: []string{"Skip", fault.ResourceOwnerType.Printable(), "name", "", string(fault.SkipMalware)},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, []string{"Action", "Type", "Name", "Container", "Cause"}, test.skip.Headers())
assert.Equal(t, test.expect, test.skip.Values())
})
}
}
func (suite *SkippedUnitSuite) TestBus_AddSkip() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
n := fault.New(true)
require.NotNil(t, n)
n.Fail(assert.AnError)
assert.Len(t, n.Skipped(), 0)
n.AddRecoverable(ctx, assert.AnError)
assert.Len(t, n.Skipped(), 0)
n.AddSkip(ctx, fault.OwnerSkip(fault.SkipMalware, "ns", "id", "name", nil))
assert.Len(t, n.Skipped(), 1)
}