Treat 413 and 429 as rate limits everywhere
This commit is contained in:
parent
6dd32456c6
commit
465b4cb0fb
4 changed files with 116 additions and 102 deletions
|
@ -42,13 +42,13 @@ type JobType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Goals for this service:
|
// Goals for this service:
|
||||||
// 1. Ensure that when we get a 413 from the server, we stop firing off profile
|
// 1. Ensure that when we get a 413/429 from the server, we stop firing off profile
|
||||||
// fetches for a while.
|
// fetches for a while.
|
||||||
// 2. Ensure that all existing profile fetches don't hang in this case; to solve this we
|
// 2. Ensure that all existing profile fetches don't hang in this case; to solve this we
|
||||||
// cancel all outstanding requests when we hit a 413, and throw instead of queueing
|
// cancel all outstanding requests when we hit a 413/429, and throw instead of
|
||||||
// something new if we're waiting due to a retry-after. Note: It's no worse than what
|
// queueing something new if we're waiting due to a retry-after. Note: It's no worse
|
||||||
// we were doing before, failing all requests and pushing the retry-after time out
|
// than what we were doing before, failing all requests and pushing the retry-after
|
||||||
// further.
|
// time out further.
|
||||||
// 3. Require no changes to callers.
|
// 3. Require no changes to callers.
|
||||||
|
|
||||||
// Potential future goals for this problem area:
|
// Potential future goals for this problem area:
|
||||||
|
@ -121,8 +121,12 @@ export class ProfileService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRecord(error) && 'code' in error && error.code === 413) {
|
if (
|
||||||
this.clearAll('got 413 from server');
|
isRecord(error) &&
|
||||||
|
'code' in error &&
|
||||||
|
(error.code === 413 || error.code === 429)
|
||||||
|
) {
|
||||||
|
this.clearAll(`got ${error.code} from server`);
|
||||||
const time = findRetryAfterTimeFromError(error);
|
const time = findRetryAfterTimeFromError(error);
|
||||||
void this.pause(time);
|
void this.pause(time);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,9 +78,9 @@ export async function reserveUsername(
|
||||||
if (error.code === 409) {
|
if (error.code === 409) {
|
||||||
return { ok: false, error: ReserveUsernameError.Conflict };
|
return { ok: false, error: ReserveUsernameError.Conflict };
|
||||||
}
|
}
|
||||||
if (error.code === 413) {
|
if (error.code === 413 || error.code === 429) {
|
||||||
const time = findRetryAfterTimeFromError(error);
|
const time = findRetryAfterTimeFromError(error);
|
||||||
log.warn(`reserveUsername: got 413, waiting ${time}ms`);
|
log.warn(`reserveUsername: got ${error.code}, waiting ${time}ms`);
|
||||||
await sleep(time, abortSignal);
|
await sleep(time, abortSignal);
|
||||||
|
|
||||||
return reserveUsername(options);
|
return reserveUsername(options);
|
||||||
|
@ -139,9 +139,9 @@ export async function confirmUsername(
|
||||||
await updateUsernameAndSyncProfile(username);
|
await updateUsernameAndSyncProfile(username);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HTTPError) {
|
if (error instanceof HTTPError) {
|
||||||
if (error.code === 413) {
|
if (error.code === 413 || error.code === 429) {
|
||||||
const time = findRetryAfterTimeFromError(error);
|
const time = findRetryAfterTimeFromError(error);
|
||||||
log.warn(`confirmUsername: got 413, waiting ${time}ms`);
|
log.warn(`confirmUsername: got ${error.code}, waiting ${time}ms`);
|
||||||
await sleep(time, abortSignal);
|
await sleep(time, abortSignal);
|
||||||
|
|
||||||
return confirmUsername(reservation, abortSignal);
|
return confirmUsername(reservation, abortSignal);
|
||||||
|
|
|
@ -101,41 +101,43 @@ describe('util/profiles', () => {
|
||||||
assert.strictEqual(runCount, 0);
|
assert.strictEqual(runCount, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears all outstanding jobs if we get a 413, then pauses', async () => {
|
for (const code of [413, 429] as const) {
|
||||||
let runCount = 0;
|
it(`clears all outstanding jobs if we get a ${code}, then pauses`, async () => {
|
||||||
const getProfileWhichThrows = async () => {
|
let runCount = 0;
|
||||||
runCount += 1;
|
const getProfileWhichThrows = async () => {
|
||||||
const error = new HTTPError('fake 413', {
|
runCount += 1;
|
||||||
code: 413,
|
const error = new HTTPError(`fake ${code}`, {
|
||||||
headers: {
|
code,
|
||||||
'retry-after': '1',
|
headers: {
|
||||||
},
|
'retry-after': '1',
|
||||||
});
|
},
|
||||||
throw error;
|
});
|
||||||
};
|
throw error;
|
||||||
const service = new ProfileService(getProfileWhichThrows);
|
};
|
||||||
|
const service = new ProfileService(getProfileWhichThrows);
|
||||||
|
|
||||||
// Queued and immediately started due to concurrency = 3
|
// Queued and immediately started due to concurrency = 3
|
||||||
const promise1 = service.get(UUID_1);
|
const promise1 = service.get(UUID_1);
|
||||||
const promise2 = service.get(UUID_2);
|
const promise2 = service.get(UUID_2);
|
||||||
const promise3 = service.get(UUID_3);
|
const promise3 = service.get(UUID_3);
|
||||||
|
|
||||||
// Never started, but queued
|
// Never started, but queued
|
||||||
const promise4 = service.get(UUID_4);
|
const promise4 = service.get(UUID_4);
|
||||||
|
|
||||||
assert.strictEqual(runCount, 3, 'before await');
|
assert.strictEqual(runCount, 3, 'before await');
|
||||||
|
|
||||||
await assert.isRejected(promise1, 'fake 413');
|
await assert.isRejected(promise1, `fake ${code}`);
|
||||||
|
|
||||||
// Never queued
|
// Never queued
|
||||||
const promise5 = service.get(UUID_5);
|
const promise5 = service.get(UUID_5);
|
||||||
|
|
||||||
await assert.isRejected(promise2, 'job cancelled');
|
await assert.isRejected(promise2, 'job cancelled');
|
||||||
await assert.isRejected(promise3, 'job cancelled');
|
await assert.isRejected(promise3, 'job cancelled');
|
||||||
await assert.isRejected(promise4, 'job cancelled');
|
await assert.isRejected(promise4, 'job cancelled');
|
||||||
await assert.isRejected(promise5, 'paused queue');
|
await assert.isRejected(promise5, 'paused queue');
|
||||||
|
|
||||||
assert.strictEqual(runCount, 3, 'after await');
|
assert.strictEqual(runCount, 3, 'after await');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -34,9 +34,9 @@ describe('maybeExpandErrors', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleMultipleSendErrors', () => {
|
describe('handleMultipleSendErrors', () => {
|
||||||
const make413 = (retryAfter: number): HTTPError =>
|
const makeSlowDown = (code: 413 | 429, retryAfter: number): HTTPError =>
|
||||||
new HTTPError('Slow down', {
|
new HTTPError('Slow down', {
|
||||||
code: 413,
|
code,
|
||||||
headers: { 'retry-after': retryAfter.toString() },
|
headers: { 'retry-after': retryAfter.toString() },
|
||||||
response: {},
|
response: {},
|
||||||
});
|
});
|
||||||
|
@ -101,76 +101,84 @@ describe('handleMultipleSendErrors', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('413 handling', () => {
|
for (const code of [413, 429] as const) {
|
||||||
it('sleeps for the longest 413 Retry-After time', async () => {
|
// eslint-disable-next-line no-loop-func
|
||||||
let done = false;
|
describe(`${code} handling`, () => {
|
||||||
|
it(`sleeps for the longest ${code} Retry-After time`, async () => {
|
||||||
|
let done = false;
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await handleMultipleSendErrors({
|
await handleMultipleSendErrors({
|
||||||
|
...defaultOptions,
|
||||||
|
errors: [
|
||||||
|
new Error('Other'),
|
||||||
|
makeSlowDown(code, 10),
|
||||||
|
makeSlowDown(code, 999),
|
||||||
|
makeSlowDown(code, 20),
|
||||||
|
],
|
||||||
|
timeRemaining: 99999999,
|
||||||
|
toThrow: new Error('to throw'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// No-op
|
||||||
|
} finally {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
await clock.tickAsync(900 * SECOND);
|
||||||
|
assert.isFalse(done, "Didn't sleep for long enough");
|
||||||
|
await clock.tickAsync(100 * SECOND);
|
||||||
|
assert.isTrue(done, 'Slept for too long');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't sleep longer than the remaining time", async () => {
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await handleMultipleSendErrors({
|
||||||
|
...defaultOptions,
|
||||||
|
errors: [makeSlowDown(code, 9999)],
|
||||||
|
timeRemaining: 99,
|
||||||
|
toThrow: new Error('to throw'),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// No-op
|
||||||
|
} finally {
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
await clock.tickAsync(100);
|
||||||
|
assert.isTrue(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't sleep if it's the final attempt", async () => {
|
||||||
|
await assert.isRejected(
|
||||||
|
handleMultipleSendErrors({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
errors: [
|
errors: [new Error('uh oh')],
|
||||||
new Error('Other'),
|
isFinalAttempt: true,
|
||||||
make413(10),
|
|
||||||
make413(999),
|
|
||||||
make413(20),
|
|
||||||
],
|
|
||||||
timeRemaining: 99999999,
|
|
||||||
toThrow: new Error('to throw'),
|
toThrow: new Error('to throw'),
|
||||||
});
|
})
|
||||||
} catch (err) {
|
);
|
||||||
// No-op
|
});
|
||||||
} finally {
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
await clock.tickAsync(900 * SECOND);
|
|
||||||
assert.isFalse(done, "Didn't sleep for long enough");
|
|
||||||
await clock.tickAsync(100 * SECOND);
|
|
||||||
assert.isTrue(done, 'Slept for too long');
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
it("doesn't sleep longer than the remaining time", async () => {
|
|
||||||
let done = false;
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await handleMultipleSendErrors({
|
|
||||||
...defaultOptions,
|
|
||||||
errors: [make413(9999)],
|
|
||||||
timeRemaining: 99,
|
|
||||||
toThrow: new Error('to throw'),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
// No-op
|
|
||||||
} finally {
|
|
||||||
done = true;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
await clock.tickAsync(100);
|
|
||||||
assert.isTrue(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't sleep if it's the final attempt", async () => {
|
|
||||||
await assert.isRejected(
|
|
||||||
handleMultipleSendErrors({
|
|
||||||
...defaultOptions,
|
|
||||||
errors: [new Error('uh oh')],
|
|
||||||
isFinalAttempt: true,
|
|
||||||
toThrow: new Error('to throw'),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('508 handling', () => {
|
describe('508 handling', () => {
|
||||||
it('resolves with no error if any 508 is received', async () => {
|
it('resolves with no error if any 508 is received', async () => {
|
||||||
await assert.isFulfilled(
|
await assert.isFulfilled(
|
||||||
handleMultipleSendErrors({
|
handleMultipleSendErrors({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
errors: [new Error('uh oh'), { code: 508 }, make413(99999)],
|
errors: [
|
||||||
|
new Error('uh oh'),
|
||||||
|
{ code: 508 },
|
||||||
|
makeSlowDown(413, 99999),
|
||||||
|
makeSlowDown(429, 99999),
|
||||||
|
],
|
||||||
markFailed: noop,
|
markFailed: noop,
|
||||||
toThrow: new Error('to throw'),
|
toThrow: new Error('to throw'),
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue