Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry api call #1691

Merged
merged 14 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/cuddly-roses-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@api3/airnode-utilities': minor
'@api3/airnode-validator': minor
'@api3/airnode-deployer': minor
'@api3/airnode-adapter': minor
'@api3/airnode-admin': minor
'@api3/airnode-node': minor
---

update promise-utils to v0.4.0
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
},
"dependencies": {},
"devDependencies": {
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.0",
"@octokit/core": "^4.2.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/airnode-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@api3/ois": "2.0.0",
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"axios": "^1.3.4",
"bignumber.js": "^9.1.1",
"ethers": "^5.7.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/airnode-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@api3/airnode-protocol": "^0.10.0",
"@api3/airnode-utilities": "^0.10.0",
"@api3/airnode-validator": "^0.10.0",
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"yargs": "^17.7.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/airnode-deployer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@api3/airnode-protocol": "^0.10.0",
"@api3/airnode-utilities": "^0.10.0",
"@api3/airnode-validator": "^0.10.0",
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"@aws-sdk/client-s3": "^3.295.0",
"@aws-sdk/signature-v4-crt": "^3.295.0",
"@google-cloud/storage": "^6.9.4",
Expand Down
2 changes: 1 addition & 1 deletion packages/airnode-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@api3/airnode-utilities": "^0.10.0",
"@api3/airnode-validator": "^0.10.0",
"@api3/ois": "2.0.0",
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"@aws-sdk/client-lambda": "^3.295.0",
"date-fns": "^2.29.3",
"dotenv": "^16.0.3",
Expand Down
77 changes: 68 additions & 9 deletions packages/airnode-node/src/api/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AxiosError, AxiosHeaders } from 'axios';
import * as fixtures from '../../test/fixtures';
import { getExpectedTemplateIdV0 } from '../evm/templates';
import { ApiCallErrorResponse, RequestErrorMessage } from '../types';
import { API_CALL_TIMEOUT } from '../constants';
import { FIRST_API_CALL_TIMEOUT, SECOND_API_CALL_TIMEOUT } from '../constants';
import { callApi, verifyTemplateId } from '.';

describe('callApi', () => {
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('callApi', () => {
},
],
},
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});

Expand Down Expand Up @@ -108,7 +108,7 @@ describe('callApi', () => {
},
],
},
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});

Expand Down Expand Up @@ -146,7 +146,7 @@ describe('callApi', () => {
},
],
},
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});

Expand Down Expand Up @@ -197,7 +197,7 @@ describe('callApi', () => {
},
],
},
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});

Expand All @@ -216,15 +216,73 @@ describe('callApi', () => {
expect.objectContaining({
parameters: { from: 'ETH', amount: '1' },
}),
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});

it('retries the API call if the first attempt fails', async () => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
spy.mockRejectedValueOnce(new Error('First attempt failed'));
spy.mockResolvedValueOnce({ data: { price: 1000 } });
const requestedGasPrice = '100000000';
const requestedMinConfirmations = '0';
const parameters = {
_type: 'int256',
_path: 'price',
from: 'ETH',
_gasPrice: requestedGasPrice,
_minConfirmations: requestedMinConfirmations,
};

const [logs, res] = await callApi({
type: 'regular',
config: fixtures.buildConfig(),
aggregatedApiCall: fixtures.buildAggregatedRegularApiCall({ parameters }),
});

expect(logs).toEqual([]);
expect(res).toEqual({
success: true,
data: {
encodedValue: '0x0000000000000000000000000000000000000000000000000000000005f5e100',
signature:
'0xe92f5ee40ddb5aa42cab65fcdc025008b2bc026af80a7c93a9aac4e474f8a88f4f2bd861b9cf9a2b050bf0fd13e9714c4575cebbea658d7501e98c0963a5a38b1c',
},
// _minConfirmations is processed before making API calls
reservedParameterOverrides: {
gasPrice: requestedGasPrice,
},
});
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith(
{
endpointName: 'convertToUSD',
ois: fixtures.buildOIS(),
parameters: { from: 'ETH', amount: '1' },
metadata: {
chainId: '31337',
chainType: 'evm',
requestId: '0xf40127616f09d41b20891bcfd326957a0e3d5a5ecf659cff4d8106c04b024374',
requesterAddress: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
sponsorAddress: '0x2479808b1216E998309A727df8A0A98A1130A162',
sponsorWalletAddress: '0x1C1CEEF1a887eDeAB20219889971e1fd4645b55D',
},
apiCredentials: [
{
securitySchemeName: 'myApiSecurityScheme',
securitySchemeValue: 'supersecret',
},
],
},
{ timeout: SECOND_API_CALL_TIMEOUT }
);
});

it('returns an error if the API call fails to execute', async () => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
const nonAxiosError = new Error('A non-axios error');
spy.mockRejectedValueOnce(nonAxiosError);

spy.mockRejectedValueOnce(nonAxiosError);
const parameters = { _type: 'int256', _path: 'unknown', from: 'ETH' };
const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters });
const [logs, res] = await callApi({ type: 'regular', config: fixtures.buildConfig(), aggregatedApiCall });
Expand Down Expand Up @@ -256,6 +314,7 @@ describe('callApi', () => {
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as any;
const axiosError = e;
spy.mockRejectedValueOnce(axiosError);
spy.mockRejectedValueOnce(axiosError);

const parameters = { _type: 'int256', _path: 'unknown', from: 'ETH' };
const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters });
Expand Down Expand Up @@ -388,7 +447,7 @@ describe('callApi', () => {
expect.objectContaining({
parameters: { from: 'BTC', source: 'airnode', amount: '1' },
}),
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});

Expand Down Expand Up @@ -432,7 +491,7 @@ describe('callApi', () => {
expect.objectContaining({
parameters: { from: 'ETH', amount: '1' },
}),
{ timeout: API_CALL_TIMEOUT }
{ timeout: FIRST_API_CALL_TIMEOUT }
);
});
});
Expand Down
34 changes: 20 additions & 14 deletions packages/airnode-node/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import compact from 'lodash/compact';
import { postProcessApiSpecifications, preProcessApiSpecifications } from './processing';
import { getAirnodeWalletFromPrivateKey, deriveSponsorWalletFromMnemonic } from '../evm';
import { getReservedParameters } from '../adapters/http/parameters';
import { API_CALL_TIMEOUT } from '../constants';
import { FIRST_API_CALL_TIMEOUT, SECOND_API_CALL_TIMEOUT } from '../constants';
import { isValidRequestId } from '../evm/verification';
import { getExpectedTemplateIdV0, getExpectedTemplateIdV1 } from '../evm/templates';
import {
Expand Down Expand Up @@ -201,21 +201,27 @@ export async function performApiCall(
payload: ApiCallPayload
): Promise<LogsData<ApiCallErrorResponse | PerformApiCallSuccess>> {
const options = buildOptions(payload);
const timeout = API_CALL_TIMEOUT;
// We also pass the timeout to adapter to gracefully abort the request after the timeout
const goRes = await go(() => adapter.buildAndExecuteRequest(options, { timeout }), {
totalTimeoutMs: timeout,
// We also pass the timeout to adapter to gracefully abort the request after the timeout.
// timeout passed to adapter will cause axios socket to hang until the timeout is reached
Ashar2shahid marked this conversation as resolved.
Show resolved Hide resolved
Ashar2shahid marked this conversation as resolved.
Show resolved Hide resolved
// even if the totalTimeoutMs is reached and the 2nd attempt is made
const goAttempt1 = await go(() => adapter.buildAndExecuteRequest(options, { timeout: FIRST_API_CALL_TIMEOUT }), {
totalTimeoutMs: FIRST_API_CALL_TIMEOUT,
});
if (!goRes.success) {
const { aggregatedApiCall } = payload;
const log = logger.pend('ERROR', `Failed to call Endpoint:${aggregatedApiCall.endpointName}`, goRes.error);
// eslint-disable-next-line import/no-named-as-default-member
const axiosErrorMsg = axios.isAxiosError(goRes.error) ? errorMsgFromAxiosError(goRes.error) : '';
const errorMessage = compact([RequestErrorMessage.ApiCallFailed, axiosErrorMsg]).join(' ');
return [[log], { success: false, errorMessage: errorMessage }];
if (goAttempt1.success) {
return [[], { ...goAttempt1.data }];
}

return [[], { ...goRes.data }];
const goAttempt2 = await go(() => adapter.buildAndExecuteRequest(options, { timeout: SECOND_API_CALL_TIMEOUT }), {
totalTimeoutMs: SECOND_API_CALL_TIMEOUT,
});
if (goAttempt2.success) {
return [[], { ...goAttempt2.data }];
}
const { aggregatedApiCall } = payload;
const log = logger.pend('ERROR', `Failed to call Endpoint:${aggregatedApiCall.endpointName}`, goAttempt2.error);
// eslint-disable-next-line import/no-named-as-default-member
const axiosErrorMsg = axios.isAxiosError(goAttempt2.error) ? errorMsgFromAxiosError(goAttempt2.error) : '';
const errorMessage = compact([RequestErrorMessage.ApiCallFailed, axiosErrorMsg]).join(' ');
return [[log], { success: false, errorMessage: errorMessage }];
}

export async function processSuccessfulApiCall(
Expand Down
5 changes: 4 additions & 1 deletion packages/airnode-node/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// The maximum time a single API call has before it is timed out
export const API_CALL_TIMEOUT = 30_000;
export const FIRST_API_CALL_TIMEOUT = 10_000;

// The maximum time a single API call has before it is timed out in the second attempt
export const SECOND_API_CALL_TIMEOUT = 20_000;

// The number of past blocks to lookup when fetching Airnode RRP events.
export const BLOCK_COUNT_HISTORY_LIMIT = 300;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('callApis', () => {
jest.spyOn(validator, 'unsafeParseConfigWithSecrets').mockReturnValue(config);
const spy = jest.spyOn(adapter, 'buildAndExecuteRequest') as jest.SpyInstance;
spy.mockRejectedValueOnce(new Error('Unexpected error'));
spy.mockRejectedValueOnce(new Error('Unexpected error'));
const parameters = { _type: 'int256', _path: 'prices.1' };
const aggregatedApiCall = fixtures.buildAggregatedRegularApiCall({ parameters });
const workerOpts = fixtures.buildWorkerOptions();
Expand All @@ -116,8 +117,8 @@ describe('callApis', () => {
errorMessage: `${RequestErrorMessage.ApiCallFailed}`,
},
]);
expect(spy).toHaveBeenCalledTimes(1);
});
expect(spy).toHaveBeenCalledTimes(2);
}, 35000);

it('returns an error if the worker crashes', async () => {
const spy = jest.spyOn(workers, 'spawn');
Expand Down
2 changes: 1 addition & 1 deletion packages/airnode-node/test/e2e/signed-data.feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ it('makes a call for signed API data', async () => {
// Verify that all internal parameters have been removed from the parameters forwarded to the API
expect(adapter.buildAndExecuteRequest).toHaveBeenCalledWith(
expect.objectContaining({ parameters: { from: 'ETH', amount: '1' } }),
{ timeout: 30000 }
{ timeout: 10000 }
);
});
2 changes: 1 addition & 1 deletion packages/airnode-utilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"main": "dist/index.js",
"dependencies": {
"@api3/airnode-validator": "^0.10.0",
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"date-fns": "^2.29.3",
"ethers": "^5.7.2"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/airnode-validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"dependencies": {
"@api3/ois": "2.0.0",
"@api3/promise-utils": "^0.3.0",
"@api3/promise-utils": "^0.4.0",
"dotenv": "^16.0.3",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
lodash "^4.17.21"
zod "^3.20.6"

"@api3/promise-utils@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@api3/promise-utils/-/promise-utils-0.3.0.tgz#e7ebf92bfd8c1d39983321fc5445070c51fce176"
integrity sha512-fH3CzEcsCQjoX6BZ5M+3yRIXZ2zz4/nFdzKUB4wvn3KjvvzvroHFZrzhbKa4mB9E4AS0xnou1AXhlrnN5Fcy+A==
"@api3/promise-utils@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@api3/promise-utils/-/promise-utils-0.4.0.tgz#d1dcd77d74377b4fdb3071d2cc76d98c9151309c"
integrity sha512-+8fcNjjQeQAuuSXFwu8PMZcYzjwjDiGYcMUfAQ0lpREb1zHonwWZ2N0B9h/g1cvWzg9YhElbeb/SyhCrNm+b/A==

"@aws-crypto/[email protected]":
version "3.0.0"
Expand Down