From aadec0dd903485383a4313c1f19b3318a1815910 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Tue, 7 Feb 2023 21:30:04 +0400 Subject: [PATCH] feat(node): exponential retry of requests on failure --- src/Node.ts | 3 ++- src/utils/autorest.ts | 29 ++++++++++++++++++++++++++++- test/integration/node.ts | 13 +++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Node.ts b/src/Node.ts index c4f8f45b32..3fdb03b83d 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js'; import { OperationArguments, OperationSpec } from '@azure/core-client'; import { genRequestQueuesPolicy, genCombineGetRequestsPolicy, genErrorFormatterPolicy, - genVersionCheckPolicy, + genVersionCheckPolicy, genRetryOnFailurePolicy, } from './utils/autorest'; import { Node as NodeApi, NodeOptionalParams, ErrorModel } from './apis/node'; import { mapObject } from './utils/other'; @@ -128,6 +128,7 @@ export default class Node extends (NodeTransformed as unknown as NodeTransformed additionalPolicies: [ genRequestQueuesPolicy(), genCombineGetRequestsPolicy(), + genRetryOnFailurePolicy(), genErrorFormatterPolicy((body: ErrorModel) => ` ${body.reason}`), ], ...options, diff --git a/src/utils/autorest.ts b/src/utils/autorest.ts index e60f6f4e83..e4fec97639 100644 --- a/src/utils/autorest.ts +++ b/src/utils/autorest.ts @@ -2,7 +2,7 @@ import { RestError, PipelineResponse, PipelinePolicy } from '@azure/core-rest-pi import { AdditionalPolicyConfig } from '@azure/core-client'; import { pause } from './other'; import semverSatisfies from './semver-satisfies'; -import { UnsupportedVersionError } from './errors'; +import { UnexpectedTsError, UnsupportedVersionError } from './errors'; export const genRequestQueuesPolicy = (): AdditionalPolicyConfig => { const requestQueues = new Map>(); @@ -106,3 +106,30 @@ export const genVersionCheckPolicy = ( return next(request); }, }); + +export const genRetryOnFailurePolicy = (): AdditionalPolicyConfig => ({ + policy: { + name: 'retry-on-failure', + async sendRequest(request, next) { + const statusesToNotRetry = [200, 400, 403]; + let error: Error | undefined; + for (let attempt = 0; attempt <= 3; attempt += 1) { + if (error != null) { + if ( + !(error instanceof RestError) + || statusesToNotRetry.includes(error.response?.status ?? 0) + ) throw error; + await pause((attempt / 3) ** 2 * 500); + } + try { + return await next(request); + } catch (e) { + error = e; + } + } + if (error == null) throw new UnexpectedTsError(); + throw error; + }, + }, + position: 'perCall', +}); diff --git a/test/integration/node.ts b/test/integration/node.ts index 1babf90ac8..4f49035c43 100644 --- a/test/integration/node.ts +++ b/test/integration/node.ts @@ -17,6 +17,8 @@ import { describe, it, before } from 'mocha'; import { expect } from 'chai'; +import { spy } from 'sinon'; +import http from 'http'; import { url, ignoreVersion } from '.'; import { AeSdkBase, Node, NodeNotFoundError } from '../../src'; @@ -43,6 +45,17 @@ describe('Node client', () => { .to.be.rejectedWith('v3/transactions/th_test error: Invalid hash'); }); + it('retries requests if failed', async () => ([ + ['ak_test', 1], + ['ak_2CxRaRcMUGn9s5UwN36UhdrtZVFUbgG1BSX5tUAyQbCNneUwti', 4], + ] as const).reduce(async (prev, [address, requestCount]) => { + await prev; + const httpSpy = spy(http, 'request'); + await node.getAccountByPubkey(address).catch(() => {}); + expect(httpSpy.callCount).to.be.equal(requestCount); + httpSpy.restore(); + }, Promise.resolve())); + describe('Node Pool', () => { it('Throw error on using API without node', () => { const nodes = new AeSdkBase({});