Skip to content

Commit 8adff21

Browse files
authored
fix: api rate limiting (#29)
We pool CodeBuild for updates, doing this every 5 seconds. This was too fast for some environments. Dropped the pooling to 15 seconds and configured a backoff. resolves #28
1 parent 406f1e1 commit 8adff21

File tree

2 files changed

+143
-3
lines changed

2 files changed

+143
-3
lines changed

code-build.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,20 @@ async function build(sdk, params) {
3535
}
3636

3737
async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
38-
const { codeBuild, cloudWatchLogs, wait = 1000 * 5 } = sdk;
38+
const {
39+
codeBuild,
40+
cloudWatchLogs,
41+
wait = 1000 * 30,
42+
backOff = 1000 * 15
43+
} = sdk;
3944

4045
// Get the CloudWatchLog info
4146
const startFromHead = true;
4247
const { cloudWatchLogsArn } = logs;
4348
const { logGroupName, logStreamName } = logName(cloudWatchLogsArn);
4449

50+
let errObject = false;
51+
4552
// Check the state
4653
const [batch, cloudWatch = {}] = await Promise.all([
4754
codeBuild.batchGetBuilds({ ids: [id] }).promise(),
@@ -50,7 +57,37 @@ async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
5057
cloudWatchLogs
5158
.getLogEvents({ logGroupName, logStreamName, startFromHead, nextToken })
5259
.promise()
53-
]);
60+
]).catch(err => {
61+
errObject = err;
62+
/* Returning [] here so that the assignment above
63+
* does not throw `TypeError: undefined is not iterable`.
64+
* The error is handled below,
65+
* since it might be a rate limit.
66+
*/
67+
return [];
68+
});
69+
70+
if (errObject) {
71+
//We caught an error in trying to make the AWS api call, and are now checking to see if it was just a rate limiting error
72+
if (errObject.message && errObject.message.search("Rate exceeded") !== -1) {
73+
//We were rate-limited, so add `backOff` seconds to the wait time
74+
let newWait = wait + backOff;
75+
76+
//Sleep before trying again
77+
await new Promise(resolve => setTimeout(resolve, newWait));
78+
79+
// Try again from the same token position
80+
return waitForBuildEndTime(
81+
{ ...sdk, wait: newWait },
82+
{ id, logs },
83+
nextToken
84+
);
85+
} else {
86+
//The error returned from the API wasn't about rate limiting, so throw it as an actual error and fail the job
87+
throw errObject;
88+
}
89+
}
90+
5491
// Pluck off the relevant state
5592
const [current] = batch.builds;
5693
const { nextForwardToken, events = [] } = cloudWatch;
@@ -64,7 +101,7 @@ async function waitForBuildEndTime(sdk, { id, logs }, nextToken) {
64101
// We did it! We can stop looking!
65102
if (current.endTime && !events.length) return current;
66103

67-
// More to do: Sleep for 5 seconds :)
104+
// More to do: Sleep for a few seconds to avoid rate limiting
68105
await new Promise(resolve => setTimeout(resolve, wait));
69106

70107
// Try again

test/code-build-test.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,109 @@ describe("waitForBuildEndTime", () => {
318318
});
319319
expect(test).to.equal(buildReplies.pop().builds[0]);
320320
});
321+
322+
it("waits after being rate limited and tries again", async function() {
323+
const buildID = "buildID";
324+
const nullArn =
325+
"arn:aws:logs:us-west-2:111122223333:log-group:null:log-stream:null";
326+
const cloudWatchLogsArn =
327+
"arn:aws:logs:us-west-2:111122223333:log-group:/aws/codebuild/CloudWatchLogGroup:log-stream:1234abcd-12ab-34cd-56ef-1234567890ab";
328+
329+
const buildReplies = [
330+
() => {
331+
throw { message: "Rate exceeded" };
332+
},
333+
{ builds: [{ id: buildID, logs: { cloudWatchLogsArn } }] },
334+
{
335+
builds: [
336+
{ id: buildID, logs: { cloudWatchLogsArn }, endTime: "endTime" }
337+
]
338+
}
339+
];
340+
341+
const sdk = help(
342+
() => {
343+
//similar to the ret function in the helper, allows me to throw an error in a function or return a more standard reply
344+
let reply = buildReplies.shift();
345+
346+
if (typeof reply === "function") return reply();
347+
return reply;
348+
},
349+
() => {
350+
if (!buildReplies.length) {
351+
return { events: [] };
352+
}
353+
354+
return { events: [{ message: "got one" }] };
355+
}
356+
);
357+
358+
const test = await waitForBuildEndTime(
359+
{ ...sdk, wait: 1, backOff: 1 },
360+
{
361+
id: buildID,
362+
logs: { cloudWatchLogsArn: nullArn }
363+
}
364+
);
365+
366+
expect(test.id).to.equal(buildID);
367+
});
368+
369+
it("dies after getting an error from the aws sdk that isn't rate limiting", async function() {
370+
const buildID = "buildID";
371+
const nullArn =
372+
"arn:aws:logs:us-west-2:111122223333:log-group:null:log-stream:null";
373+
const cloudWatchLogsArn =
374+
"arn:aws:logs:us-west-2:111122223333:log-group:/aws/codebuild/CloudWatchLogGroup:log-stream:1234abcd-12ab-34cd-56ef-1234567890ab";
375+
376+
const buildReplies = [
377+
() => {
378+
throw { message: "Some AWS error" };
379+
},
380+
{ builds: [{ id: buildID, logs: { cloudWatchLogsArn } }] },
381+
{
382+
builds: [
383+
{ id: buildID, logs: { cloudWatchLogsArn }, endTime: "endTime" }
384+
]
385+
}
386+
];
387+
388+
const sdk = help(
389+
() => {
390+
//similar to the ret function in the helper
391+
//allows me to throw an error in a function or return a more standard reply
392+
let reply = buildReplies.shift();
393+
394+
if (typeof reply === "function") return reply();
395+
return reply;
396+
},
397+
() => {
398+
if (!buildReplies.length) {
399+
return { events: [] };
400+
}
401+
402+
return { events: [{ message: "got one" }] };
403+
}
404+
);
405+
406+
//run the thing and it should fail
407+
let didFail = false;
408+
409+
try {
410+
await waitForBuildEndTime(
411+
{ ...sdk, wait: 1, backOff: 1 },
412+
{
413+
id: buildID,
414+
logs: { cloudWatchLogsArn: nullArn }
415+
}
416+
);
417+
} catch (err) {
418+
didFail = true;
419+
expect(err.message).to.equal("Some AWS error");
420+
}
421+
422+
expect(didFail).to.equal(true);
423+
});
321424
});
322425

323426
function help(builds, logs) {

0 commit comments

Comments
 (0)