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:
parent
45886e2ad9
commit
71a9087e4d
@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user