Add a fallback to non-delta pager during event item enumeration (#5201)

If the delta pager with a smaller page size also falls then attempt to
use the regular events endpoint to enumerate items

---

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

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

#### Type of change

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

#### Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2024-02-09 16:45:43 -08:00 committed by GitHub
parent 45886e2ad9
commit 71a9087e4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 217 additions and 61 deletions

View File

@ -305,10 +305,11 @@ func (c Events) GetAddedAndRemovedItemIDs(
effectivePageSize := minEventsDeltaPageSize effectivePageSize := minEventsDeltaPageSize
logger.Ctx(ctx).Infow( logger.Ctx(ctx).Infow(
"retrying event item query with reduced page size", "retrying list event item query with reduced page size",
"delta_pager_effective_page_size", effectivePageSize, "delta_pager_effective_page_size", effectivePageSize,
"delta_pager_default_page_size", c.options.DeltaPageSize) "delta_pager_default_page_size", c.options.DeltaPageSize)
// Get new pagers just to make sure we don't have partial state in them.
deltaPager = c.newEventsDeltaPagerWithPageSize( deltaPager = c.newEventsDeltaPagerWithPageSize(
ctx, ctx,
userID, userID,
@ -316,8 +317,12 @@ func (c Events) GetAddedAndRemovedItemIDs(
prevDeltaLink, prevDeltaLink,
effectivePageSize, effectivePageSize,
idAnd()...) idAnd()...)
pager = c.NewEventsPager(
userID,
containerID,
idAnd(lastModifiedDateTime)...)
return pagers.GetAddedAndRemovedItemIDs[models.Eventable]( addedRemoved, err = pagers.GetAddedAndRemovedItemIDs[models.Eventable](
ctx, ctx,
pager, pager,
deltaPager, deltaPager,
@ -325,4 +330,36 @@ func (c Events) GetAddedAndRemovedItemIDs(
config.CanMakeDeltaQueries, config.CanMakeDeltaQueries,
config.LimitResults, config.LimitResults,
pagers.AddedAndRemovedByAddtlData[models.Eventable]) pagers.AddedAndRemovedByAddtlData[models.Eventable])
if err == nil || !errors.Is(err, graph.ErrServiceUnavailableEmptyResp) {
return addedRemoved, clues.Stack(err).OrNil()
}
logger.Ctx(ctx).Infow(
"retrying list event item query with non-delta pager",
"effective_page_size", maxNonDeltaPageSize)
// Attempt yet another fallback by using the regular pager if we still can't
// get data with a smaller page size. Get new pagers just to make sure we
// don't have partial state in them.
deltaPager = c.newEventsDeltaPagerWithPageSize(
ctx,
userID,
containerID,
prevDeltaLink,
effectivePageSize,
idAnd()...)
pager = c.NewEventsPager(
userID,
containerID,
idAnd(lastModifiedDateTime)...)
return pagers.GetAddedAndRemovedItemIDs[models.Eventable](
ctx,
pager,
deltaPager,
prevDeltaLink,
// Disable delta queries.
false,
config.LimitResults,
pagers.AddedAndRemovedByAddtlData[models.Eventable])
} }

View File

@ -34,12 +34,22 @@ func TestEventsPagerUnitSuite(t *testing.T) {
}) })
} }
func (suite *EventsPagerUnitSuite) TestGetAddedAndRemovedItemIDs_SendsCorrectDeltaPageSize() { func (suite *EventsPagerUnitSuite) TestEventsList() {
const ( const (
validEmptyResponse = `{ nextLinkPath = "/next-link"
nextLinkURL = graphAPIHostURL + nextLinkPath
nextDeltaURL = graphAPIHostURL + "/next-delta"
validEventsListSingleNextLinkResponse = `{
"@odata.context": "https://graph.microsoft.com/beta/$metadata#Collection(event)",
"value": [{"id":"foo"}],
"@odata.nextLink": "` + nextLinkURL + `"
}`
validEventsListEmptyResponse = `{
"@odata.context": "https://graph.microsoft.com/beta/$metadata#Collection(event)", "@odata.context": "https://graph.microsoft.com/beta/$metadata#Collection(event)",
"value": [], "value": [],
"@odata.deltaLink": "link" "@odata.deltaLink": "` + nextDeltaURL + `"
}` }`
// deltaPath helps make gock matching a little easier since it splits out // deltaPath helps make gock matching a little easier since it splits out
@ -52,26 +62,171 @@ func (suite *EventsPagerUnitSuite) TestGetAddedAndRemovedItemIDs_SendsCorrectDel
containerID = "container-id" containerID = "container-id"
) )
pageSizeMatcher := func(
t *testing.T,
expectedPageSize int32,
) func(*http.Request, *gock.Request) (bool, error) {
return func(got *http.Request, want *gock.Request) (bool, error) {
var (
found bool
preferHeaders = got.Header.Values("Prefer")
expected = fmt.Sprintf(
"odata.maxpagesize=%d",
expectedPageSize)
)
for _, header := range preferHeaders {
if strings.Contains(header, expected) {
found = true
break
}
}
assert.Truef(
t,
found,
"header %s not found in set %v",
expected,
preferHeaders)
return true, nil
}
}
// configureFailedRequests configures the HTTP mocker to return one successful
// request with a single item and then enough failed requests to exhaust the
// retries. This allows testing that results are not mixed between fallbacks.
configureFailedRequests := func(
t *testing.T,
reqPath string,
expectedPageSize int32,
) {
gock.New(graphAPIHostURL).
Get(reqPath).
AddMatcher(pageSizeMatcher(t, expectedPageSize)).
Reply(http.StatusOK).
JSON(validEventsListSingleNextLinkResponse)
// Number of retries and delay between retries is handled by a kiota
// middleware. We can change the default config parameters when setting
// up the mock in a later PR.
gock.New(graphAPIHostURL).
Get(nextLinkPath).
AddMatcher(pageSizeMatcher(t, expectedPageSize)).
Times(4).
Reply(http.StatusServiceUnavailable).
BodyString("").
Type("text/plain")
}
deltaTests := []struct { deltaTests := []struct {
name string name string
reqPath string
inputDelta string inputDelta string
configureMocks func(t *testing.T, userID string, containerID string)
expectNextDeltaURL string
expectDeltaReset assert.BoolAssertionFunc
}{ }{
{ {
name: "NoPrevDelta", name: "NoPrevDelta",
reqPath: stdpath.Join( configureMocks: func(t *testing.T, userID string, containerID string) {
reqPath := stdpath.Join(
"/beta", "/beta",
"users", "users",
userID, userID,
"calendars", "calendars",
containerID, containerID,
"events", "events",
"delta"), "delta")
gock.New(graphAPIHostURL).
Get(reqPath).
SetMatcher(gock.NewMatcher()).
// Need a custom Matcher since the prefer header is also used for
// immutable ID behavior.
AddMatcher(pageSizeMatcher(t, maxDeltaPageSize)).
Reply(http.StatusOK).
JSON(validEventsListEmptyResponse)
},
expectNextDeltaURL: nextDeltaURL,
// OK to be true for this since we didn't have a delta to start with.
expectDeltaReset: assert.True,
}, },
{ {
name: "HasPrevDelta", name: "NoPrevDelta DeltaFallback",
reqPath: deltaPath, configureMocks: func(t *testing.T, userID string, containerID string) {
reqPath := stdpath.Join(
"/beta",
"users",
userID,
"calendars",
containerID,
"events",
"delta")
configureFailedRequests(t, reqPath, maxDeltaPageSize)
gock.New(graphAPIHostURL).
Get(reqPath).
SetMatcher(gock.NewMatcher()).
// Need a custom Matcher since the prefer header is also used for
// immutable ID behavior.
AddMatcher(pageSizeMatcher(t, minEventsDeltaPageSize)).
Reply(http.StatusOK).
JSON(validEventsListEmptyResponse)
},
expectNextDeltaURL: nextDeltaURL,
// OK to be true for this since we didn't have a delta to start with.
expectDeltaReset: assert.True,
},
{
name: "PrevDelta DeltaFallback",
inputDelta: prevDelta, inputDelta: prevDelta,
configureMocks: func(t *testing.T, userID string, containerID string) {
// Number of retries and delay between retries is handled by a kiota
// middleware. We can change the default config parameters when setting
// up the mock in a later PR.
configureFailedRequests(t, deltaPath, maxDeltaPageSize)
gock.New(graphAPIHostURL).
Get(deltaPath).
SetMatcher(gock.NewMatcher()).
// Need a custom Matcher since the prefer header is also used for
// immutable ID behavior.
AddMatcher(pageSizeMatcher(t, minEventsDeltaPageSize)).
Reply(http.StatusOK).
JSON(validEventsListEmptyResponse)
},
expectNextDeltaURL: nextDeltaURL,
expectDeltaReset: assert.False,
},
{
name: "PrevDelta SecondaryNonDeltaFallback",
inputDelta: prevDelta,
configureMocks: func(t *testing.T, userID string, containerID string) {
// Number of retries and delay between retries is handled by a kiota
// middleware. We can change the default config parameters when setting
// up the mock in a later PR.
configureFailedRequests(t, deltaPath, maxDeltaPageSize)
// Smaller page size delta fallback.
configureFailedRequests(t, deltaPath, minEventsDeltaPageSize)
// Non delta endpoint fallback
gock.New(graphAPIHostURL).
Get(v1APIURLPath(
"users",
userID,
"calendars",
containerID,
"events")).
SetMatcher(gock.NewMatcher()).
// Need a custom Matcher since the prefer header is also used for
// immutable ID behavior.
AddMatcher(pageSizeMatcher(t, maxNonDeltaPageSize)).
Reply(http.StatusOK).
JSON(validEventsListEmptyResponse)
},
expectDeltaReset: assert.True,
}, },
} }
@ -91,50 +246,9 @@ func (suite *EventsPagerUnitSuite) TestGetAddedAndRemovedItemIDs_SendsCorrectDel
t.Cleanup(gock.Off) t.Cleanup(gock.Off)
// Number of retries and delay between retries is handled by a kiota deltaTest.configureMocks(t, userID, containerID)
// middleware. We can change the default config parameters when setting up
// the mock in a later PR.
gock.New(graphAPIHostURL).
Get(deltaTest.reqPath).
Times(4).
Reply(http.StatusServiceUnavailable).
BodyString("").
Type("text/plain")
gock.New(graphAPIHostURL). res, err := client.Events().GetAddedAndRemovedItemIDs(
Get(deltaTest.reqPath).
SetMatcher(gock.NewMatcher()).
// Need a custom Matcher since the prefer header is also used for
// immutable ID behavior.
AddMatcher(func(got *http.Request, want *gock.Request) (bool, error) {
var (
found bool
preferHeaders = got.Header.Values("Prefer")
expected = fmt.Sprintf(
"odata.maxpagesize=%d",
minEventsDeltaPageSize)
)
for _, header := range preferHeaders {
if strings.Contains(header, expected) {
found = true
break
}
}
assert.Truef(
t,
found,
"header %s not found in set %v",
expected,
preferHeaders)
return true, nil
}).
Reply(http.StatusOK).
JSON(validEmptyResponse)
_, err = client.Events().GetAddedAndRemovedItemIDs(
ctx, ctx,
userID, userID,
containerID, containerID,
@ -142,7 +256,12 @@ func (suite *EventsPagerUnitSuite) TestGetAddedAndRemovedItemIDs_SendsCorrectDel
CallConfig{ CallConfig{
CanMakeDeltaQueries: true, CanMakeDeltaQueries: true,
}) })
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
assert.Empty(t, res.Added, "added items")
assert.Empty(t, res.Removed, "removed items")
assert.Equal(t, deltaTest.expectNextDeltaURL, res.DU.URL, "next delta URL")
deltaTest.expectDeltaReset(t, res.DU.Reset, "delta reset")
}) })
} }
} }