Skip to content

Commit 66f67e2

Browse files
Fixed batch calls with more than 20 requests, added retry logic (#472)
* fixed batch calls with more than 20 requests, and added retry logic for throttling * updated retry-after parsing Co-authored-by: Shane Weaver <[email protected]>
1 parent 2c896f4 commit 66f67e2

File tree

6 files changed

+195
-53
lines changed

6 files changed

+195
-53
lines changed

src/components/mgt-login/mgt-login.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { MgtFlyout } from '../sub-components/mgt-flyout/mgt-flyout';
1414
import { MgtTemplatedComponent } from '../templatedComponent';
1515
import { styles } from './mgt-login-css';
1616

17+
import { getUserWithPhoto } from '../../graph/graph.user';
1718
import '../../styles/fabric-icon-font';
1819
import '../mgt-person/mgt-person';
1920
import { PersonViewType } from '../mgt-person/mgt-person';
@@ -172,13 +173,11 @@ export class MgtLogin extends MgtTemplatedComponent {
172173
const provider = Providers.globalProvider;
173174
if (provider && !this.userDetails) {
174175
if (provider.state === ProviderState.SignedIn) {
175-
const batch = provider.graph.forComponent(this).createBatch();
176-
batch.get('me', 'me', ['user.read']);
177-
batch.get('photo', 'me/photo/$value', ['user.read']);
178-
const response = await batch.execute();
176+
this.userDetails = await getUserWithPhoto(provider.graph.forComponent(this));
179177

180-
this._image = response.photo;
181-
this.userDetails = response.me;
178+
if (this.userDetails.personImage) {
179+
this._image = this.userDetails.personImage;
180+
}
182181
} else {
183182
this.userDetails = null;
184183
}

src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function getAllMyTeams(graph: IGraph): Promise<Team[]> {
1919
const teams = await graph
2020
.api('/me/joinedTeams')
2121
.middlewareOptions(prepScopes('User.Read.All'))
22-
.select(['displayName', 'id'])
22+
.select(['displayName', 'id', 'isArchived'])
2323
.get();
2424
return teams ? teams.value : null;
2525
}

src/components/mgt-teams-channel-picker/mgt-teams-channel-picker.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
535535
}
536536

537537
/**
538-
* Quieries Microsoft Graph for Teams & respective channels then sets to items list
538+
* Queries Microsoft Graph for Teams & respective channels then sets to items list
539539
*
540540
* @protected
541541
* @memberof MgtTeamsChannelPicker
@@ -547,27 +547,34 @@ export class MgtTeamsChannelPicker extends MgtTemplatedComponent {
547547
const graph = provider.graph.forComponent(this);
548548

549549
teams = await getAllMyTeams(graph);
550+
teams = teams.filter(t => !t.isArchived);
550551

551552
const batch = provider.graph.createBatch();
552553

553-
for (const [i, team] of teams.entries()) {
554-
batch.get(`${i}`, `teams/${team.id}/channels`, ['group.read.all']);
554+
for (const team of teams) {
555+
batch.get(team.id, `teams/${team.id}/channels`, ['group.read.all']);
555556
}
556-
const response = await batch.execute();
557-
this.items = teams.map(t => {
558-
return {
559-
item: t
560-
};
561-
});
562-
for (const [i] of teams.entries()) {
563-
if (response[i] && response[i].value) {
564-
this.items[i].channels = response[i].value.map(c => {
557+
558+
const responses = await batch.executeAll();
559+
560+
for (const team of teams) {
561+
const response = responses.get(team.id);
562+
563+
if (response && response.content && response.content.value) {
564+
team.channels = response.content.value.map(c => {
565565
return {
566566
item: c
567567
};
568568
});
569569
}
570570
}
571+
572+
this.items = teams.map(t => {
573+
return {
574+
channels: t.channels as DropdownItem[],
575+
item: t
576+
};
577+
});
571578
}
572579
this.filterList();
573580
this.resetFocusState();

src/graph/graph.presence.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,13 @@ export async function getUsersPresenceByPeople(graph: IGraph, people?: IDynamicP
6666
}
6767

6868
try {
69-
const peoplePresence = await batch.execute();
69+
const peoplePresence = {};
70+
const response = await batch.executeAll();
71+
72+
for (const r of response.values()) {
73+
peoplePresence[r.id] = r.content;
74+
}
75+
7076
return peoplePresence;
7177
} catch (_) {
7278
try {

src/graph/graph.user.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,15 @@ export async function getUserWithPhoto(graph: IGraph, userId?: string): Promise<
4343
batch.get('user', 'me', ['user.read']);
4444
batch.get('photo', 'me/photo/$value', ['user.read']);
4545
}
46-
const response = await batch.execute();
46+
const response = await batch.executeAll();
4747

48-
if (response && response.user) {
49-
person = response.user;
50-
person.personImage = response.photo;
48+
const photoResponse = response.get('photo');
49+
const userDetailsResponse = response.get('user');
50+
51+
if (userDetailsResponse.content) {
52+
person = userDetailsResponse.content;
53+
54+
person.personImage = photoResponse.content;
5155
}
5256

5357
return person;
@@ -90,14 +94,14 @@ export async function getUsersForUserIds(graph: IGraph, userIds: string[]): Prom
9094
}
9195

9296
try {
93-
const response = await batch.execute();
97+
const responses = await batch.executeAll();
9498
const people = [];
9599

96100
// iterate over userIds to ensure the order of ids
97101
for (const id of userIds) {
98-
const person = response[id];
99-
if (person && person.id) {
100-
people.push(person);
102+
const response = responses.get(id);
103+
if (response && response.content) {
104+
people.push(response.content);
101105
}
102106
}
103107

@@ -135,12 +139,12 @@ export async function getUsersForPeopleQueries(graph: IGraph, peopleQueries: str
135139
}
136140

137141
try {
138-
const response = await batch.execute();
142+
const responses = await batch.executeAll();
139143

140144
for (const personQuery of peopleQueries) {
141-
const person = response[personQuery];
142-
if (person && person.value && person.value.length) {
143-
people.push(person.value[0]);
145+
const response = responses.get(personQuery);
146+
if (response && response.content && response.content.value && response.content.value.length > 0) {
147+
people.push(response.content.value[0]);
144148
}
145149
}
146150

src/utils/Batch.ts

Lines changed: 147 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import { BatchRequestContent, MiddlewareOptions } from '@microsoft/microsoft-graph-client';
99
import { IGraph } from '../IGraph';
1010
import { prepScopes } from './GraphHelpers';
11+
import { delay } from './Utils';
1112

1213
/**
13-
* Method to reduce repetitive requests to the Graph utilized in Batch
14+
* Represents a request to be executed in a batch
1415
*
1516
* @class BatchRequest
1617
*/
@@ -21,6 +22,7 @@ export class BatchRequest {
2122
* @type {string}
2223
* @memberof BatchRequest
2324
*/
25+
2426
public resource: string;
2527
/**
2628
* method passed to be requested
@@ -29,15 +31,71 @@ export class BatchRequest {
2931
* @memberof BatchRequest
3032
*/
3133
public method: string;
32-
constructor(resource: string, method: string) {
34+
35+
/**
36+
* The index of the requests as it was added to the queue
37+
* Use this value if you need to sort the responses
38+
* in the order they were added
39+
*
40+
* @type {number}
41+
* @memberof BatchRequest
42+
*/
43+
public index: number;
44+
45+
/**
46+
* The id of the requests
47+
*
48+
* @type {string}
49+
* @memberof BatchRequest
50+
*/
51+
public id: string;
52+
53+
constructor(index, id, resource: string, method: string) {
3354
if (resource.charAt(0) !== '/') {
3455
resource = '/' + resource;
3556
}
3657
this.resource = resource;
3758
this.method = method;
59+
this.index = index;
60+
this.id = id;
3861
}
3962
}
4063

64+
/**
65+
* Represents a response from a request executed in a batch
66+
*
67+
* @export
68+
* @class BatchResponse
69+
*/
70+
// tslint:disable-next-line:max-classes-per-file
71+
export class BatchResponse {
72+
/**
73+
* The index of the requests as it was added to the queue
74+
* Use this value if you need to sort the responses
75+
* in the order they were added
76+
*
77+
* @type {number}
78+
* @memberof BatchResponse
79+
*/
80+
public index: number;
81+
82+
/**
83+
* The id of the requests
84+
*
85+
* @type {string}
86+
* @memberof BatchResponse
87+
*/
88+
public id: string;
89+
90+
/**
91+
* The content of the response
92+
*
93+
* @type {*}
94+
* @memberof BatchResponse
95+
*/
96+
public content: any;
97+
}
98+
4199
/**
42100
* Method to reduce repetitive requests to the Graph
43101
*
@@ -49,12 +107,33 @@ export class Batch {
49107
// this doesn't really mater what it is as long as it's a root base url
50108
// otherwise a Request assumes the current path and that could change the relative path
51109
private static baseUrl = 'https://graph.microsoft.com';
52-
private requests: Map<string, BatchRequest> = new Map<string, BatchRequest>();
53-
private scopes: string[] = [];
110+
111+
private allRequests: BatchRequest[];
112+
private requestsQueue: number[];
113+
private scopes: string[];
114+
private retryAfter: number;
115+
54116
private graph: IGraph;
55117

118+
private nextIndex: number;
119+
56120
constructor(graph: IGraph) {
57121
this.graph = graph;
122+
this.allRequests = [];
123+
this.requestsQueue = [];
124+
this.scopes = [];
125+
this.nextIndex = 0;
126+
this.retryAfter = 0;
127+
}
128+
129+
/**
130+
* Get whether there are requests that have not been executed
131+
*
132+
* @readonly
133+
* @memberof Batch
134+
*/
135+
public get hasRequests() {
136+
return this.requestsQueue.length > 0;
58137
}
59138

60139
/**
@@ -66,31 +145,44 @@ export class Batch {
66145
* @memberof Batch
67146
*/
68147
public get(id: string, resource: string, scopes?: string[]) {
69-
const request = new BatchRequest(resource, 'GET');
70-
this.requests.set(id, request);
148+
const index = this.nextIndex++;
149+
const request = new BatchRequest(index, id, resource, 'GET');
150+
this.allRequests.push(request);
151+
this.requestsQueue.push(index);
71152
if (scopes) {
72153
this.scopes = this.scopes.concat(scopes);
73154
}
74155
}
75156

76157
/**
77-
* Promise to handle Graph request response
158+
* Execute the next set of requests.
159+
* This will execute up to 20 requests at a time
78160
*
79-
* @returns {Promise<any>}
161+
* @returns {Promise<Map<string, BatchResponse>>}
80162
* @memberof Batch
81163
*/
82-
public async execute(): Promise<any> {
83-
const responses = {};
84-
if (!this.requests.size) {
164+
public async executeNext(): Promise<Map<string, BatchResponse>> {
165+
const responses: Map<string, BatchResponse> = new Map();
166+
167+
if (this.retryAfter) {
168+
await delay(this.retryAfter * 1000);
169+
this.retryAfter = 0;
170+
}
171+
172+
if (!this.hasRequests) {
85173
return responses;
86174
}
87175

176+
// batch can support up to 20 requests
177+
const nextBatch = this.requestsQueue.splice(0, 20);
178+
88179
const batchRequestContent = new BatchRequestContent();
89-
for (const request of this.requests) {
180+
181+
for (const request of nextBatch.map(i => this.allRequests[i])) {
90182
batchRequestContent.addRequest({
91-
id: request[0],
92-
request: new Request(Batch.baseUrl + request[1].resource, {
93-
method: request[1].method
183+
id: request.index.toString(),
184+
request: new Request(Batch.baseUrl + request.resource, {
185+
method: request.method
94186
})
95187
});
96188
}
@@ -101,13 +193,47 @@ export class Batch {
101193
const batchRequestBody = await batchRequestContent.getContent();
102194
const batchResponse = await batchRequest.post(batchRequestBody);
103195

104-
for (const response of batchResponse.responses) {
105-
if (response.status !== 200) {
106-
response[response.id] = null;
107-
} else if (response.headers['Content-Type'].includes('image/jpeg')) {
108-
responses[response.id] = 'data:image/jpeg;base64,' + response.body;
196+
for (const r of batchResponse.responses) {
197+
const response = new BatchResponse();
198+
const request = this.allRequests[r.id];
199+
200+
response.id = request.id;
201+
response.index = request.index;
202+
203+
if (r.status !== 200) {
204+
if (r.status === 429) {
205+
// this request was throttled
206+
// add request back to queue and set retry wait time
207+
this.requestsQueue.unshift(r.id);
208+
this.retryAfter = Math.max(this.retryAfter, parseInt(r.headers['Retry-After'], 10) || 1);
209+
}
210+
continue;
211+
} else if (r.headers['Content-Type'].includes('image/jpeg')) {
212+
response.content = 'data:image/jpeg;base64,' + r.body;
109213
} else {
110-
responses[response.id] = response.body;
214+
response.content = r.body;
215+
}
216+
217+
responses.set(request.id, response);
218+
}
219+
220+
return responses;
221+
}
222+
223+
/**
224+
* Execute all requests, up to 20 at a time until
225+
* all requests have been executed
226+
*
227+
* @returns {Promise<Map<string, BatchResponse>>}
228+
* @memberof Batch
229+
*/
230+
public async executeAll(): Promise<Map<string, BatchResponse>> {
231+
const responses: Map<string, BatchResponse> = new Map();
232+
233+
while (this.hasRequests) {
234+
const r = await this.executeNext();
235+
for (const [key, value] of r) {
236+
responses.set(key, value);
111237
}
112238
}
113239

0 commit comments

Comments
 (0)