-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#IOPSC-118] Respond with 'too many requests' status code when creati…
…ng too much subscriptions for the same account (#202)
- Loading branch information
Showing
8 changed files
with
284 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { withRetry } from "../retry"; | ||
|
||
const aGoodResult = 42; | ||
const aBadResult = "bad things happen here"; | ||
const anAlwaysGoodOperation = jest.fn(() => Promise.resolve(aGoodResult)); | ||
const anAlwaysBadOperation = jest.fn(() => Promise.reject(aBadResult)); | ||
const anOperationGoodAtAttemptNth = (n: number) => { | ||
let count = 0; | ||
return jest.fn(() => { | ||
count++; | ||
if (count < n) { | ||
return Promise.reject(aBadResult); | ||
} else { | ||
return Promise.resolve(aGoodResult); | ||
} | ||
}); | ||
}; | ||
|
||
const sleep = ms => new Promise(done => setTimeout(done, ms)); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
// check test helper | ||
describe("anOperationGoodAtAttemptNth", () => { | ||
it("works as intended", () => { | ||
const op = anOperationGoodAtAttemptNth(3); | ||
const result1 = op(); | ||
const result2 = op(); | ||
const result3 = op(); | ||
const result4 = op(); | ||
|
||
expect(result1).rejects.toEqual(aBadResult); | ||
expect(result2).rejects.toEqual(aBadResult); | ||
expect(result3).resolves.toEqual(aGoodResult); | ||
expect(result4).resolves.toEqual(aGoodResult); | ||
}); | ||
}); | ||
|
||
describe("withRetry", () => { | ||
it("should return a promise", async () => { | ||
const retriable = withRetry()(anAlwaysGoodOperation); | ||
const result = retriable(); | ||
|
||
// check is a Promise | ||
expect( | ||
result instanceof Object && | ||
"then" in result && | ||
typeof result.then === "function" | ||
).toBe(true); | ||
|
||
const value = await result; | ||
expect(value).toEqual(aGoodResult); | ||
expect(anAlwaysGoodOperation).toBeCalledTimes(1); | ||
}); | ||
|
||
it("should fail when the operations always fails", async () => { | ||
const operation = anAlwaysBadOperation; | ||
const retriable = withRetry()(operation); | ||
const result = retriable(); | ||
|
||
try { | ||
await result; | ||
fail("expected to throw"); | ||
} catch (error) { | ||
expect(error).toEqual(aBadResult); | ||
expect(operation).toBeCalledTimes(3); | ||
} | ||
}); | ||
|
||
it("should fail when the operations fails more than retried times", async () => { | ||
const operation = anOperationGoodAtAttemptNth(4); | ||
const retriable = withRetry({ maxAttempts: 3 })(operation); | ||
const result = retriable(); | ||
|
||
try { | ||
await result; | ||
fail("expected to throw"); | ||
} catch (error) { | ||
expect(error).toEqual(aBadResult); | ||
expect(operation).toBeCalledTimes(3); | ||
} | ||
}); | ||
|
||
it("should succeed when the operations fails less than retried times", async () => { | ||
const operation = anOperationGoodAtAttemptNth(2); | ||
const retriable = withRetry({ maxAttempts: 3 })(operation); | ||
const result = retriable(); | ||
|
||
const value = await result; | ||
|
||
expect(value).toEqual(aGoodResult); | ||
expect(operation).toBeCalledTimes(2); | ||
}); | ||
|
||
it("should not retry if whileCondition is not met", async () => { | ||
const operation = anOperationGoodAtAttemptNth(2); | ||
const retriable = withRetry({ | ||
maxAttempts: 3, | ||
whileCondition: () => false | ||
})(operation); | ||
const result = retriable(); | ||
|
||
try { | ||
await result; | ||
fail("expected to throw"); | ||
} catch (error) { | ||
expect(error).toEqual(aBadResult); | ||
expect(operation).toBeCalledTimes(1); | ||
} | ||
}); | ||
|
||
it("should wait between invocations if a delay is provided", async () => { | ||
const operation = anAlwaysBadOperation; | ||
const waitFor = 1000; | ||
const retriable = withRetry({ delayMS: waitFor })(operation); | ||
const _ = retriable(); | ||
|
||
expect(operation).toBeCalledTimes(1); | ||
await sleep(waitFor * 1.01); | ||
|
||
expect(operation).toBeCalledTimes(2); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/** | ||
* Given an async operation, it retries the operation until | ||
* the retry conditions are met and maximum attempts are fulfilled. | ||
* | ||
* An optional delay in milliseconds can be waited between attempts. | ||
* | ||
* @param operation the operation to be executed | ||
* @param whileCondition stop retries when false. Default: alway true | ||
* @param maxAttempts max total calls to operation. Default: 3 | ||
* @param delayMS delay between invocations. Default: 100ms | ||
* @returns | ||
*/ | ||
|
||
export const withRetry = ({ | ||
whileCondition = (_: unknown): true => true, | ||
maxAttempts = 3, | ||
delayMS = 100 | ||
}: { | ||
readonly whileCondition?: (failure: unknown) => boolean; | ||
readonly maxAttempts?: number; | ||
readonly delayMS?: number; | ||
} = {}) => <T>(operation: () => Promise<T>) => async (): Promise<T> => { | ||
// eslint-disable-next-line functional/no-let | ||
let remainingAttempts = maxAttempts; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
try { | ||
return await operation(); | ||
} catch (error) { | ||
remainingAttempts--; | ||
|
||
if (!remainingAttempts || !whileCondition(error)) { | ||
throw error; | ||
} | ||
|
||
if (delayMS) { | ||
await new Promise(done => setTimeout(done, delayMS)); | ||
} | ||
} | ||
} | ||
}; |
Oops, something went wrong.