Compare commits
1 Commits
main
...
skip-email
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c90957d06d |
@ -120,6 +120,16 @@ var (
|
|||||||
// replies, no error should get returned.
|
// replies, no error should get returned.
|
||||||
ErrMultipleResultsMatchIdentifier = clues.New("multiple results match the identifier")
|
ErrMultipleResultsMatchIdentifier = clues.New("multiple results match the identifier")
|
||||||
|
|
||||||
|
// ErrNoRespServerFailure is a generic name for a specific condition: when the request
|
||||||
|
// fails out after all attempted retries with the conditions:
|
||||||
|
// 1. response status code 503
|
||||||
|
// 2. response content length <= 0
|
||||||
|
// This can indicate a persistent inability to access the requested resource. It's
|
||||||
|
// difficult to determine the underlying cause, since the server provides no response
|
||||||
|
// body. In many cases this is a non-transient issue and must be skipped to ensure
|
||||||
|
// the operation succeeds.
|
||||||
|
ErrNoRespServerFailure = clues.New("server failed to respond to request")
|
||||||
|
|
||||||
// ErrResourceLocked occurs when a resource has had its access locked.
|
// ErrResourceLocked occurs when a resource has had its access locked.
|
||||||
// Example case: https://learn.microsoft.com/en-us/sharepoint/manage-lock-status
|
// Example case: https://learn.microsoft.com/en-us/sharepoint/manage-lock-status
|
||||||
// This makes the resource inaccessible for any Corso operations.
|
// This makes the resource inaccessible for any Corso operations.
|
||||||
@ -285,6 +295,10 @@ func IsErrResourceLocked(err error) bool {
|
|||||||
hasErrorCode(err, NotAllowed)
|
hasErrorCode(err, NotAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsErrNoRespServerFailure(err error) bool {
|
||||||
|
return errors.Is(err, ErrNoRespServerFailure)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// error parsers
|
// error parsers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -162,6 +162,40 @@ func (suite *GraphErrorsUnitSuite) TestIsErrAuthenticationError() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *GraphErrorsUnitSuite) TestIsErrNoRespServerFailure() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expect assert.BoolAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
err: nil,
|
||||||
|
expect: assert.False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching",
|
||||||
|
err: assert.AnError,
|
||||||
|
expect: assert.False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching oDataErr",
|
||||||
|
err: odErr(ErrNoRespServerFailure.Error()),
|
||||||
|
expect: assert.False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching error",
|
||||||
|
err: ErrNoRespServerFailure,
|
||||||
|
expect: assert.True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
test.expect(suite.T(), IsErrNoRespServerFailure(test.err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *GraphErrorsUnitSuite) TestIsErrDeletedInFlight() {
|
func (suite *GraphErrorsUnitSuite) TestIsErrDeletedInFlight() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@ -180,6 +180,7 @@ func defaultTransport() http.RoundTripper {
|
|||||||
|
|
||||||
func internalMiddleware(cc *clientConfig) []khttp.Middleware {
|
func internalMiddleware(cc *clientConfig) []khttp.Middleware {
|
||||||
mw := []khttp.Middleware{
|
mw := []khttp.Middleware{
|
||||||
|
&ErrorIdentifierMiddleware{},
|
||||||
&RetryMiddleware{
|
&RetryMiddleware{
|
||||||
MaxRetries: cc.maxRetries,
|
MaxRetries: cc.maxRetries,
|
||||||
Delay: cc.minDelay,
|
Delay: cc.minDelay,
|
||||||
|
|||||||
@ -369,3 +369,35 @@ func (mw *MetricsMiddleware) Intercept(
|
|||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error Edge Case Identifier
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ErrorIdentifierMiddleware ensures known edge cases result in well-represented errors.
|
||||||
|
type ErrorIdentifierMiddleware struct{}
|
||||||
|
|
||||||
|
func (mw *ErrorIdentifierMiddleware) Intercept(
|
||||||
|
pipeline khttp.Pipeline,
|
||||||
|
middlewareIndex int,
|
||||||
|
req *http.Request,
|
||||||
|
) (*http.Response, error) {
|
||||||
|
ctx := req.Context()
|
||||||
|
resp, err := pipeline.Next(req, middlewareIndex)
|
||||||
|
|
||||||
|
if resp == nil || err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusServiceUnavailable && resp.ContentLength <= 0 {
|
||||||
|
// log the response body dump just for security. Sometimes a "0 content length"
|
||||||
|
// is actually due to the client's inability to parse the response, and not that
|
||||||
|
// the response content is actually missing.
|
||||||
|
dump := getRespDump(ctx, resp, true)
|
||||||
|
logger.Ctx(ctx).Infow("graph api resp - 503 with no content", "response", dump)
|
||||||
|
|
||||||
|
return nil, clues.Stack(ErrNoRespServerFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|||||||
@ -274,6 +274,7 @@ func kiotaMiddlewares(
|
|||||||
) []khttp.Middleware {
|
) []khttp.Middleware {
|
||||||
mw := []khttp.Middleware{
|
mw := []khttp.Middleware{
|
||||||
msgraphgocore.NewGraphTelemetryHandler(options),
|
msgraphgocore.NewGraphTelemetryHandler(options),
|
||||||
|
&ErrorIdentifierMiddleware{},
|
||||||
&RetryMiddleware{
|
&RetryMiddleware{
|
||||||
MaxRetries: cc.maxRetries,
|
MaxRetries: cc.maxRetries,
|
||||||
Delay: cc.minDelay,
|
Delay: cc.minDelay,
|
||||||
|
|||||||
@ -39,7 +39,8 @@ func (bc *ByteCounter) Count(i int64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SkippedCounts struct {
|
type SkippedCounts struct {
|
||||||
TotalSkippedItems int `json:"totalSkippedItems"`
|
TotalSkippedItems int `json:"totalSkippedItems"`
|
||||||
SkippedMalware int `json:"skippedMalware"`
|
SkippedMalware int `json:"skippedMalware"`
|
||||||
SkippedInvalidOneNoteFile int `json:"skippedInvalidOneNoteFile"`
|
SkippedInvalidOneNoteFile int `json:"skippedInvalidOneNoteFile"`
|
||||||
|
SkippedPermanentServiceFailure int `json:"skippedPermanentServiceFailure"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ func New(
|
|||||||
skipCount = len(fe.Skipped)
|
skipCount = len(fe.Skipped)
|
||||||
failMsg string
|
failMsg string
|
||||||
|
|
||||||
malware, invalidONFile, otherSkips int
|
malware, invalidONFile, permanentServiceFailure, otherSkips int
|
||||||
)
|
)
|
||||||
|
|
||||||
if fe.Failure != nil {
|
if fe.Failure != nil {
|
||||||
@ -104,6 +104,8 @@ func New(
|
|||||||
malware++
|
malware++
|
||||||
case s.HasCause(fault.SkipOneNote):
|
case s.HasCause(fault.SkipOneNote):
|
||||||
invalidONFile++
|
invalidONFile++
|
||||||
|
case s.HasCause(fault.SkipPermanentServiceFailure):
|
||||||
|
permanentServiceFailure++
|
||||||
default:
|
default:
|
||||||
otherSkips++
|
otherSkips++
|
||||||
}
|
}
|
||||||
@ -134,9 +136,10 @@ func New(
|
|||||||
ReadWrites: rw,
|
ReadWrites: rw,
|
||||||
StartAndEndTime: se,
|
StartAndEndTime: se,
|
||||||
SkippedCounts: stats.SkippedCounts{
|
SkippedCounts: stats.SkippedCounts{
|
||||||
TotalSkippedItems: skipCount,
|
TotalSkippedItems: skipCount,
|
||||||
SkippedMalware: malware,
|
SkippedMalware: malware,
|
||||||
SkippedInvalidOneNoteFile: invalidONFile,
|
SkippedInvalidOneNoteFile: invalidONFile,
|
||||||
|
SkippedPermanentServiceFailure: permanentServiceFailure,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,6 +248,10 @@ func (b Backup) Values() []string {
|
|||||||
skipped = append(skipped, fmt.Sprintf("%d invalid OneNote file", b.SkippedInvalidOneNoteFile))
|
skipped = append(skipped, fmt.Sprintf("%d invalid OneNote file", b.SkippedInvalidOneNoteFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.SkippedPermanentServiceFailure > 0 {
|
||||||
|
skipped = append(skipped, fmt.Sprintf("%d permanent service failures", b.SkippedPermanentServiceFailure))
|
||||||
|
}
|
||||||
|
|
||||||
status += strings.Join(skipped, ", ")
|
status += strings.Join(skipped, ", ")
|
||||||
|
|
||||||
if errCount+b.TotalSkippedItems > 0 {
|
if errCount+b.TotalSkippedItems > 0 {
|
||||||
|
|||||||
@ -202,17 +202,30 @@ func (suite *BackupUnitSuite) TestBackup_Values_statusVariations() {
|
|||||||
expect: "test (42 errors, 1 skipped: 1 invalid OneNote file)",
|
expect: "test (42 errors, 1 skipped: 1 invalid OneNote file)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "errors, malware, notFound, invalid OneNote",
|
name: "errors and permanent service failures",
|
||||||
bup: backup.Backup{
|
bup: backup.Backup{
|
||||||
Status: "test",
|
Status: "test",
|
||||||
ErrorCount: 42,
|
ErrorCount: 42,
|
||||||
SkippedCounts: stats.SkippedCounts{
|
SkippedCounts: stats.SkippedCounts{
|
||||||
TotalSkippedItems: 1,
|
TotalSkippedItems: 1,
|
||||||
SkippedMalware: 1,
|
SkippedPermanentServiceFailure: 1,
|
||||||
SkippedInvalidOneNoteFile: 1,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expect: "test (42 errors, 1 skipped: 1 malware, 1 invalid OneNote file)",
|
expect: "test (42 errors, 1 skipped: 1 permanent service failures)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "errors, malware, notFound, invalid OneNote, permanent service failures",
|
||||||
|
bup: backup.Backup{
|
||||||
|
Status: "test",
|
||||||
|
ErrorCount: 42,
|
||||||
|
SkippedCounts: stats.SkippedCounts{
|
||||||
|
TotalSkippedItems: 1,
|
||||||
|
SkippedMalware: 1,
|
||||||
|
SkippedInvalidOneNoteFile: 1,
|
||||||
|
SkippedPermanentServiceFailure: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: "test (42 errors, 1 skipped: 1 malware, 1 invalid OneNote file, 1 permanent service failures)",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
|
|||||||
@ -32,6 +32,12 @@ const (
|
|||||||
//nolint:lll
|
//nolint:lll
|
||||||
// https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#onenotenotebooks
|
// https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#onenotenotebooks
|
||||||
SkipOneNote skipCause = "inaccessible_one_note_file"
|
SkipOneNote skipCause = "inaccessible_one_note_file"
|
||||||
|
|
||||||
|
// SkipPermanentServiceFailure identifies that a file was skipped
|
||||||
|
// because a request failed out with a 503 status code and a response
|
||||||
|
// with no content. We assume this case to represent non-transient
|
||||||
|
// conditions.
|
||||||
|
SkipPermanentServiceFailure skipCause = "permanent_service_failure"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ print.Printable = &Skipped{}
|
var _ print.Printable = &Skipped{}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user