Skip to content

Commit c69c63a

Browse files
retry api calls
1 parent cf74f86 commit c69c63a

File tree

5 files changed

+445
-60
lines changed

5 files changed

+445
-60
lines changed

src/providers/base_provider.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import utils from '../utils';
66
import Upload from '../upload';
77
import platform from '../utils/platform';
88
import logger from '../logger';
9-
import { handleAxiosError, isNetworkError } from '../utils/error-helpers';
9+
import {
10+
handleAxiosError,
11+
isNetworkError,
12+
isRetryableError,
13+
} from '../utils/error-helpers';
1014
import {
1115
checkInternetConnectivity,
1216
formatConnectivityResults,
@@ -212,6 +216,66 @@ export default abstract class BaseProvider<
212216
return new Promise((resolve) => setTimeout(resolve, ms));
213217
}
214218

219+
/**
220+
* Maximum number of retries for transient errors
221+
*/
222+
protected readonly MAX_RETRIES = 3;
223+
224+
/**
225+
* Base delay for exponential backoff (in milliseconds)
226+
*/
227+
protected readonly BASE_RETRY_DELAY_MS = 2000;
228+
229+
/**
230+
* Executes an async operation with automatic retry for transient errors.
231+
* Uses exponential backoff between retries.
232+
*
233+
* @param operation - Description of the operation (for logging)
234+
* @param fn - Async function to execute
235+
* @returns The result of the function
236+
* @throws The last error if all retries fail
237+
*/
238+
protected async withRetry<T>(
239+
operation: string,
240+
fn: () => Promise<T>,
241+
): Promise<T> {
242+
let lastError: unknown;
243+
244+
for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {
245+
try {
246+
return await fn();
247+
} catch (error) {
248+
lastError = error;
249+
250+
// Check if this is a retryable error
251+
const isRetryable =
252+
axios.isAxiosError(error) && isRetryableError(error);
253+
254+
// Don't retry non-retryable errors or on the last attempt
255+
if (!isRetryable || attempt === this.MAX_RETRIES) {
256+
break;
257+
}
258+
259+
// Calculate delay with exponential backoff: 2s, 4s, 8s
260+
const delay = this.BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
261+
262+
if (!this.options.quiet) {
263+
const statusCode = axios.isAxiosError(error)
264+
? error.response?.status
265+
: undefined;
266+
logger.warn(
267+
`${operation} failed${statusCode ? ` (HTTP ${statusCode})` : ''}, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${this.MAX_RETRIES})`,
268+
);
269+
}
270+
271+
await this.sleep(delay);
272+
}
273+
}
274+
275+
// All retries failed, throw the error
276+
throw lastError;
277+
}
278+
215279
/**
216280
* Extracts an error message from various error types.
217281
* For Axios errors, uses enhanced error handling with diagnostics.

src/providers/espresso.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -243,22 +243,24 @@ export default class Espresso extends BaseProvider<EspressoOptions> {
243243

244244
private async getStatus(): Promise<EspressoStatusResponse> {
245245
try {
246-
const response = await axios.get(`${this.URL}/${this.appId}`, {
247-
headers: {
248-
'User-Agent': utils.getUserAgent(),
249-
},
250-
auth: {
251-
username: this.credentials.userName,
252-
password: this.credentials.accessKey,
253-
},
254-
timeout: 30000, // 30 second timeout
255-
});
246+
return await this.withRetry('Getting Espresso test status', async () => {
247+
const response = await axios.get(`${this.URL}/${this.appId}`, {
248+
headers: {
249+
'User-Agent': utils.getUserAgent(),
250+
},
251+
auth: {
252+
username: this.credentials.userName,
253+
password: this.credentials.accessKey,
254+
},
255+
timeout: 30000, // 30 second timeout
256+
});
256257

257-
// Check for version update notification
258-
const latestVersion = response.headers?.['x-testingbotctl-version'];
259-
utils.checkForUpdate(latestVersion);
258+
// Check for version update notification
259+
const latestVersion = response.headers?.['x-testingbotctl-version'];
260+
utils.checkForUpdate(latestVersion);
260261

261-
return response.data;
262+
return response.data;
263+
});
262264
} catch (error) {
263265
throw await this.handleErrorWithDiagnostics(
264266
error,

src/providers/maestro.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -897,21 +897,23 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
897897

898898
private async getStatus(): Promise<MaestroStatusResponse> {
899899
try {
900-
const response = await axios.get(`${this.URL}/${this.appId}`, {
901-
headers: {
902-
'User-Agent': utils.getUserAgent(),
903-
},
904-
auth: {
905-
username: this.credentials.userName,
906-
password: this.credentials.accessKey,
907-
},
908-
timeout: 30000, // 30 second timeout
909-
});
900+
return await this.withRetry('Getting Maestro test status', async () => {
901+
const response = await axios.get(`${this.URL}/${this.appId}`, {
902+
headers: {
903+
'User-Agent': utils.getUserAgent(),
904+
},
905+
auth: {
906+
username: this.credentials.userName,
907+
password: this.credentials.accessKey,
908+
},
909+
timeout: 30000, // 30 second timeout
910+
});
910911

911-
// Check for version update notification
912-
const latestVersion = response.headers?.['x-testingbotctl-version'];
913-
utils.checkForUpdate(latestVersion);
914-
return response.data;
912+
// Check for version update notification
913+
const latestVersion = response.headers?.['x-testingbotctl-version'];
914+
utils.checkForUpdate(latestVersion);
915+
return response.data;
916+
});
915917
} catch (error) {
916918
throw await this.handleErrorWithDiagnostics(
917919
error,
@@ -1465,21 +1467,29 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
14651467

14661468
private async getRunDetails(runId: number): Promise<MaestroRunDetails> {
14671469
try {
1468-
const response = await axios.get(`${this.URL}/${this.appId}/${runId}`, {
1469-
headers: {
1470-
'User-Agent': utils.getUserAgent(),
1471-
},
1472-
auth: {
1473-
username: this.credentials.userName,
1474-
password: this.credentials.accessKey,
1475-
},
1476-
timeout: 30000, // 30 second timeout
1477-
});
1470+
return await this.withRetry(
1471+
`Getting run details for run ${runId}`,
1472+
async () => {
1473+
const response = await axios.get(
1474+
`${this.URL}/${this.appId}/${runId}`,
1475+
{
1476+
headers: {
1477+
'User-Agent': utils.getUserAgent(),
1478+
},
1479+
auth: {
1480+
username: this.credentials.userName,
1481+
password: this.credentials.accessKey,
1482+
},
1483+
timeout: 30000, // 30 second timeout
1484+
},
1485+
);
14781486

1479-
const latestVersion = response.headers?.['x-testingbotctl-version'];
1480-
utils.checkForUpdate(latestVersion);
1487+
const latestVersion = response.headers?.['x-testingbotctl-version'];
1488+
utils.checkForUpdate(latestVersion);
14811489

1482-
return response.data;
1490+
return response.data;
1491+
},
1492+
);
14831493
} catch (error) {
14841494
throw await this.handleErrorWithDiagnostics(
14851495
error,

src/providers/xcuitest.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -243,22 +243,24 @@ export default class XCUITest extends BaseProvider<XCUITestOptions> {
243243

244244
private async getStatus(): Promise<XCUITestStatusResponse> {
245245
try {
246-
const response = await axios.get(`${this.URL}/${this.appId}`, {
247-
headers: {
248-
'User-Agent': utils.getUserAgent(),
249-
},
250-
auth: {
251-
username: this.credentials.userName,
252-
password: this.credentials.accessKey,
253-
},
254-
timeout: 30000, // 30 second timeout
255-
});
246+
return await this.withRetry('Getting XCUITest status', async () => {
247+
const response = await axios.get(`${this.URL}/${this.appId}`, {
248+
headers: {
249+
'User-Agent': utils.getUserAgent(),
250+
},
251+
auth: {
252+
username: this.credentials.userName,
253+
password: this.credentials.accessKey,
254+
},
255+
timeout: 30000, // 30 second timeout
256+
});
256257

257-
// Check for version update notification
258-
const latestVersion = response.headers?.['x-testingbotctl-version'];
259-
utils.checkForUpdate(latestVersion);
258+
// Check for version update notification
259+
const latestVersion = response.headers?.['x-testingbotctl-version'];
260+
utils.checkForUpdate(latestVersion);
260261

261-
return response.data;
262+
return response.data;
263+
});
262264
} catch (error) {
263265
throw await this.handleErrorWithDiagnostics(
264266
error,

0 commit comments

Comments
 (0)