add NoData operation status (#1042)

## Description

Adds a NoData status to operations in the event
that a backup (or, possibly in the future, restore)
finishes processing without having any items to
store or restore.

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #1000

## Test Plan

- [x] 💪 Manual
- [x]  Unit test
This commit is contained in:
Keepers 2022-10-04 17:08:17 -06:00 committed by GitHub
parent 3a62f7f90c
commit 630d74bee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 154 additions and 88 deletions

View File

@ -171,6 +171,10 @@ func (op *BackupOperation) persistResults(
opStats.writeErr)
}
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
op.Status = NoData
}
op.Results.ReadErrors = opStats.readErr
op.Results.WriteErrors = opStats.writeErr

View File

@ -5,7 +5,6 @@ import (
"testing"
"time"
multierror "github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -35,18 +34,24 @@ func TestBackupOpSuite(t *testing.T) {
}
func (suite *BackupOpSuite) TestBackupOperation_PersistResults() {
t := suite.T()
ctx := context.Background()
var (
ctx = context.Background()
kw = &kopia.Wrapper{}
sw = &store.Wrapper{}
acct = account.Account{}
now = time.Now()
stats = backupStats{
)
table := []struct {
expectStatus opStatus
expectErr assert.ErrorAssertionFunc
stats backupStats
}{
{
expectStatus: Completed,
expectErr: assert.NoError,
stats: backupStats{
started: true,
readErr: multierror.Append(nil, assert.AnError),
writeErr: assert.AnError,
resourceCount: 1,
k: &kopia.BackupStats{
TotalFileCount: 1,
@ -56,9 +61,29 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() {
gc: &support.ConnectorOperationStatus{
Successful: 1,
},
},
},
{
expectStatus: Failed,
expectErr: assert.Error,
stats: backupStats{
started: false,
k: &kopia.BackupStats{},
gc: &support.ConnectorOperationStatus{},
},
},
{
expectStatus: NoData,
expectErr: assert.NoError,
stats: backupStats{
started: true,
k: &kopia.BackupStats{},
gc: &support.ConnectorOperationStatus{},
},
},
}
)
for _, test := range table {
suite.T().Run(test.expectStatus.String(), func(t *testing.T) {
op, err := NewBackupOperation(
ctx,
control.Options{},
@ -68,19 +93,20 @@ func (suite *BackupOpSuite) TestBackupOperation_PersistResults() {
selectors.Selector{},
evmock.NewBus())
require.NoError(t, err)
test.expectErr(t, op.persistResults(now, &test.stats))
require.NoError(t, op.persistResults(now, &stats))
assert.Equal(t, op.Status.String(), Completed.String(), "status")
assert.Equal(t, op.Results.ItemsRead, stats.gc.Successful, "items read")
assert.Equal(t, op.Results.ReadErrors, stats.readErr, "read errors")
assert.Equal(t, op.Results.ItemsWritten, stats.k.TotalFileCount, "items written")
assert.Equal(t, stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read")
assert.Equal(t, stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written")
assert.Equal(t, op.Results.ResourceOwners, stats.resourceCount, "resource owners")
assert.Equal(t, op.Results.WriteErrors, stats.writeErr, "write errors")
assert.Equal(t, op.Results.StartedAt, now, "started at")
assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status")
assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsRead, "items read")
assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors")
assert.Equal(t, test.stats.k.TotalFileCount, op.Results.ItemsWritten, "items written")
assert.Equal(t, test.stats.k.TotalHashedBytes, op.Results.BytesRead, "bytes read")
assert.Equal(t, test.stats.k.TotalUploadedBytes, op.Results.BytesUploaded, "bytes written")
assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners")
assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors")
assert.Equal(t, now, op.Results.StartedAt, "started at")
assert.Less(t, now, op.Results.CompletedAt, "completed at")
})
}
}
// ---------------------------------------------------------------------------

View File

@ -13,13 +13,17 @@ import (
// opStatus describes the current status of an operation.
// InProgress - the standard value for any process that has not
// arrived at an end state. The two end states are Failed and
// Completed.
// arrived at an end state. The end states are Failed, Completed,
// or NoData.
// Failed - the operation was unable to begin processing data at all.
// No items have been written by the consumer.
// Completed - the operation was able to process one or more of the
// items in the request. Both partial success (0 < N < len(items)
// errored) and total success (0 errors) are set as Completed.
// NoData - only occurs when no data was involved in an operation.
// For example, if a backup is requested for a specific user's
// mail, but that account contains zero mail messages, the backup
// contains No Data.
type opStatus int
//go:generate stringer -type=opStatus -linecomment
@ -28,6 +32,7 @@ const (
InProgress // In Progress
Completed // Completed
Failed // Failed
NoData // No Data
)
// --------------------------------------------------------------------------------

View File

@ -12,11 +12,12 @@ func _() {
_ = x[InProgress-1]
_ = x[Completed-2]
_ = x[Failed-3]
_ = x[NoData-4]
}
const _opStatus_name = "Status UnknownIn ProgressCompletedFailed"
const _opStatus_name = "Status UnknownIn ProgressCompletedFailedNo Data"
var _opStatus_index = [...]uint8{0, 14, 25, 34, 40}
var _opStatus_index = [...]uint8{0, 14, 25, 34, 40, 47}
func (i opStatus) String() string {
if i < 0 || i >= opStatus(len(_opStatus_index)-1) {

View File

@ -195,6 +195,10 @@ func (op *RestoreOperation) persistResults(
opStats.writeErr)
}
if opStats.readErr == nil && opStats.writeErr == nil && opStats.gc.Successful == 0 {
op.Status = NoData
}
op.Results.ReadErrors = opStats.readErr
op.Results.WriteErrors = opStats.writeErr

View File

@ -5,7 +5,6 @@ import (
"testing"
"time"
multierror "github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -38,21 +37,26 @@ func TestRestoreOpSuite(t *testing.T) {
suite.Run(t, new(RestoreOpSuite))
}
// TODO: after modelStore integration is added, mock the store and/or
// move this to an integration test.
func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() {
var (
t = suite.T()
ctx = context.Background()
kw = &kopia.Wrapper{}
sw = &store.Wrapper{}
acct = account.Account{}
now = time.Now()
rs = restoreStats{
dest = control.DefaultRestoreDestination(common.SimpleDateTimeFormat)
)
table := []struct {
expectStatus opStatus
expectErr assert.ErrorAssertionFunc
stats restoreStats
}{
{
expectStatus: Completed,
expectErr: assert.NoError,
stats: restoreStats{
started: true,
readErr: multierror.Append(nil, assert.AnError),
writeErr: assert.AnError,
resourceCount: 1,
bytesRead: &stats.ByteCounter{
NumBytes: 42,
@ -60,11 +64,32 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() {
cs: []data.Collection{&exchange.Collection{}},
gc: &support.ConnectorOperationStatus{
ObjectCount: 1,
Successful: 1,
},
},
},
{
expectStatus: Failed,
expectErr: assert.Error,
stats: restoreStats{
started: false,
bytesRead: &stats.ByteCounter{},
gc: &support.ConnectorOperationStatus{},
},
},
{
expectStatus: NoData,
expectErr: assert.NoError,
stats: restoreStats{
started: true,
bytesRead: &stats.ByteCounter{},
cs: []data.Collection{},
gc: &support.ConnectorOperationStatus{},
},
},
}
dest = control.DefaultRestoreDestination(common.SimpleDateTimeFormat)
)
for _, test := range table {
suite.T().Run(test.expectStatus.String(), func(t *testing.T) {
op, err := NewRestoreOperation(
ctx,
control.Options{},
@ -76,18 +101,19 @@ func (suite *RestoreOpSuite) TestRestoreOperation_PersistResults() {
dest,
evmock.NewBus())
require.NoError(t, err)
test.expectErr(t, op.persistResults(ctx, now, &test.stats))
require.NoError(t, op.persistResults(ctx, now, &rs))
assert.Equal(t, op.Status.String(), Completed.String(), "status")
assert.Equal(t, op.Results.ItemsRead, len(rs.cs), "items read")
assert.Equal(t, op.Results.ReadErrors, rs.readErr, "read errors")
assert.Equal(t, op.Results.ItemsWritten, rs.gc.Successful, "items written")
assert.Equal(t, rs.bytesRead.NumBytes, op.Results.BytesRead, "resource owners")
assert.Equal(t, rs.resourceCount, op.Results.ResourceOwners, "resource owners")
assert.Equal(t, op.Results.WriteErrors, rs.writeErr, "write errors")
assert.Equal(t, op.Results.StartedAt, now, "started at")
assert.Equal(t, test.expectStatus.String(), op.Status.String(), "status")
assert.Equal(t, len(test.stats.cs), op.Results.ItemsRead, "items read")
assert.Equal(t, test.stats.readErr, op.Results.ReadErrors, "read errors")
assert.Equal(t, test.stats.gc.Successful, op.Results.ItemsWritten, "items written")
assert.Equal(t, test.stats.bytesRead.NumBytes, op.Results.BytesRead, "resource owners")
assert.Equal(t, test.stats.resourceCount, op.Results.ResourceOwners, "resource owners")
assert.Equal(t, test.stats.writeErr, op.Results.WriteErrors, "write errors")
assert.Equal(t, now, op.Results.StartedAt, "started at")
assert.Less(t, now, op.Results.CompletedAt, "completed at")
})
}
}
// ---------------------------------------------------------------------------