Skip to content

Commit 9b22d7d

Browse files
HantingZhang2ci.datadog-api-spectherve
authored
Add retry support (#1293)
* add configs * Implement retry and refactor fetch * retry test + sleep mock * pre-commit fixes * Test sleep time * Revert "Merge branch 'hzhang/add-retry' of github.com:DataDog/datadog-api-client-typescript into hzhang/add-retry" This reverts commit 61c0f17, reversing changes made to 11fc11f. * add test for backoffbase validation * change generator * pre-commit fixes * AddPropertyBack * pre-commit fixes * add params to constructor * Add nock to license * add backoff base validation * Update Readme * changes * pre-commit fixes * refactor execute request * pre-commit fixes * add () * pre-commit fixes * simplifie * pre-commit fixes --------- Co-authored-by: ci.datadog-api-spec <[email protected]> Co-authored-by: Thomas Hervé <[email protected]>
1 parent a13998b commit 9b22d7d

File tree

12 files changed

+385
-90
lines changed

12 files changed

+385
-90
lines changed

.generator/src/generator/templates/configuration.j2

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export class Configuration {
1212
readonly authMethods: AuthMethods;
1313
readonly httpConfig: HttpConfiguration;
1414
readonly debug: boolean | undefined;
15+
readonly enableRetry: boolean | undefined;
16+
readonly maxRetries: number | undefined;
17+
readonly backoffBase: number | undefined;
18+
readonly backoffMultiplier: number | undefined;
1519
unstableOperations: { [name: string]: boolean };
1620
servers: BaseServerConfiguration[];
1721
operationServers: { [endpoint: string]: BaseServerConfiguration[] };
@@ -24,6 +28,10 @@ export class Configuration {
2428
authMethods: AuthMethods,
2529
httpConfig: HttpConfiguration,
2630
debug: boolean | undefined,
31+
enableRetry: boolean | undefined,
32+
maxRetries: number | undefined,
33+
backoffBase: number | undefined,
34+
backoffMultiplier: number | undefined,
2735
unstableOperations: { [name: string]: boolean },
2836
) {
2937
this.baseServer = baseServer;
@@ -33,6 +41,10 @@ export class Configuration {
3341
this.authMethods = authMethods;
3442
this.httpConfig = httpConfig;
3543
this.debug = debug;
44+
this.enableRetry= enableRetry;
45+
this.maxRetries = maxRetries;
46+
this.backoffBase = backoffBase;
47+
this.backoffMultiplier = backoffMultiplier;
3648
this.unstableOperations = unstableOperations;
3749
this.servers = [];
3850
for (const server of servers) {
@@ -45,6 +57,9 @@ export class Configuration {
4557
this.operationServers[endpoint].push(server.clone());
4658
}
4759
}
60+
if (backoffBase && backoffBase < 2) {
61+
throw new Error("Backoff base must be at least 2");
62+
}
4863
}
4964

5065
setServerVariables(serverVariables: { [key: string]: string }): void {
@@ -111,6 +126,22 @@ export interface ConfigurationParameters {
111126
* Callback method to compress string body with zstd
112127
*/
113128
zstdCompressorCallback?: ZstdCompressorCallback
129+
/**
130+
* Maximum of retry attempts allowed
131+
*/
132+
maxRetries?: number;
133+
/**
134+
* Backoff base
135+
*/
136+
backoffBase?: number;
137+
/**
138+
* Backoff multiplier
139+
*/
140+
backoffMultiplier?: number;
141+
/**
142+
* Enable retry on status code 429 or 5xx
143+
*/
144+
enableRetry?: boolean;
114145
}
115146

116147
/**
@@ -153,6 +184,10 @@ export function createConfiguration(conf: ConfigurationParameters = {}): Configu
153184
configureAuthMethods(authMethods),
154185
conf.httpConfig || {},
155186
conf.debug,
187+
conf.enableRetry || false,
188+
conf.maxRetries || 3,
189+
conf.backoffBase || 2,
190+
conf.backoffMultiplier || 2,
156191
{
157192
{%- for version, api in apis.items() %}
158193
{%- for operations in api.values() %}
@@ -167,6 +202,10 @@ export function createConfiguration(conf: ConfigurationParameters = {}): Configu
167202
);
168203
configuration.httpApi.zstdCompressorCallback = conf.zstdCompressorCallback
169204
configuration.httpApi.debug = configuration.debug;
205+
configuration.httpApi.enableRetry = configuration.enableRetry;
206+
configuration.httpApi.maxRetries = configuration.maxRetries;
207+
configuration.httpApi.backoffBase = configuration.backoffBase;
208+
configuration.httpApi.backoffMultiplier = configuration.backoffMultiplier;
170209
return configuration;
171210
}
172211

.generator/src/generator/templates/http/http.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ export class ResponseContext {
244244
export type ZstdCompressorCallback = (body: string) => Buffer;
245245

246246
export interface HttpLibrary {
247+
enableRetry?: boolean | undefined;
248+
maxRetries?: number;
249+
backoffBase?: number;
250+
backoffMultiplier?: number;
247251
debug?: boolean;
248252
zstdCompressorCallback?: ZstdCompressorCallback;
249253
send(request: RequestContext): Promise<ResponseContext>;

.generator/src/generator/templates/http/isomorphic-fetch.j2

Lines changed: 82 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { isBrowser, isNode } from "../util";
77
export class IsomorphicFetchHttpLibrary implements HttpLibrary {
88
public debug = false;
99
public zstdCompressorCallback: ZstdCompressorCallback | undefined;
10+
public enableRetry!: boolean;
11+
public maxRetries!: number ;
12+
public backoffBase!: number ;
13+
public backoffMultiplier!: number;
1014

1115
public send(request: RequestContext): Promise<ResponseContext> {
1216
if (this.debug) {
@@ -46,60 +50,94 @@ export class IsomorphicFetchHttpLibrary implements HttpLibrary {
4650
}
4751
}
4852
}
53+
54+
return this.executeRequest(request,0,headers);
55+
}
4956

50-
let resultPromise: Promise<ResponseContext>;
51-
57+
private async executeRequest(
58+
request: RequestContext,
59+
currentAttempt: number,
60+
headers: {[key: string]: string}
61+
): Promise<ResponseContext> {
5262
// On non-node environments, use native fetch if available.
53-
// `cross-fetch` incorrectly assumes all browsers have XHR available.
63+
// `cross-fetch` incorrectly assumes all browsers have XHR available.
5464
// See https://github.com/lquixada/cross-fetch/issues/78
5565
// TODO: Remove once once above issue is resolved.
56-
if (!isNode && typeof fetch === "function") {
57-
resultPromise = fetch(request.getUrl(), {
58-
method: method,
59-
body: body as any,
60-
headers: headers,
61-
signal: request.getHttpConfig().signal,
62-
}).then((resp: any) => {
63-
const headers: { [name: string]: string } = {};
66+
const fetchFunction =!isNode && typeof fetch === "function" ? fetch : crossFetch;
67+
const fetchOptions = {
68+
method: request.getHttpMethod().toString(),
69+
body: request.getBody() as any,
70+
headers: headers,
71+
signal: request.getHttpConfig().signal,
72+
}
73+
try {
74+
const resp = await fetchFunction(request.getUrl(),fetchOptions);
75+
const responseHeaders: { [name: string]: string } = {};
6476
resp.headers.forEach((value: string, name: string) => {
65-
headers[name] = value;
77+
responseHeaders[name] = value;
6678
});
6779

68-
const body = {
69-
text: () => resp.text(),
70-
binary: () => resp.buffer(),
71-
};
72-
const response = new ResponseContext(resp.status, headers, body);
73-
if (this.debug) {
74-
this.logResponse(response);
75-
}
76-
return response;
77-
});
78-
} else {
79-
resultPromise = crossFetch(request.getUrl(), {
80-
method: method,
81-
body: body as any,
82-
headers: headers,
83-
signal: request.getHttpConfig().signal,
84-
}).then((resp: any) => {
85-
const headers: { [name: string]: string } = {};
86-
resp.headers.forEach((value: string, name: string) => {
87-
headers[name] = value;
88-
});
80+
const responseBody = {
81+
text: () => resp.text(),
82+
binary: async () => {
83+
const arrayBuffer = await resp.arrayBuffer();
84+
return Buffer.from(arrayBuffer);
85+
},
86+
};
8987

90-
const body = {
91-
text: () => resp.text(),
92-
binary: () => resp.buffer(),
93-
};
94-
const response = new ResponseContext(resp.status, headers, body);
95-
if (this.debug) {
96-
this.logResponse(response);
97-
}
98-
return response;
99-
});
88+
const response = new ResponseContext(
89+
resp.status,
90+
responseHeaders,
91+
responseBody
92+
);
93+
94+
if (this.debug) {
95+
this.logResponse(response);
96+
}
97+
98+
if (
99+
this.shouldRetry(
100+
this.enableRetry,
101+
currentAttempt,
102+
this.maxRetries,
103+
response.httpStatusCode
104+
)
105+
) {
106+
const delay = this.calculateRetryInterval(
107+
currentAttempt,
108+
this.backoffBase,
109+
this.backoffMultiplier,
110+
responseHeaders
111+
);
112+
currentAttempt++;
113+
await this.sleep(delay * 1000);
114+
return this.executeRequest(request, currentAttempt, headers);
115+
}
116+
return response;
117+
} catch (error) {
118+
console.error("An error occurred during the HTTP request:", error);
119+
throw error;
100120
}
121+
}
122+
123+
private sleep(milliseconds: number): Promise<void> {
124+
return new Promise((resolve) => {
125+
setTimeout(resolve, milliseconds);
126+
});
127+
}
128+
129+
private shouldRetry(enableRetry:boolean, currentAttempt:number, maxRetries:number, responseCode:number):boolean{
130+
return (responseCode == 429 || responseCode >=500 ) && maxRetries>currentAttempt && enableRetry
131+
}
101132

102-
return resultPromise;
133+
private calculateRetryInterval(currentAttempt:number, backoffBase:number, backoffMultiplier:number, headers: {[name: string]: string}) : number{
134+
if ("x-ratelimit-reset" in headers) {
135+
const rateLimitHeaderString = headers["x-ratelimit-reset"]
136+
const retryIntervalFromHeader = parseInt(rateLimitHeaderString,10);
137+
return retryIntervalFromHeader
138+
} else {
139+
return (backoffMultiplier ** currentAttempt) * backoffBase
140+
}
103141
}
104142

105143
private logRequest(request: RequestContext): void {

LICENSE-3rdparty.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ eslint-plugin-unused-imports,MIT,Copyright (c) 2021 Mikkel Holmer
2626
eslint,MIT,Copyright JS Foundation and other contributors, https://js.foundation
2727
form-data,MIT,Copyright (c) 2012 Felix Geisendörfer ([email protected]) and contributors
2828
pako,MIT,Copyright (C) 2014-2017 Vitaly Puzrin and Andrei Tuputcyn
29+
nock,MIT,Copyright (c) 2011-2019 Pedro Teixeira and other contributors
2930
prettier,MIT,Copyright © James Long and contributors
3031
ts-node,MIT,Copyright (c) 2014 Blake Embrey ([email protected])
3132
typedoc,Apache-2.0,Copyright (c) 2015 Sebastian Lenz Copyright (c) 2016-2021 TypeDoc Contributors.

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,29 @@ const configurationOpts = {
108108
const configuration = client.createConfiguration(configurationOpts);
109109
```
110110

111+
### Enable retry
112+
113+
To enable the client to retry when rate limited (status 429) or status 500 and above:
114+
115+
```typescript
116+
import { client } from '@datadog/datadog-api-client';
117+
const configurationOpts = {
118+
enableRetry: true
119+
};
120+
121+
const configuration = client.createConfiguration(configurationOpts);
122+
```
123+
The interval between 2 retry attempts will be the value of the x-ratelimit-reset response header when available. If not, it will be :
124+
125+
```typescript
126+
(backoffMultiplier ** current_retry_count) * backoffBase
127+
```
128+
The maximum number of retry attempts is 3 by default and can be modified with
129+
130+
```typescript
131+
maxRetries
132+
```
133+
111134
### Adding timeout to requests
112135

113136
To add timeout or other mechanism to cancel requests, you need an abort

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"eslint-plugin-node": "^11.1.0",
9898
"eslint-plugin-unused-imports": "^2.0.0",
9999
"jest": "^29.5.0",
100+
"nock": "^13.3.3",
100101
"prettier": "^3.0.0",
101102
"ts-jest": "^29.1.0",
102103
"ts-node": "^10.9.1",

packages/datadog-api-client-common/configuration.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export class Configuration {
2626
readonly authMethods: AuthMethods;
2727
readonly httpConfig: HttpConfiguration;
2828
readonly debug: boolean | undefined;
29+
readonly enableRetry: boolean | undefined;
30+
readonly maxRetries: number | undefined;
31+
readonly backoffBase: number | undefined;
32+
readonly backoffMultiplier: number | undefined;
2933
unstableOperations: { [name: string]: boolean };
3034
servers: BaseServerConfiguration[];
3135
operationServers: { [endpoint: string]: BaseServerConfiguration[] };
@@ -38,6 +42,10 @@ export class Configuration {
3842
authMethods: AuthMethods,
3943
httpConfig: HttpConfiguration,
4044
debug: boolean | undefined,
45+
enableRetry: boolean | undefined,
46+
maxRetries: number | undefined,
47+
backoffBase: number | undefined,
48+
backoffMultiplier: number | undefined,
4149
unstableOperations: { [name: string]: boolean }
4250
) {
4351
this.baseServer = baseServer;
@@ -47,6 +55,10 @@ export class Configuration {
4755
this.authMethods = authMethods;
4856
this.httpConfig = httpConfig;
4957
this.debug = debug;
58+
this.enableRetry = enableRetry;
59+
this.maxRetries = maxRetries;
60+
this.backoffBase = backoffBase;
61+
this.backoffMultiplier = backoffMultiplier;
5062
this.unstableOperations = unstableOperations;
5163
this.servers = [];
5264
for (const server of servers) {
@@ -59,6 +71,9 @@ export class Configuration {
5971
this.operationServers[endpoint].push(server.clone());
6072
}
6173
}
74+
if (backoffBase && backoffBase < 2) {
75+
throw new Error("Backoff base must be at least 2");
76+
}
6277
}
6378

6479
setServerVariables(serverVariables: { [key: string]: string }): void {
@@ -125,6 +140,22 @@ export interface ConfigurationParameters {
125140
* Callback method to compress string body with zstd
126141
*/
127142
zstdCompressorCallback?: ZstdCompressorCallback;
143+
/**
144+
* Maximum of retry attempts allowed
145+
*/
146+
maxRetries?: number;
147+
/**
148+
* Backoff base
149+
*/
150+
backoffBase?: number;
151+
/**
152+
* Backoff multiplier
153+
*/
154+
backoffMultiplier?: number;
155+
/**
156+
* Enable retry on status code 429 or 5xx
157+
*/
158+
enableRetry?: boolean;
128159
}
129160

130161
/**
@@ -178,6 +209,10 @@ export function createConfiguration(
178209
configureAuthMethods(authMethods),
179210
conf.httpConfig || {},
180211
conf.debug,
212+
conf.enableRetry || false,
213+
conf.maxRetries || 3,
214+
conf.backoffBase || 2,
215+
conf.backoffMultiplier || 2,
181216
{
182217
"v2.createCIAppPipelineEvent": false,
183218
"v2.cancelDowntime": false,
@@ -225,6 +260,10 @@ export function createConfiguration(
225260
);
226261
configuration.httpApi.zstdCompressorCallback = conf.zstdCompressorCallback;
227262
configuration.httpApi.debug = configuration.debug;
263+
configuration.httpApi.enableRetry = configuration.enableRetry;
264+
configuration.httpApi.maxRetries = configuration.maxRetries;
265+
configuration.httpApi.backoffBase = configuration.backoffBase;
266+
configuration.httpApi.backoffMultiplier = configuration.backoffMultiplier;
228267
return configuration;
229268
}
230269

packages/datadog-api-client-common/http/http.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ export class ResponseContext {
247247
export type ZstdCompressorCallback = (body: string) => Buffer;
248248

249249
export interface HttpLibrary {
250+
enableRetry?: boolean | undefined;
251+
maxRetries?: number;
252+
backoffBase?: number;
253+
backoffMultiplier?: number;
250254
debug?: boolean;
251255
zstdCompressorCallback?: ZstdCompressorCallback;
252256
send(request: RequestContext): Promise<ResponseContext>;

0 commit comments

Comments
 (0)