More robust folder fetching for container cache (#3464)

Update folder cache population
* try to fetch missing parent folders
* refresh current folder if parent wasn't cached
* only populate paths during populatePaths not during IDToFolder

Manually tested email folder resolver population with
stable reproducer and succeeded

---

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

- [x]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-05-19 17:05:52 -07:00 committed by GitHub
parent d0c153aa15
commit 9520de1a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 496 additions and 43 deletions

View File

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
### Fixed ### Fixed
- Fix Exchange folder cache population error when parent folder isn't found.
### Known Issues ### Known Issues
## [v0.8.0] (beta) - 2023-05-15 ## [v0.8.0] (beta) - 2023-05-15

View File

@ -11,7 +11,29 @@ import (
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
var _ graph.ContainerResolver = &contactFolderCache{} var (
_ graph.ContainerResolver = &contactFolderCache{}
_ containerRefresher = &contactRefresher{}
)
type contactRefresher struct {
getter containerGetter
userID string
}
func (r *contactRefresher) refreshContainer(
ctx context.Context,
id string,
) (graph.CachedContainer, error) {
c, err := r.getter.GetContainerByID(ctx, r.userID, id)
if err != nil {
return nil, clues.Stack(err)
}
f := graph.NewCacheFolder(c, nil, nil)
return &f, nil
}
type contactFolderCache struct { type contactFolderCache struct {
*containerResolver *containerResolver
@ -34,7 +56,7 @@ func (cfc *contactFolderCache) populateContactRoot(
f, f,
path.Builder{}.Append(ptr.Val(f.GetId())), // path of IDs path.Builder{}.Append(ptr.Val(f.GetId())), // path of IDs
path.Builder{}.Append(baseContainerPath...)) // display location path.Builder{}.Append(baseContainerPath...)) // display location
if err := cfc.addFolder(temp); err != nil { if err := cfc.addFolder(&temp); err != nil {
return clues.Wrap(err, "adding resolver dir").WithClues(ctx) return clues.Wrap(err, "adding resolver dir").WithClues(ctx)
} }
@ -77,7 +99,10 @@ func (cfc *contactFolderCache) init(
} }
if cfc.containerResolver == nil { if cfc.containerResolver == nil {
cfc.containerResolver = newContainerResolver() cfc.containerResolver = newContainerResolver(&contactRefresher{
userID: cfc.userID,
getter: cfc.getter,
})
} }
return cfc.populateContactRoot(ctx, baseNode, baseContainerPath) return cfc.populateContactRoot(ctx, baseNode, baseContainerPath)

View File

@ -8,6 +8,7 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
@ -26,11 +27,18 @@ type containersEnumerator interface {
EnumerateContainers( EnumerateContainers(
ctx context.Context, ctx context.Context,
userID, baseDirID string, userID, baseDirID string,
fn func(graph.CacheFolder) error, fn func(graph.CachedContainer) error,
errs *fault.Bus, errs *fault.Bus,
) error ) error
} }
type containerRefresher interface {
refreshContainer(
ctx context.Context,
dirID string,
) (graph.CachedContainer, error)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// controller // controller
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -40,59 +48,243 @@ type containersEnumerator interface {
// folders if each folder is only a single character. // folders if each folder is only a single character.
const maxIterations = 300 const maxIterations = 300
func newContainerResolver() *containerResolver { func newContainerResolver(refresher containerRefresher) *containerResolver {
return &containerResolver{ return &containerResolver{
cache: map[string]graph.CachedContainer{}, cache: map[string]graph.CachedContainer{},
refresher: refresher,
} }
} }
type containerResolver struct { type containerResolver struct {
cache map[string]graph.CachedContainer cache map[string]graph.CachedContainer
refresher containerRefresher
} }
func (cr *containerResolver) IDToPath( func (cr *containerResolver) IDToPath(
ctx context.Context, ctx context.Context,
folderID string, folderID string,
) (*path.Builder, *path.Builder, error) { ) (*path.Builder, *path.Builder, error) {
return cr.idToPath(ctx, folderID, 0) ctx = clues.Add(ctx, "container_id", folderID)
c, ok := cr.cache[folderID]
if !ok {
return nil, nil, clues.New("container not cached").WithClues(ctx)
}
p := c.Path()
if p == nil {
return nil, nil, clues.New("cached container has no path").WithClues(ctx)
}
return p, c.Location(), nil
}
// refreshContainer attempts to fetch the container with the given ID from Graph
// API. Returns a graph.CachedContainer if the container was found. If the
// container was deleted, returns nil, true, nil to note the container should
// be removed from the cache.
func (cr *containerResolver) refreshContainer(
ctx context.Context,
id string,
) (graph.CachedContainer, bool, error) {
ctx = clues.Add(ctx, "refresh_container_id", id)
logger.Ctx(ctx).Debug("refreshing container")
if cr.refresher == nil {
return nil, false, clues.New("nil refresher").WithClues(ctx)
}
c, err := cr.refresher.refreshContainer(ctx, id)
if err != nil && graph.IsErrDeletedInFlight(err) {
logger.Ctx(ctx).Debug("container deleted")
return nil, true, nil
} else if err != nil {
// This is some other error, just return it.
return nil, false, clues.Wrap(err, "refreshing container").WithClues(ctx)
}
return c, false, nil
}
// recoverContainer attempts to fetch a missing container from Graph API and
// populate the path for it. It returns
// - the ID path for the folder
// - the display name path for the folder
// - if the folder was deleted
// - any error that occurred
//
// If the folder is marked as deleted, child folders of this folder should be
// deleted if they haven't been moved to another folder.
func (cr *containerResolver) recoverContainer(
ctx context.Context,
folderID string,
depth int,
) (*path.Builder, *path.Builder, bool, error) {
c, deleted, err := cr.refreshContainer(ctx, folderID)
if err != nil {
return nil, nil, false, clues.Wrap(err, "fetching uncached container")
}
if deleted {
logger.Ctx(ctx).Debug("fetching uncached container showed it was deleted")
return nil, nil, deleted, err
}
if err := cr.addFolder(c); err != nil {
return nil, nil, false, clues.Wrap(err, "adding new container").WithClues(ctx)
}
// Retry populating this container's paths.
//
// TODO(ashmrtn): May want to bump the depth here just so we don't get stuck
// retrying too much if for some reason things keep moving around?
resolved, err := cr.idToPath(ctx, folderID, depth)
if err != nil {
err = clues.Wrap(err, "repopulating uncached container")
}
return resolved.idPath, resolved.locPath, resolved.deleted, err
}
type resolvedPath struct {
idPath *path.Builder
locPath *path.Builder
cached bool
deleted bool
} }
func (cr *containerResolver) idToPath( func (cr *containerResolver) idToPath(
ctx context.Context, ctx context.Context,
folderID string, folderID string,
depth int, depth int,
) (*path.Builder, *path.Builder, error) { ) (resolvedPath, error) {
ctx = clues.Add(ctx, "container_id", folderID) ctx = clues.Add(ctx, "container_id", folderID)
if depth >= maxIterations { if depth >= maxIterations {
return nil, nil, clues.New("path contains cycle or is too tall").WithClues(ctx) return resolvedPath{
idPath: nil,
locPath: nil,
cached: false,
deleted: false,
}, clues.New("path contains cycle or is too tall").WithClues(ctx)
} }
c, ok := cr.cache[folderID] c, ok := cr.cache[folderID]
if !ok { if !ok {
return nil, nil, clues.New("folder not cached").WithClues(ctx) pth, loc, deleted, err := cr.recoverContainer(ctx, folderID, depth)
if err != nil {
err = clues.Stack(err)
}
return resolvedPath{
idPath: pth,
locPath: loc,
cached: false,
deleted: deleted,
}, err
} }
p := c.Path() p := c.Path()
if p != nil { if p != nil {
return p, c.Location(), nil return resolvedPath{
idPath: p,
locPath: c.Location(),
cached: true,
deleted: false,
}, nil
} }
parentPath, parentLoc, err := cr.idToPath( resolved, err := cr.idToPath(
ctx, ctx,
ptr.Val(c.GetParentFolderId()), ptr.Val(c.GetParentFolderId()),
depth+1) depth+1)
if err != nil { if err != nil {
return nil, nil, clues.Wrap(err, "retrieving parent folder") return resolvedPath{
idPath: nil,
locPath: nil,
cached: true,
deleted: false,
}, clues.Wrap(err, "retrieving parent container")
} }
fullPath := parentPath.Append(ptr.Val(c.GetId())) if !resolved.cached {
logger.Ctx(ctx).Debug("parent container was refreshed")
newContainer, shouldDelete, err := cr.refreshContainer(ctx, folderID)
if err != nil {
return resolvedPath{
idPath: nil,
locPath: nil,
cached: true,
deleted: false,
}, clues.Wrap(err, "refreshing container").WithClues(ctx)
}
if shouldDelete {
logger.Ctx(ctx).Debug("refreshing container showed it was deleted")
delete(cr.cache, folderID)
return resolvedPath{
idPath: nil,
locPath: nil,
cached: true,
deleted: true,
}, nil
}
// See if the newer version of the current container we got back has
// changed. If it has then it could be that the container was moved prior to
// deleting the parent and we just hit some eventual consistency case in
// Graph.
//
// TODO(ashmrtn): May want to bump the depth here just so we don't get stuck
// retrying too much if for some reason things keep moving around?
if ptr.Val(newContainer.GetParentFolderId()) != ptr.Val(c.GetParentFolderId()) ||
ptr.Val(newContainer.GetDisplayName()) != ptr.Val(c.GetDisplayName()) {
delete(cr.cache, folderID)
if err := cr.addFolder(newContainer); err != nil {
return resolvedPath{
idPath: nil,
locPath: nil,
cached: false,
deleted: false,
}, clues.Wrap(err, "updating cached container").WithClues(ctx)
}
return cr.idToPath(ctx, folderID, depth)
}
}
// If the parent wasn't found and refreshing the current container produced no
// diffs then delete the current container on the assumption that the parent
// was deleted and the current container will later get deleted via eventual
// consistency. If w're wrong then the container will get picked up again on
// the next backup.
if resolved.deleted {
logger.Ctx(ctx).Debug("deleting container since parent was deleted")
delete(cr.cache, folderID)
return resolvedPath{
idPath: nil,
locPath: nil,
cached: true,
deleted: true,
}, nil
}
fullPath := resolved.idPath.Append(ptr.Val(c.GetId()))
c.SetPath(fullPath) c.SetPath(fullPath)
locPath := parentLoc.Append(ptr.Val(c.GetDisplayName())) locPath := resolved.locPath.Append(ptr.Val(c.GetDisplayName()))
c.SetLocation(locPath) c.SetLocation(locPath)
return fullPath, locPath, nil return resolvedPath{
idPath: fullPath,
locPath: locPath,
cached: true,
deleted: false,
}, nil
} }
// PathInCache is a utility function to return m365ID of a folder if the // PathInCache is a utility function to return m365ID of a folder if the
@ -139,14 +331,14 @@ func (cr *containerResolver) LocationInCache(pathString string) (string, bool) {
// addFolder adds a folder to the cache with the given ID. If the item is // addFolder adds a folder to the cache with the given ID. If the item is
// already in the cache does nothing. The path for the item is not modified. // already in the cache does nothing. The path for the item is not modified.
func (cr *containerResolver) addFolder(cf graph.CacheFolder) error { func (cr *containerResolver) addFolder(cf graph.CachedContainer) error {
// Only require a non-nil non-empty parent if the path isn't already populated. // Only require a non-nil non-empty parent if the path isn't already populated.
if cf.Path() != nil { if cf.Path() != nil {
if err := checkIDAndName(cf.Container); err != nil { if err := checkIDAndName(cf); err != nil {
return clues.Wrap(err, "adding item to cache") return clues.Wrap(err, "adding item to cache")
} }
} else { } else {
if err := checkRequiredValues(cf.Container); err != nil { if err := checkRequiredValues(cf); err != nil {
return clues.Wrap(err, "adding item to cache") return clues.Wrap(err, "adding item to cache")
} }
} }
@ -155,7 +347,7 @@ func (cr *containerResolver) addFolder(cf graph.CacheFolder) error {
return nil return nil
} }
cr.cache[ptr.Val(cf.GetId())] = &cf cr.cache[ptr.Val(cf.GetId())] = cf
return nil return nil
} }
@ -176,7 +368,7 @@ func (cr *containerResolver) AddToCache(
ctx context.Context, ctx context.Context,
f graph.Container, f graph.Container,
) error { ) error {
temp := graph.CacheFolder{ temp := &graph.CacheFolder{
Container: f, Container: f,
} }
if err := cr.addFolder(temp); err != nil { if err := cr.addFolder(temp); err != nil {
@ -185,7 +377,7 @@ func (cr *containerResolver) AddToCache(
// Populate the path for this entry so calls to PathInCache succeed no matter // Populate the path for this entry so calls to PathInCache succeed no matter
// when they're made. // when they're made.
_, _, err := cr.IDToPath(ctx, ptr.Val(f.GetId())) _, err := cr.idToPath(ctx, ptr.Val(f.GetId()), 0)
if err != nil { if err != nil {
return clues.Wrap(err, "adding cache entry") return clues.Wrap(err, "adding cache entry")
} }
@ -208,7 +400,7 @@ func (cr *containerResolver) populatePaths(
return el.Failure() return el.Failure()
} }
_, _, err := cr.IDToPath(ctx, ptr.Val(f.GetId())) _, err := cr.idToPath(ctx, ptr.Val(f.GetId()), 0)
if err != nil { if err != nil {
err = clues.Wrap(err, "populating path") err = clues.Wrap(err, "populating path")
el.AddRecoverable(err) el.AddRecoverable(err)

View File

@ -1,6 +1,7 @@
package exchange package exchange
import ( import (
"context"
"fmt" "fmt"
stdpath "path" stdpath "path"
"testing" "testing"
@ -232,8 +233,8 @@ func (suite *FolderCacheUnitSuite) TestAddFolder() {
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
fc := newContainerResolver() fc := newContainerResolver(nil)
err := fc.addFolder(test.cf) err := fc.addFolder(&test.cf)
test.check(suite.T(), err, clues.ToCore(err)) test.check(suite.T(), err, clues.ToCore(err))
}) })
} }
@ -293,7 +294,7 @@ func resolverWithContainers(numContainers int, useIDInPath bool) (*containerReso
containers[i].expectedLocation = stdpath.Join(containers[i-1].expectedLocation, dn) containers[i].expectedLocation = stdpath.Join(containers[i-1].expectedLocation, dn)
} }
resolver := newContainerResolver() resolver := newContainerResolver(nil)
for _, c := range containers { for _, c := range containers {
resolver.cache[c.id] = c resolver.cache[c.id] = c
@ -302,6 +303,37 @@ func resolverWithContainers(numContainers int, useIDInPath bool) (*containerReso
return resolver, containers return resolver, containers
} }
// ---------------------------------------------------------------------------
// mock container refresher
// ---------------------------------------------------------------------------
type refreshResult struct {
err error
c graph.CachedContainer
}
type mockContainerRefresher struct {
// Folder ID -> result
entries map[string]refreshResult
}
func (r mockContainerRefresher) refreshContainer(
ctx context.Context,
id string,
) (graph.CachedContainer, error) {
rr, ok := r.entries[id]
if !ok {
// May not be this precise error, but it's easy to get a handle on.
return nil, graph.ErrDeletedInFlight
}
if rr.err != nil {
return nil, rr.err
}
return rr.c, nil
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// configured unit suite // configured unit suite
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -326,6 +358,160 @@ func TestConfiguredFolderCacheUnitSuite(t *testing.T) {
suite.Run(t, &ConfiguredFolderCacheUnitSuite{Suite: tester.NewUnitSuite(t)}) suite.Run(t, &ConfiguredFolderCacheUnitSuite{Suite: tester.NewUnitSuite(t)})
} }
func (suite *ConfiguredFolderCacheUnitSuite) TestRefreshContainer_RefreshParent() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
resolver, containers := resolverWithContainers(4, true)
almostLast := containers[len(containers)-2]
last := containers[len(containers)-1]
refresher := mockContainerRefresher{
entries: map[string]refreshResult{
almostLast.id: {c: almostLast},
last.id: {c: last},
},
}
resolver.refresher = refresher
delete(resolver.cache, almostLast.id)
ferrs := fault.New(true)
err := resolver.populatePaths(ctx, ferrs)
require.NoError(t, err, "populating paths", clues.ToCore(err))
p, l, err := resolver.IDToPath(ctx, last.id)
require.NoError(t, err, "getting paths", clues.ToCore(err))
assert.Equal(t, last.expectedPath, p.String())
assert.Equal(t, last.expectedLocation, l.String())
}
func (suite *ConfiguredFolderCacheUnitSuite) TestRefreshContainer_RefreshParent_NotFoundDeletes() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
resolver, containers := resolverWithContainers(4, true)
almostLast := containers[len(containers)-2]
last := containers[len(containers)-1]
refresher := mockContainerRefresher{
entries: map[string]refreshResult{
last.id: {c: last},
},
}
resolver.refresher = refresher
delete(resolver.cache, almostLast.id)
ferrs := fault.New(true)
err := resolver.populatePaths(ctx, ferrs)
require.NoError(t, err, "populating paths", clues.ToCore(err))
_, _, err = resolver.IDToPath(ctx, last.id)
assert.Error(t, err, "getting paths", clues.ToCore(err))
}
func (suite *ConfiguredFolderCacheUnitSuite) TestRefreshContainer_RefreshAncestor_NotFoundDeletes() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
resolver, containers := resolverWithContainers(4, true)
gone := containers[0]
child := containers[1]
last := containers[len(containers)-1]
refresher := mockContainerRefresher{
entries: map[string]refreshResult{
child.id: {c: child},
},
}
resolver.refresher = refresher
delete(resolver.cache, gone.id)
ferrs := fault.New(true)
err := resolver.populatePaths(ctx, ferrs)
require.NoError(t, err, "populating paths", clues.ToCore(err))
_, _, err = resolver.IDToPath(ctx, last.id)
assert.Error(t, err, "getting paths", clues.ToCore(err))
}
func (suite *ConfiguredFolderCacheUnitSuite) TestRefreshContainer_RefreshAncestor_NewParent() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
resolver, containers := resolverWithContainers(4, true)
other := containers[len(containers)-3]
gone := containers[len(containers)-2]
last := containers[len(containers)-1]
expected := *last
expected.parentID = other.id
expected.expectedPath = stdpath.Join(other.expectedPath, expected.id)
expected.expectedLocation = stdpath.Join(other.expectedLocation, expected.displayName)
refresher := mockContainerRefresher{
entries: map[string]refreshResult{
last.id: {c: &expected},
},
}
resolver.refresher = refresher
delete(resolver.cache, gone.id)
ferrs := fault.New(true)
err := resolver.populatePaths(ctx, ferrs)
require.NoError(t, err, "populating paths", clues.ToCore(err))
p, l, err := resolver.IDToPath(ctx, last.id)
require.NoError(t, err, "getting paths", clues.ToCore(err))
assert.Equal(t, expected.expectedPath, p.String())
assert.Equal(t, expected.expectedLocation, l.String())
}
func (suite *ConfiguredFolderCacheUnitSuite) TestRefreshContainer_RefreshFolder_FolderDeleted() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
resolver, containers := resolverWithContainers(4, true)
parent := containers[len(containers)-2]
last := containers[len(containers)-1]
refresher := mockContainerRefresher{
entries: map[string]refreshResult{
parent.id: {c: parent},
},
}
resolver.refresher = refresher
delete(resolver.cache, parent.id)
ferrs := fault.New(true)
err := resolver.populatePaths(ctx, ferrs)
require.NoError(t, err, "populating paths", clues.ToCore(err))
_, _, err = resolver.IDToPath(ctx, last.id)
assert.Error(t, err, "getting paths", clues.ToCore(err))
}
func (suite *ConfiguredFolderCacheUnitSuite) TestDepthLimit() { func (suite *ConfiguredFolderCacheUnitSuite) TestDepthLimit() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
@ -350,7 +536,7 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestDepthLimit() {
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
resolver, containers := resolverWithContainers(test.numContainers, false) resolver, containers := resolverWithContainers(test.numContainers, false)
_, _, err := resolver.IDToPath(ctx, containers[len(containers)-1].id) _, err := resolver.idToPath(ctx, containers[len(containers)-1].id, 0)
test.check(suite.T(), err, clues.ToCore(err)) test.check(suite.T(), err, clues.ToCore(err))
}) })
} }
@ -384,6 +570,9 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
err := suite.fc.populatePaths(ctx, fault.New(true))
require.NoError(suite.T(), err, clues.ToCore(err))
for _, c := range suite.allContainers { for _, c := range suite.allContainers {
suite.Run(ptr.Val(c.GetDisplayName()), func() { suite.Run(ptr.Val(c.GetDisplayName()), func() {
t := suite.T() t := suite.T()
@ -396,10 +585,14 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached
} }
} }
// TODO(ashmrtn): Remove this since the same cache can do IDs or locations.
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached_useID() { func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached_useID() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
err := suite.fcWithID.populatePaths(ctx, fault.New(true))
require.NoError(suite.T(), err, clues.ToCore(err))
for _, c := range suite.containersWithID { for _, c := range suite.containersWithID {
suite.Run(ptr.Val(c.GetDisplayName()), func() { suite.Run(ptr.Val(c.GetDisplayName()), func() {
t := suite.T() t := suite.T()
@ -419,6 +612,9 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths()
t := suite.T() t := suite.T()
c := suite.allContainers[len(suite.allContainers)-1] c := suite.allContainers[len(suite.allContainers)-1]
err := suite.fc.populatePaths(ctx, fault.New(true))
require.NoError(t, err, clues.ToCore(err))
p, l, err := suite.fc.IDToPath(ctx, c.id) p, l, err := suite.fc.IDToPath(ctx, c.id)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, c.expectedPath, p.String()) assert.Equal(t, c.expectedPath, p.String())
@ -432,6 +628,7 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths()
assert.Equal(t, c.expectedLocation, l.String()) assert.Equal(t, c.expectedLocation, l.String())
} }
// TODO(ashmrtn): Remove this since the same cache can do IDs or locations.
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths_useID() { func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths_useID() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
@ -439,6 +636,9 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths_u
t := suite.T() t := suite.T()
c := suite.containersWithID[len(suite.containersWithID)-1] c := suite.containersWithID[len(suite.containersWithID)-1]
err := suite.fcWithID.populatePaths(ctx, fault.New(true))
require.NoError(t, err, clues.ToCore(err))
p, l, err := suite.fcWithID.IDToPath(ctx, c.id) p, l, err := suite.fcWithID.IDToPath(ctx, c.id)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, c.expectedPath, p.String()) assert.Equal(t, c.expectedPath, p.String())
@ -457,12 +657,21 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderErrorsParentN
defer flush() defer flush()
t := suite.T() t := suite.T()
last := suite.allContainers[len(suite.allContainers)-1]
almostLast := suite.allContainers[len(suite.allContainers)-2] almostLast := suite.allContainers[len(suite.allContainers)-2]
delete(suite.fc.cache, almostLast.id) delete(suite.fc.cache, almostLast.id)
_, _, err := suite.fc.IDToPath(ctx, last.id) err := suite.fc.populatePaths(ctx, fault.New(true))
assert.Error(t, err, clues.ToCore(err))
}
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolder_Errors_PathsNotBuilt() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
_, _, err := suite.fc.IDToPath(ctx, suite.allContainers[len(suite.allContainers)-1].id)
assert.Error(t, err, clues.ToCore(err)) assert.Error(t, err, clues.ToCore(err))
} }

View File

@ -597,7 +597,7 @@ func (suite *DataCollectionsIntegrationSuite) TestEventsSerializationRegression(
bdayID string bdayID string
) )
fn := func(gcf graph.CacheFolder) error { fn := func(gcf graph.CachedContainer) error {
if ptr.Val(gcf.GetDisplayName()) == DefaultCalendar { if ptr.Val(gcf.GetDisplayName()) == DefaultCalendar {
calID = ptr.Val(gcf.GetId()) calID = ptr.Val(gcf.GetId())
} }

View File

@ -27,7 +27,7 @@ func (ecc *eventCalendarCache) init(
ctx context.Context, ctx context.Context,
) error { ) error {
if ecc.containerResolver == nil { if ecc.containerResolver == nil {
ecc.containerResolver = newContainerResolver() ecc.containerResolver = newContainerResolver(nil)
} }
return ecc.populateEventRoot(ctx) return ecc.populateEventRoot(ctx)
@ -49,7 +49,7 @@ func (ecc *eventCalendarCache) populateEventRoot(ctx context.Context) error {
f, f,
path.Builder{}.Append(ptr.Val(f.GetId())), // storage path path.Builder{}.Append(ptr.Val(f.GetId())), // storage path
path.Builder{}.Append(ptr.Val(f.GetDisplayName()))) // display location path.Builder{}.Append(ptr.Val(f.GetDisplayName()))) // display location
if err := ecc.addFolder(temp); err != nil { if err := ecc.addFolder(&temp); err != nil {
return clues.Wrap(err, "initializing calendar resolver").WithClues(ctx) return clues.Wrap(err, "initializing calendar resolver").WithClues(ctx)
} }
@ -98,7 +98,7 @@ func (ecc *eventCalendarCache) AddToCache(ctx context.Context, f graph.Container
path.Builder{}.Append(ptr.Val(f.GetId())), // storage path path.Builder{}.Append(ptr.Val(f.GetId())), // storage path
path.Builder{}.Append(ptr.Val(f.GetDisplayName()))) // display location path.Builder{}.Append(ptr.Val(f.GetDisplayName()))) // display location
if err := ecc.addFolder(temp); err != nil { if err := ecc.addFolder(&temp); err != nil {
return clues.Wrap(err, "adding container").WithClues(ctx) return clues.Wrap(err, "adding container").WithClues(ctx)
} }

View File

@ -10,7 +10,29 @@ import (
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
var _ graph.ContainerResolver = &mailFolderCache{} var (
_ graph.ContainerResolver = &mailFolderCache{}
_ containerRefresher = &mailRefresher{}
)
type mailRefresher struct {
getter containerGetter
userID string
}
func (r *mailRefresher) refreshContainer(
ctx context.Context,
id string,
) (graph.CachedContainer, error) {
c, err := r.getter.GetContainerByID(ctx, r.userID, id)
if err != nil {
return nil, clues.Stack(err)
}
f := graph.NewCacheFolder(c, nil, nil)
return &f, nil
}
// mailFolderCache struct used to improve lookup of directories within exchange.Mail // mailFolderCache struct used to improve lookup of directories within exchange.Mail
// cache map of cachedContainers where the key = M365ID // cache map of cachedContainers where the key = M365ID
@ -29,7 +51,10 @@ func (mc *mailFolderCache) init(
ctx context.Context, ctx context.Context,
) error { ) error {
if mc.containerResolver == nil { if mc.containerResolver == nil {
mc.containerResolver = newContainerResolver() mc.containerResolver = newContainerResolver(&mailRefresher{
userID: mc.userID,
getter: mc.getter,
})
} }
return mc.populateMailRoot(ctx) return mc.populateMailRoot(ctx)
@ -52,7 +77,7 @@ func (mc *mailFolderCache) populateMailRoot(ctx context.Context) error {
// the user doesn't see in the regular UI for Exchange. // the user doesn't see in the regular UI for Exchange.
path.Builder{}.Append(), // path of IDs path.Builder{}.Append(), // path of IDs
path.Builder{}.Append()) // display location path.Builder{}.Append()) // display location
if err := mc.addFolder(temp); err != nil { if err := mc.addFolder(&temp); err != nil {
return clues.Wrap(err, "adding resolver dir").WithClues(ctx) return clues.Wrap(err, "adding resolver dir").WithClues(ctx)
} }

View File

@ -118,7 +118,7 @@ func (c Contacts) GetContainerByID(
func (c Contacts) EnumerateContainers( func (c Contacts) EnumerateContainers(
ctx context.Context, ctx context.Context,
userID, baseDirID string, userID, baseDirID string,
fn func(graph.CacheFolder) error, fn func(graph.CachedContainer) error,
errs *fault.Bus, errs *fault.Bus,
) error { ) error {
service, err := c.Service() service, err := c.Service()
@ -166,7 +166,7 @@ func (c Contacts) EnumerateContainers(
"container_display_name", ptr.Val(fold.GetDisplayName())) "container_display_name", ptr.Val(fold.GetDisplayName()))
temp := graph.NewCacheFolder(fold, nil, nil) temp := graph.NewCacheFolder(fold, nil, nil)
if err := fn(temp); err != nil { if err := fn(&temp); err != nil {
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
continue continue
} }

View File

@ -192,7 +192,7 @@ func (c Events) GetItem(
func (c Events) EnumerateContainers( func (c Events) EnumerateContainers(
ctx context.Context, ctx context.Context,
userID, baseDirID string, userID, baseDirID string,
fn func(graph.CacheFolder) error, fn func(graph.CachedContainer) error,
errs *fault.Bus, errs *fault.Bus,
) error { ) error {
service, err := c.Service() service, err := c.Service()
@ -239,7 +239,7 @@ func (c Events) EnumerateContainers(
cd, cd,
path.Builder{}.Append(ptr.Val(cd.GetId())), // storage path path.Builder{}.Append(ptr.Val(cd.GetId())), // storage path
path.Builder{}.Append(ptr.Val(cd.GetDisplayName()))) // display location path.Builder{}.Append(ptr.Val(cd.GetDisplayName()))) // display location
if err := fn(temp); err != nil { if err := fn(&temp); err != nil {
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
continue continue
} }

View File

@ -309,7 +309,7 @@ func (p *mailFolderPager) valuesIn(pl api.PageLinker) ([]models.MailFolderable,
func (c Mail) EnumerateContainers( func (c Mail) EnumerateContainers(
ctx context.Context, ctx context.Context,
userID, baseDirID string, userID, baseDirID string,
fn func(graph.CacheFolder) error, fn func(graph.CachedContainer) error,
errs *fault.Bus, errs *fault.Bus,
) error { ) error {
service, err := c.Service() service, err := c.Service()
@ -347,7 +347,7 @@ func (c Mail) EnumerateContainers(
"container_name", ptr.Val(v.GetDisplayName())) "container_name", ptr.Val(v.GetDisplayName()))
temp := graph.NewCacheFolder(v, nil, nil) temp := graph.NewCacheFolder(v, nil, nil)
if err := fn(temp); err != nil { if err := fn(&temp); err != nil {
errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation)) errs.AddRecoverable(graph.Stack(fctx, err).Label(fault.LabelForceNoBackupCreation))
continue continue
} }