Skip to content

Commit d14d784

Browse files
authored
fix(cdk): changed retry mechanism for hotswapping AppSync.function (#32179)
### Reason for this change Fixing bug in hotswap for `AppSync.function` where `ConcurrentModificationException` error continues to appear when attempting hotswap on a large collection of resources. ### Description of changes Switches the retry mechanism for hotswapping AppSync.function from a simple retry where hotswap of AppSync.function is retried 5 times at 1 second intervals if a Concurrent Modification Exception occurs to an exponential back off retry, where the retry interval doubles with each failure. Exponential backoff will try 6 times (which through my testing should cover all reasonable cases) to resolve AppSync.function hotswap. ### Description of how you validated changes No integration or unit tests added. Passes all current unit tests. Tested locally on an Amplify project to ensure that cases that previously experienced `ConcurrentModificationException` errors no longer experienced them. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a73f84b commit d14d784

File tree

2 files changed

+164
-5
lines changed

2 files changed

+164
-5
lines changed

packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,14 @@ export async function isHotswappableAppSyncChange(
118118
const functions = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId });
119119
const { functionId } = functions.find((fn) => fn.name === physicalName) ?? {};
120120
// Updating multiple functions at the same time or along with graphql schema results in `ConcurrentModificationException`
121-
await simpleRetry(
121+
await exponentialBackOffRetry(
122122
() =>
123123
sdk.appsync().updateFunction({
124124
...sdkRequestObject,
125125
functionId: functionId,
126126
}),
127-
5,
127+
6,
128+
1000,
128129
'ConcurrentModificationException',
129130
);
130131
} else if (isGraphQLSchema) {
@@ -169,13 +170,13 @@ async function fetchFileFromS3(s3Url: string, sdk: SDK) {
169170
return (await sdk.s3().getObject({ Bucket: s3Bucket, Key: s3Key })).Body?.transformToString();
170171
}
171172

172-
async function simpleRetry(fn: () => Promise<any>, numOfRetries: number, errorCodeToRetry: string) {
173+
async function exponentialBackOffRetry(fn: () => Promise<any>, numOfRetries: number, backOff: number, errorCodeToRetry: string) {
173174
try {
174175
await fn();
175176
} catch (error: any) {
176177
if (error && error.name === errorCodeToRetry && numOfRetries > 0) {
177-
await sleep(1000); // wait a whole second
178-
await simpleRetry(fn, numOfRetries - 1, errorCodeToRetry);
178+
await sleep(backOff); // time to wait doubles everytime function fails, starts at 1 second
179+
await exponentialBackOffRetry(fn, numOfRetries - 1, backOff * 2, errorCodeToRetry);
179180
} else {
180181
throw error;
181182
}

packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts

+158
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,164 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot
969969
},
970970
);
971971

972+
silentTest(
973+
'updateFunction() API recovers from failed update attempt through retry logic',
974+
async () => {
975+
976+
// GIVEN
977+
mockAppSyncClient
978+
.on(ListFunctionsCommand)
979+
.resolvesOnce({
980+
functions: [{ name: 'my-function', functionId: 'functionId' }],
981+
});
982+
983+
const ConcurrentModError = new Error('ConcurrentModificationException: Schema is currently being altered, please wait until that is complete.');
984+
ConcurrentModError.name = 'ConcurrentModificationException';
985+
mockAppSyncClient
986+
.on(UpdateFunctionCommand)
987+
.rejectsOnce(ConcurrentModError)
988+
.resolvesOnce({ functionConfiguration: { name: 'my-function', dataSourceName: 'my-datasource', functionId: 'functionId' } });
989+
990+
setup.setCurrentCfnStackTemplate({
991+
Resources: {
992+
AppSyncFunction: {
993+
Type: 'AWS::AppSync::FunctionConfiguration',
994+
Properties: {
995+
Name: 'my-function',
996+
ApiId: 'apiId',
997+
DataSourceName: 'my-datasource',
998+
FunctionVersion: '2018-05-29',
999+
RequestMappingTemplate: '## original request template',
1000+
ResponseMappingTemplate: '## original response template',
1001+
},
1002+
Metadata: {
1003+
'aws:asset:path': 'old-path',
1004+
},
1005+
},
1006+
},
1007+
});
1008+
const cdkStackArtifact = setup.cdkStackArtifactOf({
1009+
template: {
1010+
Resources: {
1011+
AppSyncFunction: {
1012+
Type: 'AWS::AppSync::FunctionConfiguration',
1013+
Properties: {
1014+
Name: 'my-function',
1015+
ApiId: 'apiId',
1016+
DataSourceName: 'my-datasource',
1017+
FunctionVersion: '2018-05-29',
1018+
RequestMappingTemplate: '## original request template',
1019+
ResponseMappingTemplate: '## new response template',
1020+
},
1021+
Metadata: {
1022+
'aws:asset:path': 'new-path',
1023+
},
1024+
},
1025+
},
1026+
},
1027+
});
1028+
1029+
// WHEN
1030+
const deployStackResult = await hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact);
1031+
1032+
// THEN
1033+
expect(deployStackResult).not.toBeUndefined();
1034+
expect(mockAppSyncClient).toHaveReceivedCommandTimes(UpdateFunctionCommand, 2); // 1st failure then success on retry
1035+
expect(mockAppSyncClient).toHaveReceivedCommandWith(UpdateFunctionCommand, {
1036+
apiId: 'apiId',
1037+
dataSourceName: 'my-datasource',
1038+
functionId: 'functionId',
1039+
functionVersion: '2018-05-29',
1040+
name: 'my-function',
1041+
requestMappingTemplate: '## original request template',
1042+
responseMappingTemplate: '## new response template',
1043+
});
1044+
},
1045+
);
1046+
1047+
silentTest(
1048+
'updateFunction() API fails if it recieves 7 failed attempts in a row - this is a long running test',
1049+
async () => {
1050+
1051+
// GIVEN
1052+
mockAppSyncClient
1053+
.on(ListFunctionsCommand)
1054+
.resolvesOnce({
1055+
functions: [{ name: 'my-function', functionId: 'functionId' }],
1056+
});
1057+
1058+
const ConcurrentModError = new Error('ConcurrentModificationException: Schema is currently being altered, please wait until that is complete.');
1059+
ConcurrentModError.name = 'ConcurrentModificationException';
1060+
mockAppSyncClient
1061+
.on(UpdateFunctionCommand)
1062+
.rejectsOnce(ConcurrentModError)
1063+
.rejectsOnce(ConcurrentModError)
1064+
.rejectsOnce(ConcurrentModError)
1065+
.rejectsOnce(ConcurrentModError)
1066+
.rejectsOnce(ConcurrentModError)
1067+
.rejectsOnce(ConcurrentModError)
1068+
.rejectsOnce(ConcurrentModError)
1069+
.resolvesOnce({ functionConfiguration: { name: 'my-function', dataSourceName: 'my-datasource', functionId: 'functionId' } });
1070+
1071+
setup.setCurrentCfnStackTemplate({
1072+
Resources: {
1073+
AppSyncFunction: {
1074+
Type: 'AWS::AppSync::FunctionConfiguration',
1075+
Properties: {
1076+
Name: 'my-function',
1077+
ApiId: 'apiId',
1078+
DataSourceName: 'my-datasource',
1079+
FunctionVersion: '2018-05-29',
1080+
RequestMappingTemplate: '## original request template',
1081+
ResponseMappingTemplate: '## original response template',
1082+
},
1083+
Metadata: {
1084+
'aws:asset:path': 'old-path',
1085+
},
1086+
},
1087+
},
1088+
});
1089+
const cdkStackArtifact = setup.cdkStackArtifactOf({
1090+
template: {
1091+
Resources: {
1092+
AppSyncFunction: {
1093+
Type: 'AWS::AppSync::FunctionConfiguration',
1094+
Properties: {
1095+
Name: 'my-function',
1096+
ApiId: 'apiId',
1097+
DataSourceName: 'my-datasource',
1098+
FunctionVersion: '2018-05-29',
1099+
RequestMappingTemplate: '## original request template',
1100+
ResponseMappingTemplate: '## new response template',
1101+
},
1102+
Metadata: {
1103+
'aws:asset:path': 'new-path',
1104+
},
1105+
},
1106+
},
1107+
},
1108+
});
1109+
1110+
// WHEN
1111+
await expect(() => hotswapMockSdkProvider.tryHotswapDeployment(hotswapMode, cdkStackArtifact)).rejects.toThrow(
1112+
'ConcurrentModificationException',
1113+
);
1114+
1115+
// THEN
1116+
expect(mockAppSyncClient).toHaveReceivedCommandTimes(UpdateFunctionCommand, 7); // 1st attempt and then 6 retries before bailing
1117+
expect(mockAppSyncClient).toHaveReceivedCommandWith(UpdateFunctionCommand, {
1118+
apiId: 'apiId',
1119+
dataSourceName: 'my-datasource',
1120+
functionId: 'functionId',
1121+
functionVersion: '2018-05-29',
1122+
name: 'my-function',
1123+
requestMappingTemplate: '## original request template',
1124+
responseMappingTemplate: '## new response template',
1125+
});
1126+
},
1127+
320000,
1128+
);
1129+
9721130
silentTest('calls the updateFunction() API with functionId when function is listed on second page', async () => {
9731131
// GIVEN
9741132
mockAppSyncClient

0 commit comments

Comments
 (0)