Skip to content

Commit

Permalink
adding more validation for backoff
Browse files Browse the repository at this point in the history
  • Loading branch information
corymhall committed Oct 12, 2022
1 parent dfe4673 commit f71ab86
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 6 deletions.
4 changes: 2 additions & 2 deletions packages/@aws-cdk/integ-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ integ.assertions.awsApiCall('S3', 'putObject', {

A common use case when performing assertions is to wait for a condition to pass. Sometimes the thing
that you are asserting against is not done provisioning by the time the assertion runs. In these
cases it is possible to run the assertion asynchronously by calling the `wait()` method.
cases it is possible to run the assertion asynchronously by calling the `waitForAssertions()` method.

Taking the example above of executing a StepFunctions state machine, depending on the complexity of
the state machine, it might take a while for it to complete.
Expand All @@ -459,7 +459,7 @@ const describe = testCase.assertions.awsApiCall('StepFunctions', 'describeExecut
})).waitForAssertions();
```

When you call `wait()` the assertion provider will continuously make the `awsApiCall` until the
When you call `waitForAssertions()` the assertion provider will continuously make the `awsApiCall` until the
`ExpectedResult` is met. You can also control the parameters for waiting, for example:

```ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface WaiterStateMachineOptions {
readonly totalTimeout?: Duration;

/**
* The interval to wait between attempts.
* The interval (number of seconds) to wait between attempts.
*
* @default Duration.seconds(5)
*/
Expand All @@ -24,7 +24,13 @@ export interface WaiterStateMachineOptions {
/**
* Backoff between attempts.
*
* @default 1
* This is the multiplier by which the retry interval increases
* after each retry attempt.
*
* By default there is no backoff. Each retry will wait the amount of time
* specified by `interval`.
*
* @default 1 (no backoff)
*/
readonly backoffRate?: number;
}
Expand All @@ -40,6 +46,20 @@ export interface WaiterStateMachineProps extends WaiterStateMachineOptions {}
*
* The state machine continuously calls the isCompleteHandler, until it succeeds or times out.
* The handler is called `maxAttempts` times with an `interval` duration and a `backoffRate` rate.
*
* For example with:
* - maxAttempts = 360 (30 minutes)
* - interval = 5
* - backoffRate = 1 (no backoff)
*
* it will make the API Call every 5 seconds and fail after 360 failures.
*
* If the backoff rate is changed to 2 (for example), it will
* - make the first call
* - wait 5 seconds
* - make the second call
* - wait 15 seconds
* - etc.
*/
export class WaiterStateMachine extends Construct {
/**
Expand All @@ -61,7 +81,7 @@ export class WaiterStateMachine extends Construct {
super(scope, id);
const interval = props.interval || Duration.seconds(5);
const totalTimeout = props.totalTimeout || Duration.minutes(30);
const maxAttempts = totalTimeout.toSeconds() / interval.toSeconds();
const maxAttempts = calculateMaxRetries(totalTimeout.toSeconds(), interval.toSeconds(), props.backoffRate ?? 1);

if (Math.round(maxAttempts) !== maxAttempts) {
throw new Error(`Cannot determine retry count since totalTimeout=${totalTimeout.toSeconds()}s is not integrally dividable by queryInterval=${interval.toSeconds()}s`);
Expand Down Expand Up @@ -144,3 +164,19 @@ export class WaiterStateMachine extends Construct {
timeoutProvider.grantInvoke(this.roleArn);
}
}

/**
* Calculate the max number of retries
*/
function calculateMaxRetries(maxSeconds: number, intervalSeconds: number, backoff: number): number {
let retries = 1;
let nextInterval = intervalSeconds;
let i = 0;
while (i < maxSeconds) {
nextInterval = nextInterval+nextInterval*backoff;
i+=nextInterval;
if (i >= maxSeconds) break;
retries++;
}
return retries;
}
45 changes: 44 additions & 1 deletion packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Template, Match } from '@aws-cdk/assertions';
import { App, CfnOutput } from '@aws-cdk/core';
import { App, CfnOutput, Duration } from '@aws-cdk/core';
import { LogType, InvocationType, ExpectedResult } from '../../lib/assertions';
import { DeployAssert } from '../../lib/assertions/private/deploy-assert';

Expand Down Expand Up @@ -165,6 +165,49 @@ describe('AwsApiCall', () => {
},
});
});
test('waitFor with options', () => {
// GIVEN
const app = new App();
const deplossert = new DeployAssert(app);

// WHEN
deplossert.awsApiCall('MyService', 'MyApi', {
param1: 'val1',
param2: 2,
}).expect(ExpectedResult.objectLike({
Key: 'Value',
})).waitForAssertions({
interval: Duration.seconds(10),
backoffRate: 2,
totalTimeout: Duration.minutes(10),
});

// THEN
Template.fromStack(deplossert.scope).hasResourceProperties('AWS::StepFunctions::StateMachine', {
DefinitionString: {
'Fn::Join': [
'',
[
'{"StartAt":"framework-isComplete-task","States":{"framework-isComplete-task":{"End":true,"Retry":[{"ErrorEquals":["States.ALL"],"IntervalSeconds":10,"MaxAttempts":4,"BackoffRate":2}],"Catch":[{"ErrorEquals":["States.ALL"],"Next":"framework-onTimeout-task"}],"Type":"Task","Resource":"',
{
'Fn::GetAtt': [
'SingletonFunction76b3e830a873425f8453eddd85c86925Handler81461ECE',
'Arn',
],
},
'"},"framework-onTimeout-task":{"End":true,"Type":"Task","Resource":"',
{
'Fn::GetAtt': [
'SingletonFunction5c1898e096fb4e3e95d5f6c67f3ce41aHandlerADF3E6EA',
'Arn',
],
},
'"}}}',
],
],
},
});
});

describe('get attribute', () => {
test('getAttString', () => {
Expand Down

0 comments on commit f71ab86

Please sign in to comment.