-
Notifications
You must be signed in to change notification settings - Fork 246
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(pacmak): retry select external command invocations (#2013)
When generating packages, `jsii-pacmak` will defer to external tools for compilation (`dotnet`, `mvn`, `pip`, ...). Those commands will typically require some kind of network transfer (e.g: to resolve dependencies from their standard package registry), which is susceptible to fail (due to poor network conditions, getting throttled in CI/CD, etc...). In order to reduce the likelihood that such failures will interrupt the user workflow abruptly (forcing a manual retry, and possibly breaking CI/CD workflows), back-off and retries will be performed when these commands fail (up to 5 tentatives in total, with exponential back-off using a 150ms base cooldown and a factor of 2). --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
- Loading branch information
1 parent
485887b
commit 66cf186
Showing
5 changed files
with
287 additions
and
25 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,133 @@ | ||
import { AllAttemptsFailed, retry, wait } from '../lib/util'; | ||
|
||
type Waiter = typeof wait; | ||
|
||
describe(retry, () => { | ||
const mockWaiter = jest.fn<ReturnType<Waiter>, Parameters<Waiter>>(); | ||
|
||
beforeEach((done) => { | ||
mockWaiter.mockImplementation(() => Promise.resolve()); | ||
done(); | ||
}); | ||
|
||
afterEach((done) => { | ||
jest.resetAllMocks(); | ||
done(); | ||
}); | ||
|
||
test('throws on invalid maxTries', async () => { | ||
// GIVEN | ||
const fakeCallback = jest.fn(); | ||
|
||
// WHEN | ||
const result = retry(fakeCallback, { maxAttempts: 0 }, mockWaiter); | ||
|
||
// THEN | ||
await expect(result).rejects.toThrow('maxTries must be > 0'); | ||
expect(fakeCallback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('throws on invalid backoffBaseMilliseconds', async () => { | ||
// GIVEN | ||
const fakeCallback = jest.fn(); | ||
|
||
// WHEN | ||
const result = retry( | ||
fakeCallback, | ||
{ backoffBaseMilliseconds: 0 }, | ||
mockWaiter, | ||
); | ||
|
||
// THEN | ||
await expect(result).rejects.toThrow('backoffBaseMilliseconds must be > 0'); | ||
expect(fakeCallback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('throws on invalid backoffMultiplier', async () => { | ||
// GIVEN | ||
const fakeCallback = jest.fn(); | ||
|
||
// WHEN | ||
const result = retry(fakeCallback, { backoffMultiplier: 1 }, mockWaiter); | ||
|
||
// THEN | ||
await expect(result).rejects.toThrow('backoffMultiplier must be > 1'); | ||
expect(fakeCallback).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('throws when it exhausted all attempts', async () => { | ||
// GIVEN | ||
const alwaysFail = () => Promise.reject(new Error('Always')); | ||
|
||
// WHEN | ||
const result = retry(alwaysFail, {}, mockWaiter); | ||
|
||
// THEN | ||
await expect(result).rejects.toThrow(AllAttemptsFailed); | ||
expect(mockWaiter).toHaveBeenCalledTimes(4); | ||
for (let i = 0; i < 4; i++) { | ||
expect(mockWaiter).toHaveBeenNthCalledWith(i + 1, 150 * Math.pow(2, i)); | ||
} | ||
}); | ||
|
||
test('behaves correctly if callback throws non-Error', async () => { | ||
// GIVEN | ||
const alwaysFail = () => Promise.reject('Always'); | ||
|
||
// WHEN | ||
const result = retry( | ||
alwaysFail, | ||
{ maxAttempts: 3, backoffBaseMilliseconds: 1337, backoffMultiplier: 42 }, | ||
mockWaiter, | ||
); | ||
|
||
// THEN | ||
await expect(result).rejects.toThrow(AllAttemptsFailed); | ||
expect(mockWaiter).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
test('correctly implements exponential back-off and calls onFaieldAttempt appropriately', async () => { | ||
// GIVEN | ||
const expectedResult = Math.random(); | ||
let attemptNumber = 0; | ||
const failFourTimes = () => | ||
new Promise((ok, ko) => { | ||
attemptNumber++; | ||
if (attemptNumber <= 4) { | ||
ko(new Error(`Attempt #${attemptNumber}`)); | ||
} else { | ||
ok(expectedResult); | ||
} | ||
}); | ||
const onFailedAttempt = jest | ||
.fn() | ||
.mockName('onFailedAttempt') | ||
.mockImplementation( | ||
(error: Error, attemptsLeft: number, backoffMs: number) => { | ||
expect(error.message).toBe(`Attempt #${attemptNumber}`); | ||
expect(attemptsLeft).toBe(5 - attemptNumber); | ||
expect(backoffMs).toBe(1337 * Math.pow(42, attemptNumber - 1)); | ||
}, | ||
); | ||
|
||
// WHEN | ||
const result = await retry( | ||
failFourTimes, | ||
{ | ||
backoffBaseMilliseconds: 1337, | ||
backoffMultiplier: 42, | ||
onFailedAttempt, | ||
}, | ||
mockWaiter, | ||
); | ||
|
||
// THEN | ||
expect(result).toBe(result); | ||
|
||
expect(onFailedAttempt).toHaveBeenCalledTimes(4); | ||
expect(mockWaiter).toHaveBeenCalledTimes(4); | ||
for (let i = 0; i < 4; i++) { | ||
expect(mockWaiter).toHaveBeenNthCalledWith(i + 1, 1337 * Math.pow(42, i)); | ||
} | ||
}); | ||
}); |