first pass on compliance with the reason

establishes behavior around using the reasoner interface
in a world where paths can contain multiple services.  Primarily
focused on ensuring the reasoner clearly guides maintainers
towards proper usage.
This commit is contained in:
ryanfkeepers 2023-08-15 13:29:14 -06:00
parent 48aae5d485
commit 3d15a0d649
12 changed files with 155 additions and 38 deletions

View File

@ -111,6 +111,12 @@ func (bb *backupBases) ClearAssistBases() {
bb.assistBases = nil
}
// BaseKeyServiceCategory makes a backup base key using
// the reasoner's Service and Category.
func BaseKeyServiceCategory(br identity.Reasoner) string {
return br.Service().String() + br.Category().String()
}
// MergeBackupBases reduces the two BackupBases into a single BackupBase.
// Assumes the passed in BackupBases represents a prior backup version (across
// some migration that disrupts lookup), and that the BackupBases used to call

View File

@ -14,6 +14,7 @@ import (
"github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/identity"
idMock "github.com/alcionai/corso/src/pkg/backup/identity/mock"
"github.com/alcionai/corso/src/pkg/path"
)
@ -460,18 +461,12 @@ func (suite *BackupBasesUnitSuite) TestMergeBackupBases() {
bb := makeBackupBases(test.merge, test.assist)
other := makeBackupBases(test.otherMerge, test.otherAssist)
expected := test.expect()
ctx, flush := tester.NewContext(t)
defer flush()
got := bb.MergeBackupBases(
ctx,
other,
func(r identity.Reasoner) string {
return r.Service().String() + r.Category().String()
})
AssertBackupBasesEqual(t, expected, got)
got := bb.MergeBackupBases(ctx, other, BaseKeyServiceCategory)
AssertBackupBasesEqual(t, test.expect(), got)
})
}
}
@ -843,3 +838,37 @@ func (suite *BackupBasesUnitSuite) TestFixupAndVerify() {
})
}
}
func (suite *BackupBasesUnitSuite) TestBaseKeyServiceCategory() {
table := []struct {
name string
service path.ServiceType
category path.CategoryType
expect string
}{
{
name: "unknown",
service: path.UnknownService,
category: path.UnknownCategory,
expect: "unknown",
},
{
name: "known service",
service: path.ExchangeService,
category: path.EmailCategory,
expect: "exchangeEmail",
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := BaseKeyServiceCategory(idMock.Reason{
TenantID: "tid",
Cat: test.category,
Svc: test.service,
})
assert.Equal(t, test.expect, result)
})
}
}

View File

@ -70,11 +70,14 @@ func (r reason) Category() path.CategoryType {
}
func (r reason) SubtreePath() (path.Path, error) {
p, err := path.BuildPrefix(
r.Tenant(),
r.ProtectedResource(),
srs, err := path.NewServiceResources(
r.Service(),
r.Category())
r.ProtectedResource())
if err != nil {
return nil, clues.Wrap(err, "building path service prefix")
}
p, err := path.BuildPrefix(r.Tenant(), srs, r.Category())
return p, clues.Wrap(err, "building path").OrNil()
}

View File

@ -196,14 +196,17 @@ func (cp *corsoProgress) FinishedFile(relativePath string, err error) {
return
}
ctx := clues.Add(
cp.ctx,
"services", path.ServiceResourcesToServices(d.repoPath.ServiceResources()),
"category", d.repoPath.Category().String())
// These items were sourced from a base snapshot or were cached in kopia so we
// never had to materialize their details in-memory.
if d.info == nil || d.cached {
if d.prevPath == nil {
cp.errs.AddRecoverable(cp.ctx, clues.New("item sourced from previous backup with no previous path").
With(
"service", d.repoPath.Service().String(),
"category", d.repoPath.Category().String()).
WithClues(ctx).
Label(fault.LabelForceNoBackupCreation))
return
@ -219,9 +222,7 @@ func (cp *corsoProgress) FinishedFile(relativePath string, err error) {
d.locationPath)
if err != nil {
cp.errs.AddRecoverable(cp.ctx, clues.Wrap(err, "adding item to merge list").
With(
"service", d.repoPath.Service().String(),
"category", d.repoPath.Category().String()).
WithClues(ctx).
Label(fault.LabelForceNoBackupCreation))
}
@ -235,9 +236,7 @@ func (cp *corsoProgress) FinishedFile(relativePath string, err error) {
*d.info)
if err != nil {
cp.errs.AddRecoverable(cp.ctx, clues.New("adding item to details").
With(
"service", d.repoPath.Service().String(),
"category", d.repoPath.Category().String()).
WithClues(ctx).
Label(fault.LabelForceNoBackupCreation))
return

View File

@ -80,12 +80,7 @@ func getManifestsAndMetadata(
// 3. the current reasons contain all the necessary manifests.
// Note: This is not relevant for assist backups, since they are newly introduced
// and they don't exist with fallback reasons.
bb = bb.MergeBackupBases(
ctx,
fbb,
func(r identity.Reasoner) string {
return r.Service().String() + r.Category().String()
})
bb = bb.MergeBackupBases(ctx, fbb, kopia.BaseKeyServiceCategory)
if !getMetadata {
return bb, nil, false, nil

View File

@ -33,6 +33,7 @@ import (
"github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/backup/identity"
idMock "github.com/alcionai/corso/src/pkg/backup/identity/mock"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/count"
@ -244,7 +245,11 @@ func checkBackupIsInManifests(
for _, category := range categories {
t.Run(category.String(), func(t *testing.T) {
var (
r = kopia.NewReason("", resourceOwner, sel.PathService(), category)
r = idMock.Reason{
Cat: category,
Svc: sel.PathService(),
Resource: resourceOwner,
}
tags = map[string]string{kopia.TagBackupCategory: ""}
found bool
)

View File

@ -5,9 +5,20 @@ import "github.com/alcionai/corso/src/pkg/path"
// Reasoner describes the parts of the backup that make up its
// data identity: the tenant, protected resources, services, and
// categories which are held within the backup.
//
// Reasoner only recognizes the "primary" protected resource and
// service. IE: subservice resources and services are not recognized
// as part of the backup Reason.
type Reasoner interface {
Tenant() string
// ProtectedResource represents the Primary protected resource.
// IE: if a path or backup supports subservices, this value
// should only provide the first service's resource, and not the
// resource for any subservice.
ProtectedResource() string
// Service represents the Primary service.
// IE: if a path or backup supports subservices, this value
// should only provide the first service; not a subservice.
Service() path.ServiceType
Category() path.CategoryType
// SubtreePath returns the path prefix for data in existing backups that have

View File

@ -0,0 +1,47 @@
package mock
import (
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/pkg/path"
)
type Reason struct {
TenantID string
Cat path.CategoryType
Svc path.ServiceType
Resource string
SubtreeErr error
}
func (r Reason) Tenant() string {
return r.TenantID
}
func (r Reason) Category() path.CategoryType {
return r.Cat
}
func (r Reason) Service() path.ServiceType {
return r.Svc
}
func (r Reason) ProtectedResource() string {
return r.Resource
}
func (r Reason) SubtreePath() (path.Path, error) {
if r.SubtreeErr != nil {
return nil, r.SubtreeErr
}
p, err := path.BuildPrefix(
r.Tenant(),
[]path.ServiceResource{{
ProtectedResource: r.Resource,
Service: r.Svc,
}},
r.Category())
return p, clues.Wrap(err, "building path").OrNil()
}

View File

@ -113,9 +113,10 @@ func (suite *ServiceCategoryUnitSuite) TestToServiceType() {
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
assert.Equal(t, test.expected, toServiceType(test.service))
assert.Equal(
suite.T(),
test.expected,
ToServiceType(test.service))
})
}
}

View File

@ -85,6 +85,16 @@ func ServiceResourcesToResources(srs []ServiceResource) []string {
return prs
}
func ServiceResourcesToServices(srs []ServiceResource) []ServiceType {
sts := make([]ServiceType, len(srs))
for i := range srs {
sts[i] = srs[i].Service
}
return sts
}
func ServiceResourcesMatchServices(srs []ServiceResource, sts []ServiceType) bool {
return slices.EqualFunc(srs, sts, func(sr ServiceResource, st ServiceType) bool {
return sr.Service == st
@ -125,7 +135,7 @@ func elementsToServiceResources(elems Elements) ([]ServiceResource, int, error)
)
for j := 1; i < len(elems); i, j = i+2, j+2 {
service := toServiceType(elems[i])
service := ToServiceType(elems[i])
if service == UnknownService {
if i == 0 {
return nil, -1, clues.Wrap(errMissingSegment, "service")

View File

@ -33,7 +33,7 @@ const (
GroupsMetadataService // groupsMetadata
)
func toServiceType(service string) ServiceType {
func ToServiceType(service string) ServiceType {
s := strings.ToLower(service)
switch s {

View File

@ -3,6 +3,8 @@ package selectors
import (
"golang.org/x/exp/maps"
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/pkg/backup/identity"
"github.com/alcionai/corso/src/pkg/path"
@ -38,11 +40,12 @@ func (br backupReason) Category() path.CategoryType {
}
func (br backupReason) SubtreePath() (path.Path, error) {
return path.BuildPrefix(
br.tenant,
br.resource,
br.service,
br.category)
srs, err := path.NewServiceResources(br.service, br.resource)
if err != nil {
return nil, clues.Wrap(err, "building path prefix services")
}
return path.BuildPrefix(br.tenant, srs, br.category)
}
func (br backupReason) key() string {
@ -59,6 +62,14 @@ type servicerCategorizerProvider interface {
idname.Provider
}
// produces the Reasoner basis described by the selector.
// In cases of reasons with subservices (ie, multiple
// services described by a backup or path), the selector
// will only ever generate a ServiceResource for the first
// service+resource pair in the set.
//
// TODO: it may be possible, if necessary, to add subservice
// recognition to the service via additional scopes.
func reasonsFor(
sel servicerCategorizerProvider,
tenantID string,