Skip to content

Commit 06e4def

Browse files
vigy02Kamil Sobol
andauthored
Added retry mechanism to catch known errors for health checks (#2209)
* Added retry mechanism to catch known errors * Updated the error message to cover multiple scenarios * Added Retry Mechanism that can work for any tests * Update .changeset/nine-toes-dress.md Co-authored-by: Kamil Sobol <[email protected]> * chore:Update API * Refined retry mechanism removed timeout and delay * chore: Update API * chore: update API * Updated error check for test case * Updated retry test case condition * updated the maxretry parameter * chore: update changeset * chore: update changeset * Updated a test case without retry * Update packages/integration-tests/src/amplify_app_pool.ts Co-authored-by: Kamil Sobol <[email protected]> * updated retry mechanism --------- Co-authored-by: Kamil Sobol <[email protected]>
1 parent 469b9f2 commit 06e4def

File tree

5 files changed

+111
-44
lines changed

5 files changed

+111
-44
lines changed

.changeset/nine-toes-dress.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.eslint_dictionary.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
"renderer",
137137
"repo",
138138
"resolvers",
139+
"retryable",
139140
"saml",
140141
"scala",
141142
"schema",

packages/integration-tests/src/amplify_app_pool.ts

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@aws-sdk/client-amplify';
1414
import { shortUuid } from './short_uuid.js';
1515
import { e2eToolingClientConfig } from './e2e_tooling_client_config.js';
16+
import { runWithRetry } from './retry.js';
1617

1718
export type TestBranch = {
1819
readonly appId: string;
@@ -46,42 +47,46 @@ class DefaultAmplifyAppPool implements AmplifyAppPool {
4647
}
4748

4849
fetchTestBranchDetails = async (testBranch: TestBranch): Promise<Branch> => {
49-
const branch = (
50-
await this.amplifyClient.send(
51-
new GetBranchCommand({
52-
appId: testBranch.appId,
53-
branchName: testBranch.branchName,
54-
})
55-
)
56-
).branch;
57-
if (!branch) {
58-
throw new Error(
59-
`Failed to retrieve ${testBranch.branchName} branch of app ${testBranch.appId}`
60-
);
61-
}
62-
return branch;
50+
return this.retryableOperation(async () => {
51+
const branch = (
52+
await this.amplifyClient.send(
53+
new GetBranchCommand({
54+
appId: testBranch.appId,
55+
branchName: testBranch.branchName,
56+
})
57+
)
58+
).branch;
59+
if (!branch) {
60+
throw new Error(
61+
`Failed to retrieve ${testBranch.branchName} branch of app ${testBranch.appId}`
62+
);
63+
}
64+
return branch;
65+
});
6366
};
6467

6568
createTestBranch = async (): Promise<TestBranch> => {
66-
const app = await this.getAppWithCapacity();
67-
const branch = (
68-
await this.amplifyClient.send(
69-
new CreateBranchCommand({
70-
branchName: `${this.testBranchPrefix}${shortUuid()}`,
69+
return this.retryableOperation(async () => {
70+
const app = await this.getAppWithCapacity();
71+
const branch = (
72+
await this.amplifyClient.send(
73+
new CreateBranchCommand({
74+
branchName: `${this.testBranchPrefix}${shortUuid()}`,
75+
appId: app.appId,
76+
})
77+
)
78+
).branch;
79+
if (app.appId && branch?.branchName) {
80+
const testBranch: TestBranch = {
7181
appId: app.appId,
72-
})
73-
)
74-
).branch;
75-
if (app.appId && branch?.branchName) {
76-
const testBranch: TestBranch = {
77-
appId: app.appId,
78-
branchName: branch.branchName,
79-
};
80-
this.branchesCreated.push(testBranch);
81-
return testBranch;
82-
}
82+
branchName: branch.branchName,
83+
};
84+
this.branchesCreated.push(testBranch);
85+
return testBranch;
86+
}
8387

84-
throw new Error('Unable to create branch');
88+
throw new Error('Unable to create branch');
89+
});
8590
};
8691

8792
private listAllTestAmplifyApps = async (): Promise<Array<App>> => {
@@ -173,6 +178,16 @@ class DefaultAmplifyAppPool implements AmplifyAppPool {
173178
}
174179
}
175180
};
181+
182+
private retryableOperation = <T>(operation: () => Promise<T>) => {
183+
return runWithRetry(operation, (error) => {
184+
// Add specific error conditions here that warrant a retry
185+
return (
186+
error.message.includes('Unexpected token') ||
187+
error.message.includes('Bad control character')
188+
);
189+
});
190+
};
176191
}
177192

178193
export const amplifyAppPool: AmplifyAppPool = new DefaultAmplifyAppPool(

packages/integration-tests/src/package_manager_sanity_checks.test.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
runWithPackageManager,
2727
} from './process-controller/process_controller.js';
2828
import { amplifyAtTag } from './constants.js';
29+
import { runWithRetry } from './retry.js';
2930

3031
void describe('getting started happy path', async () => {
3132
let branchBackendIdentifier: BackendIdentifier;
@@ -81,19 +82,35 @@ void describe('getting started happy path', async () => {
8182
if (packageManager === 'pnpm' && process.platform === 'win32') {
8283
return;
8384
}
84-
if (packageManager === 'yarn-classic') {
85-
await execa('yarn', ['add', 'create-amplify'], { cwd: tempDir });
86-
await execaCommand('./node_modules/.bin/create-amplify --yes --debug', {
87-
cwd: tempDir,
88-
env: { npm_config_user_agent: 'yarn/1.22.21' },
89-
});
90-
} else {
91-
await runPackageManager(
92-
packageManager,
93-
['create', amplifyAtTag, '--yes'],
94-
tempDir
95-
).run();
96-
}
85+
86+
await runWithRetry(
87+
async () => {
88+
if (packageManager === 'yarn-classic') {
89+
await execa('yarn', ['add', 'create-amplify'], { cwd: tempDir });
90+
await execaCommand(
91+
'./node_modules/.bin/create-amplify --yes --debug',
92+
{
93+
cwd: tempDir,
94+
env: { npm_config_user_agent: 'yarn/1.22.21' },
95+
}
96+
);
97+
} else {
98+
await runPackageManager(
99+
packageManager,
100+
['create', amplifyAtTag, '--yes'],
101+
tempDir
102+
).run();
103+
}
104+
},
105+
(error) => {
106+
// Retry on network-related errors or command failures
107+
return (
108+
error.message.includes('exit code 1') &&
109+
(error.message.includes('aws-amplify') ||
110+
error.message.includes('Command failed with exit code 1: yarn add'))
111+
);
112+
}
113+
);
97114

98115
const pathPrefix = path.join(tempDir, 'amplify');
99116

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Executes an asynchronous operation with retry logic.
3+
* This function attempts to execute the provided callable function multiple times
4+
* based on the specified retry conditions. It's useful for handling transient
5+
* errors or temporary service unavailability.
6+
*/
7+
export const runWithRetry = async <T>(
8+
callable: () => Promise<T>,
9+
retryPredicate: (error: Error) => boolean,
10+
maxAttempts = 3
11+
): Promise<T> => {
12+
const collectedErrors: Error[] = [];
13+
14+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
15+
try {
16+
const result = await callable();
17+
return result;
18+
} catch (error) {
19+
if (error instanceof Error) {
20+
collectedErrors.push(error);
21+
if (!retryPredicate(error)) {
22+
throw error;
23+
}
24+
}
25+
}
26+
}
27+
28+
throw new AggregateError(
29+
collectedErrors,
30+
`All ${maxAttempts} attempts failed`
31+
);
32+
};

0 commit comments

Comments
 (0)