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:
parent
ff6c1eaf8d
commit
aa66675b83
70
src/pkg/fault/alert.go
Normal file
70
src/pkg/fault/alert.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/pkg/fault/alert_test.go
Normal file
88
src/pkg/fault/alert_test.go
Normal 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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -441,3 +441,28 @@ func ExampleBus_AddSkip() {
|
|||||||
|
|
||||||
// Output: skipped processing file: malware_detected
|
// 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!
|
||||||
|
}
|
||||||
|
|||||||
@ -15,11 +15,24 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"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 {
|
type Bus struct {
|
||||||
mu *sync.Mutex
|
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
|
// 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.
|
// to the failure position due to failFast=true configuration.
|
||||||
// Alternatively, the process controller might have set failure
|
// Alternatively, the process controller might have set failure
|
||||||
// by calling Fail(err).
|
// 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.
|
// FailFast returns the failFast flag in the bus.
|
||||||
func (e *Bus) FailFast() bool {
|
func (e *Bus) FailFast() bool {
|
||||||
return e.failFast
|
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)
|
// Fail sets the non-recoverable error (ie: bus.failure)
|
||||||
// in the bus. If a failure error is already present,
|
// in the bus. If a failure error is already present,
|
||||||
// the error gets added to the recoverable slice for
|
// the error gets added to the recoverable slice for
|
||||||
// purposes of tracking.
|
// 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 {
|
func (e *Bus) Fail(err error) *Bus {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
return e.setFailure(err)
|
return e.setFailure(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setErr handles setting bus.failure. Sync locking gets
|
// setErr handles setting bus.failure. Sync locking gets
|
||||||
// handled upstream of this call.
|
// handled upstream of this call.
|
||||||
func (e *Bus) setFailure(err error) *Bus {
|
func (e *Bus) setFailure(err error) *Bus {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
if e.failure == nil {
|
if e.failure == nil {
|
||||||
e.failure = err
|
e.failure = err
|
||||||
return e
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// technically not a recoverable error: we're using the
|
// technically not a recoverable error: we're using the
|
||||||
// recoverable slice as an overflow container here to
|
// recoverable slice as an overflow container here to
|
||||||
// ensure everything is tracked.
|
// ensure everything is tracked.
|
||||||
e.recoverable = append(e.recoverable, err)
|
e.recoverable = append(e.recoverable, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.parent != nil {
|
||||||
|
e.parent.setFailure(err)
|
||||||
|
}
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
@ -127,17 +134,11 @@ func (e *Bus) setFailure(err error) *Bus {
|
|||||||
// errors (ie: bus.recoverable). If failFast is true, the first
|
// errors (ie: bus.recoverable). If failFast is true, the first
|
||||||
// added error will get copied to bus.failure, causing the bus
|
// added error will get copied to bus.failure, causing the bus
|
||||||
// to identify as non-recoverably failed.
|
// 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) {
|
func (e *Bus) AddRecoverable(ctx context.Context, err error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
e.logAndAddRecoverable(ctx, err, 1)
|
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
|
// gets handled upstream of this call. Returns true if the
|
||||||
// error is a failure, false otherwise.
|
// error is a failure, false otherwise.
|
||||||
func (e *Bus) addRecoverableErr(err error) bool {
|
func (e *Bus) addRecoverableErr(err error) bool {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
var isFail bool
|
var isFail bool
|
||||||
|
|
||||||
if e.failure == nil && e.failFast {
|
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
|
isFail = true
|
||||||
}
|
}
|
||||||
|
|
||||||
e.recoverable = append(e.recoverable, err)
|
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
|
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.
|
// AddSkip appends a record of a Skipped item to the fault bus.
|
||||||
// Importantly, skipped items are not the same as recoverable
|
// Importantly, skipped items are not the same as recoverable
|
||||||
// errors. An item should only be skipped under the following
|
// errors. An item should only be skipped under the following
|
||||||
@ -186,9 +245,6 @@ func (e *Bus) AddSkip(ctx context.Context, s *Skipped) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e.mu.Lock()
|
|
||||||
defer e.mu.Unlock()
|
|
||||||
|
|
||||||
e.logAndAddSkip(ctx, s, 1)
|
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) {
|
func (e *Bus) logAndAddSkip(ctx context.Context, s *Skipped, trace int) {
|
||||||
logger.CtxStack(ctx, trace+1).
|
logger.CtxStack(ctx, trace+1).
|
||||||
With("skipped", s).
|
With("skipped", s).
|
||||||
Info("skipped item")
|
Info("skipped an item")
|
||||||
e.addSkip(s)
|
e.addSkip(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Bus) addSkip(s *Skipped) *Bus {
|
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()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
e.logAndAddAlert(ctx, a, 1)
|
e.skipped = append(e.skipped, *s)
|
||||||
}
|
|
||||||
|
|
||||||
// logs the error and adds an alert.
|
// local bus instances must promote skipped items to the root bus.
|
||||||
func (e *Bus) logAndAddAlert(ctx context.Context, a *Alert, trace int) {
|
if e.parent != nil {
|
||||||
logger.CtxStack(ctx, trace+1).
|
e.parent.addSkip(s)
|
||||||
With("alert", a).
|
}
|
||||||
Info("alert: " + a.Message)
|
|
||||||
e.addAlert(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Bus) addAlert(a *Alert) *Bus {
|
|
||||||
e.alerts = append(e.alerts, *a)
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Results
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Errors returns the plain record of errors that were aggregated
|
// Errors returns the plain record of errors that were aggregated
|
||||||
// within a fult Bus.
|
// within a fult Bus.
|
||||||
func (e *Bus) Errors() *Errors {
|
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
|
// ItemsAndRecovered returns the items that failed along with other
|
||||||
// recoverable errors
|
// recoverable errors
|
||||||
func (e *Bus) ItemsAndRecovered() ([]Item, []error) {
|
func (e *Bus) ItemsAndRecovered() ([]Item, []error) {
|
||||||
@ -275,10 +348,6 @@ func (e *Bus) ItemsAndRecovered() ([]Item, []error) {
|
|||||||
return maps.Values(is), non
|
return maps.Values(is), non
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Errors Data
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Errors provides the errors data alone, without sync controls
|
// Errors provides the errors data alone, without sync controls
|
||||||
// or adders/setters. Expected to get called at the end of processing,
|
// or adders/setters. Expected to get called at the end of processing,
|
||||||
// as a way to aggregate results.
|
// 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
|
// Print writes the DetailModel Entries to StdOut, in the format
|
||||||
// requested by the caller.
|
// requested by the caller.
|
||||||
func (e *Errors) PrintItems(
|
func (e *Errors) PrintItems(
|
||||||
@ -430,73 +503,3 @@ func (pec printableErrCore) Values() []string {
|
|||||||
|
|
||||||
return []string{pec.Msg}
|
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"
|
|
||||||
|
|||||||
@ -189,25 +189,6 @@ func (suite *FaultErrorsUnitSuite) TestAdd() {
|
|||||||
assert.Len(t, n.Recovered(), 2)
|
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() {
|
func (suite *FaultErrorsUnitSuite) TestErrors() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
|
|||||||
@ -11,15 +11,15 @@ const (
|
|||||||
AddtlMalwareDesc = "malware_description"
|
AddtlMalwareDesc = "malware_description"
|
||||||
)
|
)
|
||||||
|
|
||||||
type itemType string
|
type ItemType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FileType itemType = "file"
|
FileType ItemType = "file"
|
||||||
ContainerType itemType = "container"
|
ContainerType ItemType = "container"
|
||||||
ResourceOwnerType itemType = "resource_owner"
|
ResourceOwnerType ItemType = "resource_owner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (it itemType) Printable() string {
|
func (it ItemType) Printable() string {
|
||||||
switch it {
|
switch it {
|
||||||
case FileType:
|
case FileType:
|
||||||
return "File"
|
return "File"
|
||||||
@ -62,7 +62,7 @@ type Item struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
// tracks the type of item represented by this entry.
|
// 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
|
// Error() of the causal error, or a sentinel if this is the
|
||||||
// source of the error. In case of ID collisions, the first
|
// 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.
|
// 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{
|
return &Item{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
ID: id,
|
ID: id,
|
||||||
@ -148,183 +148,3 @@ func itemErr(t itemType, cause error, namespace, id, name string, addtl map[stri
|
|||||||
Additional: addtl,
|
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package fault
|
package fault_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ItemUnitSuite struct {
|
type ItemUnitSuite struct {
|
||||||
@ -21,28 +22,28 @@ func TestItemUnitSuite(t *testing.T) {
|
|||||||
func (suite *ItemUnitSuite) TestItem_Error() {
|
func (suite *ItemUnitSuite) TestItem_Error() {
|
||||||
var (
|
var (
|
||||||
t = suite.T()
|
t = suite.T()
|
||||||
i *Item
|
i *fault.Item
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Contains(t, i.Error(), "nil")
|
assert.Contains(t, i.Error(), "nil")
|
||||||
|
|
||||||
i = &Item{}
|
i = &fault.Item{}
|
||||||
assert.Contains(t, i.Error(), "unknown type")
|
assert.Contains(t, i.Error(), "unknown type")
|
||||||
|
|
||||||
i = &Item{Type: FileType}
|
i = &fault.Item{Type: fault.FileType}
|
||||||
assert.Contains(t, i.Error(), FileType)
|
assert.Contains(t, i.Error(), fault.FileType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ItemUnitSuite) TestContainerErr() {
|
func (suite *ItemUnitSuite) TestContainerErr() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
addtl := map[string]any{"foo": "bar"}
|
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",
|
Namespace: "ns",
|
||||||
ID: "id",
|
ID: "id",
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Type: ContainerType,
|
Type: fault.ContainerType,
|
||||||
Cause: "foo",
|
Cause: "foo",
|
||||||
Additional: addtl,
|
Additional: addtl,
|
||||||
}
|
}
|
||||||
@ -53,13 +54,13 @@ func (suite *ItemUnitSuite) TestContainerErr() {
|
|||||||
func (suite *ItemUnitSuite) TestFileErr() {
|
func (suite *ItemUnitSuite) TestFileErr() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
addtl := map[string]any{"foo": "bar"}
|
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",
|
Namespace: "ns",
|
||||||
ID: "id",
|
ID: "id",
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Type: FileType,
|
Type: fault.FileType,
|
||||||
Cause: "foo",
|
Cause: "foo",
|
||||||
Additional: addtl,
|
Additional: addtl,
|
||||||
}
|
}
|
||||||
@ -70,13 +71,13 @@ func (suite *ItemUnitSuite) TestFileErr() {
|
|||||||
func (suite *ItemUnitSuite) TestOwnerErr() {
|
func (suite *ItemUnitSuite) TestOwnerErr() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
addtl := map[string]any{"foo": "bar"}
|
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",
|
Namespace: "ns",
|
||||||
ID: "id",
|
ID: "id",
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Type: ResourceOwnerType,
|
Type: fault.ResourceOwnerType,
|
||||||
Cause: "foo",
|
Cause: "foo",
|
||||||
Additional: addtl,
|
Additional: addtl,
|
||||||
}
|
}
|
||||||
@ -86,23 +87,23 @@ func (suite *ItemUnitSuite) TestOwnerErr() {
|
|||||||
|
|
||||||
func (suite *ItemUnitSuite) TestItemType_Printable() {
|
func (suite *ItemUnitSuite) TestItemType_Printable() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
t itemType
|
t fault.ItemType
|
||||||
expect string
|
expect string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
t: FileType,
|
t: fault.FileType,
|
||||||
expect: "File",
|
expect: "File",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: ContainerType,
|
t: fault.ContainerType,
|
||||||
expect: "Container",
|
expect: "Container",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: ResourceOwnerType,
|
t: fault.ResourceOwnerType,
|
||||||
expect: "Resource Owner",
|
expect: "Resource Owner",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
t: itemType("foo"),
|
t: fault.ItemType("foo"),
|
||||||
expect: "Unknown",
|
expect: "Unknown",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -118,30 +119,30 @@ func (suite *ItemUnitSuite) TestItem_HeadersValues() {
|
|||||||
err = assert.AnError
|
err = assert.AnError
|
||||||
cause = err.Error()
|
cause = err.Error()
|
||||||
addtl = map[string]any{
|
addtl = map[string]any{
|
||||||
AddtlContainerID: "cid",
|
fault.AddtlContainerID: "cid",
|
||||||
AddtlContainerName: "cname",
|
fault.AddtlContainerName: "cname",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
item *Item
|
item *fault.Item
|
||||||
expect []string
|
expect []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "file",
|
name: "file",
|
||||||
item: FileErr(assert.AnError, "ns", "id", "name", addtl),
|
item: fault.FileErr(assert.AnError, "ns", "id", "name", addtl),
|
||||||
expect: []string{"Error", FileType.Printable(), "name", "cname", cause},
|
expect: []string{"Error", fault.FileType.Printable(), "name", "cname", cause},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "container",
|
name: "container",
|
||||||
item: ContainerErr(assert.AnError, "ns", "id", "name", addtl),
|
item: fault.ContainerErr(assert.AnError, "ns", "id", "name", addtl),
|
||||||
expect: []string{"Error", ContainerType.Printable(), "name", "cname", cause},
|
expect: []string{"Error", fault.ContainerType.Printable(), "name", "cname", cause},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "owner",
|
name: "owner",
|
||||||
item: OwnerErr(assert.AnError, "ns", "id", "name", nil),
|
item: fault.OwnerErr(assert.AnError, "ns", "id", "name", nil),
|
||||||
expect: []string{"Error", ResourceOwnerType.Printable(), "name", "", cause},
|
expect: []string{"Error", fault.ResourceOwnerType.Printable(), "name", "", cause},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
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
117
src/pkg/fault/skipped.go
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/pkg/fault/skipped_test.go
Normal file
146
src/pkg/fault/skipped_test.go
Normal 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)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user