From f71ab862db0c31bd011a4789312969131cbb9243 Mon Sep 17 00:00:00 2001 From: corymhall <43035978+corymhall@users.noreply.github.com> Date: Wed, 12 Oct 2022 17:53:50 +0000 Subject: [PATCH] adding more validation for backoff --- packages/@aws-cdk/integ-tests/README.md | 4 +- .../lib/assertions/waiter-state-machine.ts | 42 +++++++++++++++-- .../integ-tests/test/assertions/sdk.test.ts | 45 ++++++++++++++++++- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/integ-tests/README.md b/packages/@aws-cdk/integ-tests/README.md index e4779ff56252d..1d09541a36377 100644 --- a/packages/@aws-cdk/integ-tests/README.md +++ b/packages/@aws-cdk/integ-tests/README.md @@ -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. @@ -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 diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/waiter-state-machine.ts b/packages/@aws-cdk/integ-tests/lib/assertions/waiter-state-machine.ts index 566cc9bddca12..a2a63df342a7e 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/waiter-state-machine.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/waiter-state-machine.ts @@ -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) */ @@ -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; } @@ -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 { /** @@ -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`); @@ -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; +} diff --git a/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts index 79541b8817051..8a0123ca8e96b 100644 --- a/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts +++ b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts @@ -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'; @@ -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', () => {