@digitraffic/common 2023.5.31-2 → 2023.6.27-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws/runtime/dt-logger.d.ts +1 -1
- package/dist/types/async-timeout-error.d.ts +3 -0
- package/dist/types/async-timeout-error.js +10 -0
- package/dist/types/http-error.d.ts +4 -0
- package/dist/types/http-error.js +11 -0
- package/dist/utils/date-utils.d.ts +6 -0
- package/dist/utils/date-utils.js +11 -1
- package/dist/utils/retry.d.ts +21 -1
- package/dist/utils/retry.js +114 -18
- package/package.json +21 -17
- package/src/aws/runtime/dt-logger.ts +1 -0
- package/src/types/async-timeout-error.ts +5 -0
- package/src/types/http-error.ts +8 -0
- package/src/utils/date-utils.ts +13 -0
- package/src/utils/retry.ts +172 -23
@@ -26,7 +26,7 @@ export interface CustomParams {
|
|
26
26
|
/** do not log your apikey! */
|
27
27
|
customApiKey?: never;
|
28
28
|
[key: `custom${Capitalize<string>}Count`]: number;
|
29
|
-
[key: `custom${Capitalize<string>}`]: string | number | boolean | Date | null | undefined;
|
29
|
+
[key: `custom${Capitalize<string>}`]: string | number | bigint | boolean | Date | null | undefined;
|
30
30
|
}
|
31
31
|
/**
|
32
32
|
* Digitraffic logging object.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.AsyncTimeoutError = void 0;
|
4
|
+
class AsyncTimeoutError extends Error {
|
5
|
+
constructor() {
|
6
|
+
super("Async operation timed out");
|
7
|
+
}
|
8
|
+
}
|
9
|
+
exports.AsyncTimeoutError = AsyncTimeoutError;
|
10
|
+
//# sourceMappingURL=async-timeout-error.js.map
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.HttpError = void 0;
|
4
|
+
class HttpError extends Error {
|
5
|
+
constructor(statusCode, message) {
|
6
|
+
super(message);
|
7
|
+
this.statusCode = statusCode;
|
8
|
+
}
|
9
|
+
}
|
10
|
+
exports.HttpError = HttpError;
|
11
|
+
//# sourceMappingURL=http-error.js.map
|
@@ -2,6 +2,8 @@
|
|
2
2
|
* Constant for the 1970-01-01T00:00:00Z epoch Date.
|
3
3
|
*/
|
4
4
|
export declare const EPOCH: Date;
|
5
|
+
export declare const UTC = "UTC";
|
6
|
+
export declare const MYSQL_DATETIME_FORMAT = "yyyy-MM-dd HH:mm";
|
5
7
|
/**
|
6
8
|
* Counts difference in milliseconds between dates.
|
7
9
|
* @param start
|
@@ -19,3 +21,7 @@ export declare function countDiffInSeconds(start: Date, end: Date): number;
|
|
19
21
|
* @param isoString to convert
|
20
22
|
*/
|
21
23
|
export declare function dateFromIsoString(isoString: string): Date;
|
24
|
+
/**
|
25
|
+
* Formats a date in UTC in the given format, regardless of system time zone
|
26
|
+
*/
|
27
|
+
export declare function dateToUTCString(date: Date, format: string): string;
|
package/dist/utils/date-utils.js
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.dateFromIsoString = exports.countDiffInSeconds = exports.countDiffMs = exports.EPOCH = void 0;
|
3
|
+
exports.dateToUTCString = exports.dateFromIsoString = exports.countDiffInSeconds = exports.countDiffMs = exports.MYSQL_DATETIME_FORMAT = exports.UTC = exports.EPOCH = void 0;
|
4
|
+
const date_fns_tz_1 = require("date-fns-tz");
|
4
5
|
/**
|
5
6
|
* Constant for the 1970-01-01T00:00:00Z epoch Date.
|
6
7
|
*/
|
7
8
|
exports.EPOCH = new Date(Date.UTC(1970, 0, 1));
|
9
|
+
exports.UTC = "UTC";
|
10
|
+
exports.MYSQL_DATETIME_FORMAT = "yyyy-MM-dd HH:mm";
|
8
11
|
/**
|
9
12
|
* Counts difference in milliseconds between dates.
|
10
13
|
* @param start
|
@@ -38,4 +41,11 @@ exports.dateFromIsoString = dateFromIsoString;
|
|
38
41
|
function isValidDate(d) {
|
39
42
|
return d instanceof Date && !isNaN(d.getTime());
|
40
43
|
}
|
44
|
+
/**
|
45
|
+
* Formats a date in UTC in the given format, regardless of system time zone
|
46
|
+
*/
|
47
|
+
function dateToUTCString(date, format) {
|
48
|
+
return (0, date_fns_tz_1.formatInTimeZone)(date, exports.UTC, format);
|
49
|
+
}
|
50
|
+
exports.dateToUTCString = dateToUTCString;
|
41
51
|
//# sourceMappingURL=date-utils.js.map
|
package/dist/utils/retry.d.ts
CHANGED
@@ -3,11 +3,31 @@ export declare enum RetryLogError {
|
|
3
3
|
LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS = 1,
|
4
4
|
NO_LOGGING = 2
|
5
5
|
}
|
6
|
+
export type TimeoutFn = (retryCount: number) => number;
|
7
|
+
export type RetryPredicate = (error: unknown) => boolean;
|
8
|
+
/**
|
9
|
+
* Utility timeout functions for "retry" function.
|
10
|
+
*/
|
11
|
+
export declare const timeoutFunctions: {
|
12
|
+
noTimeout: (retryCount: number) => number;
|
13
|
+
exponentialTimeout: (retryCount: number) => number;
|
14
|
+
};
|
15
|
+
/**
|
16
|
+
* Utility retry predicates for "retry" function.
|
17
|
+
*/
|
18
|
+
export declare const retryPredicates: {
|
19
|
+
retryBasedOnStatusCode: (error: unknown) => boolean;
|
20
|
+
alwaysRetry: (error: unknown) => boolean;
|
21
|
+
};
|
22
|
+
export declare let retryCount: number;
|
6
23
|
/**
|
7
24
|
* Utility function for retrying async functions.
|
8
25
|
* @param asyncFn Function
|
9
26
|
* @param retries Amount of retries, default is 3. If set to <= 0, no retries will be done. Using non-finite numbers will throw an error. The maximum allowed retry count is 100.
|
10
27
|
* @param logError Logging options
|
28
|
+
* @param timeoutBetweenRetries A function that returns the timeout between retries in milliseconds. Default is a function returning 0. The function is called with the current retry count.
|
29
|
+
* @param retryPredicate A function that returns true if the error should be retried. Default is a function that always returns true. The function is called with the error object.
|
11
30
|
* @return Promise return value
|
12
31
|
*/
|
13
|
-
export declare function retry<T>(asyncFn: () => Promise<T>, retries?: number, logError?: RetryLogError): Promise<T>;
|
32
|
+
export declare function retry<T>(asyncFn: () => Promise<T>, retries?: number, logError?: RetryLogError, timeoutBetweenRetries?: TimeoutFn, retryPredicate?: RetryPredicate): Promise<T>;
|
33
|
+
export declare function retryRequest<T>(request: (...args: unknown[]) => Promise<T>, ...args: unknown[]): Promise<T>;
|
package/dist/utils/retry.js
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
-
exports.retry = exports.RetryLogError = void 0;
|
3
|
+
exports.retryRequest = exports.retry = exports.retryCount = exports.retryPredicates = exports.timeoutFunctions = exports.RetryLogError = void 0;
|
4
|
+
const http_error_1 = require("../types/http-error");
|
5
|
+
const async_timeout_error_1 = require("../types/async-timeout-error");
|
6
|
+
const dt_logger_default_1 = require("../aws/runtime/dt-logger-default");
|
4
7
|
var RetryLogError;
|
5
8
|
(function (RetryLogError) {
|
6
9
|
RetryLogError[RetryLogError["LOG_ALL_AS_ERRORS"] = 0] = "LOG_ALL_AS_ERRORS";
|
@@ -8,43 +11,136 @@ var RetryLogError;
|
|
8
11
|
RetryLogError[RetryLogError["NO_LOGGING"] = 2] = "NO_LOGGING";
|
9
12
|
})(RetryLogError = exports.RetryLogError || (exports.RetryLogError = {}));
|
10
13
|
/**
|
11
|
-
* Utility
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
* Utility timeout functions for "retry" function.
|
15
|
+
*/
|
16
|
+
exports.timeoutFunctions = (function () {
|
17
|
+
return {
|
18
|
+
noTimeout: (retryCount) => {
|
19
|
+
return 0;
|
20
|
+
},
|
21
|
+
exponentialTimeout: (retryCount) => {
|
22
|
+
return 2 ** retryCount * 1000;
|
23
|
+
},
|
24
|
+
};
|
25
|
+
})();
|
26
|
+
/**
|
27
|
+
* Utility retry predicates for "retry" function.
|
16
28
|
*/
|
17
|
-
|
29
|
+
exports.retryPredicates = (function () {
|
30
|
+
const retryStatusCodes = new Set([
|
31
|
+
// service might return 403 for no apparent reason
|
32
|
+
403,
|
33
|
+
// Opensearch responds 429, if you make too many requests too fast
|
34
|
+
429,
|
35
|
+
]);
|
36
|
+
return {
|
37
|
+
retryBasedOnStatusCode: (error) => {
|
38
|
+
if (error instanceof http_error_1.HttpError) {
|
39
|
+
return retryStatusCodes.has(error.statusCode);
|
40
|
+
}
|
41
|
+
return false;
|
42
|
+
},
|
43
|
+
alwaysRetry: (error) => {
|
44
|
+
return true;
|
45
|
+
},
|
46
|
+
};
|
47
|
+
})();
|
48
|
+
function readPossibleErrorMessage(error) {
|
49
|
+
if (error instanceof Error) {
|
50
|
+
return error.message;
|
51
|
+
}
|
52
|
+
return "Something else than an Error object was thrown";
|
53
|
+
}
|
54
|
+
// Tämä muuttuja on testejä varten määritelty täällä.
|
55
|
+
exports.retryCount = 0;
|
56
|
+
async function retryRecursive(asyncFn, retries, retryCountInj, logError, timeoutBetweenRetries, retryPredicate) {
|
57
|
+
const asyncFnTimeout = 30 * 60 * 1000; // 30 minutes
|
18
58
|
if (!isFinite(retries)) {
|
19
|
-
throw new Error(
|
59
|
+
throw new Error("Only finite numbers are supported");
|
20
60
|
}
|
21
61
|
if (retries > 100) {
|
22
|
-
throw new Error(
|
62
|
+
throw new Error("Exceeded the maximum retry count of 100");
|
23
63
|
}
|
24
64
|
try {
|
25
|
-
|
65
|
+
// NOTE, a Promise cannot be cancelled. So if the asyncFn calls multiple async/await paris and the first one takes 31 minutes to complete,
|
66
|
+
// then the rest of async/await pairs will be called even though AysncTimeoutError is allready thrown.
|
67
|
+
const result = await Promise.race([
|
68
|
+
asyncFn(),
|
69
|
+
new Promise((_, reject) => setTimeout(() => reject(new async_timeout_error_1.AsyncTimeoutError()), asyncFnTimeout)),
|
70
|
+
]);
|
71
|
+
return result;
|
26
72
|
}
|
27
73
|
catch (error) {
|
28
74
|
const remainingRetries = retries - 1;
|
29
|
-
const errorMessage = 'method=retry error';
|
30
75
|
if (logError === RetryLogError.LOG_ALL_AS_ERRORS) {
|
31
|
-
|
76
|
+
dt_logger_default_1.logger.error({
|
77
|
+
message: readPossibleErrorMessage(error),
|
78
|
+
method: "retry.retryRecursive",
|
79
|
+
});
|
32
80
|
}
|
33
81
|
else if (logError === RetryLogError.LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS) {
|
34
82
|
if (remainingRetries < 0) {
|
35
|
-
|
83
|
+
dt_logger_default_1.logger.error({
|
84
|
+
message: readPossibleErrorMessage(error),
|
85
|
+
method: "retry.retryRecursive",
|
86
|
+
});
|
36
87
|
}
|
37
88
|
else {
|
38
|
-
|
89
|
+
dt_logger_default_1.logger.warn({
|
90
|
+
message: readPossibleErrorMessage(error),
|
91
|
+
method: "retry.retryRecursive",
|
92
|
+
});
|
39
93
|
}
|
40
94
|
}
|
41
95
|
if (remainingRetries < 0) {
|
42
|
-
|
43
|
-
|
96
|
+
dt_logger_default_1.logger.warn({
|
97
|
+
message: "No retries left",
|
98
|
+
method: "retry.retryRecursive",
|
99
|
+
});
|
100
|
+
throw new Error("No retries left");
|
101
|
+
}
|
102
|
+
dt_logger_default_1.logger.warn({
|
103
|
+
message: `Retrying with remaining retries ${remainingRetries}`,
|
104
|
+
method: "retry.retryRecursive",
|
105
|
+
});
|
106
|
+
if (retryPredicate(error)) {
|
107
|
+
retryCountInj++;
|
108
|
+
exports.retryCount = retryCountInj;
|
109
|
+
const milliseconds = timeoutBetweenRetries(retryCountInj);
|
110
|
+
if (milliseconds > 0) {
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
112
|
+
}
|
113
|
+
return retryRecursive(asyncFn, remainingRetries, retryCountInj, logError, timeoutBetweenRetries, retryPredicate);
|
114
|
+
}
|
115
|
+
else {
|
116
|
+
throw new Error("Retry predicate failed");
|
44
117
|
}
|
45
|
-
console.warn('method=retry invocation failed, retrying with remaining retries %d', remainingRetries);
|
46
|
-
return retry(asyncFn, remainingRetries, logError);
|
47
118
|
}
|
48
119
|
}
|
120
|
+
/**
|
121
|
+
* Utility function for retrying async functions.
|
122
|
+
* @param asyncFn Function
|
123
|
+
* @param retries Amount of retries, default is 3. If set to <= 0, no retries will be done. Using non-finite numbers will throw an error. The maximum allowed retry count is 100.
|
124
|
+
* @param logError Logging options
|
125
|
+
* @param timeoutBetweenRetries A function that returns the timeout between retries in milliseconds. Default is a function returning 0. The function is called with the current retry count.
|
126
|
+
* @param retryPredicate A function that returns true if the error should be retried. Default is a function that always returns true. The function is called with the error object.
|
127
|
+
* @return Promise return value
|
128
|
+
*/
|
129
|
+
async function retry(asyncFn, retries = 3, logError = RetryLogError.LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS, timeoutBetweenRetries = exports.timeoutFunctions.noTimeout, retryPredicate = exports.retryPredicates.alwaysRetry) {
|
130
|
+
exports.retryCount = 0;
|
131
|
+
dt_logger_default_1.logger.debug({
|
132
|
+
message: `Retrying with ${retries} retries`,
|
133
|
+
method: "retry.retry",
|
134
|
+
});
|
135
|
+
return retryRecursive(asyncFn, retries, 0, logError, timeoutBetweenRetries, retryPredicate);
|
136
|
+
}
|
49
137
|
exports.retry = retry;
|
138
|
+
function wrapArgsToFn(fn, ...args) {
|
139
|
+
return async () => await fn(...args);
|
140
|
+
}
|
141
|
+
async function retryRequest(request, ...args) {
|
142
|
+
const asyncFn = wrapArgsToFn(request, ...args);
|
143
|
+
return retry(asyncFn, 5, RetryLogError.LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS, exports.timeoutFunctions.exponentialTimeout, exports.retryPredicates.retryBasedOnStatusCode);
|
144
|
+
}
|
145
|
+
exports.retryRequest = retryRequest;
|
50
146
|
//# sourceMappingURL=retry.js.map
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@digitraffic/common",
|
3
|
-
"version": "2023.
|
3
|
+
"version": "2023.6.27-1",
|
4
4
|
"description": "",
|
5
5
|
"repository": {
|
6
6
|
"type": "git",
|
@@ -17,37 +17,41 @@
|
|
17
17
|
"src/**/*.ts"
|
18
18
|
],
|
19
19
|
"peerDependencies": {
|
20
|
-
"@aws-cdk/aws-synthetics-alpha": "^2.
|
20
|
+
"@aws-cdk/aws-synthetics-alpha": "^2.85.0-alpha.0",
|
21
21
|
"@types/geojson": "^7946.0.10",
|
22
|
-
"aws-cdk-lib": "^2.
|
23
|
-
"aws-sdk": "^2.
|
22
|
+
"aws-cdk-lib": "^2.85.0",
|
23
|
+
"aws-sdk": "^2.1405.0",
|
24
24
|
"axios": "^1.2.6",
|
25
25
|
"change-case": "^4.1.2",
|
26
26
|
"constructs": "^10.2.17",
|
27
|
+
"date-fns-tz": "~2.0.0",
|
28
|
+
"date-fns": "~2.30.0",
|
27
29
|
"etag": "^1.8.1",
|
28
30
|
"geojson-validation": "^1.0.2",
|
29
31
|
"node-ttl": "^0.2.0",
|
30
32
|
"pg-native": "^3.0.1",
|
31
|
-
"pg-promise": "^11.
|
33
|
+
"pg-promise": "^11.5.0"
|
32
34
|
},
|
33
35
|
"devDependencies": {
|
34
|
-
"@aws-cdk/aws-synthetics-alpha": "2.
|
35
|
-
"@types/aws-lambda": "~8.10.
|
36
|
+
"@aws-cdk/aws-synthetics-alpha": "2.85.0-alpha.0",
|
37
|
+
"@types/aws-lambda": "~8.10.119",
|
36
38
|
"@types/geojson": "^7946.0.10",
|
37
39
|
"@types/etag": "^1.8.1",
|
38
|
-
"@types/jest": "^29.5.
|
40
|
+
"@types/jest": "^29.5.2",
|
39
41
|
"@types/lodash": "^4.14.195",
|
40
42
|
"@types/node": "18.15.13",
|
41
43
|
"@types/ramda": "~0.29.1",
|
42
44
|
"@types/sinon": "10.0.15",
|
43
|
-
"@typescript-eslint/eslint-plugin": "~5.
|
44
|
-
"@typescript-eslint/parser": "^5.
|
45
|
-
"aws-cdk-lib": "~2.
|
46
|
-
"aws-sdk": "~2.
|
45
|
+
"@typescript-eslint/eslint-plugin": "~5.60.1",
|
46
|
+
"@typescript-eslint/parser": "^5.60.1",
|
47
|
+
"aws-cdk-lib": "~2.85.0",
|
48
|
+
"aws-sdk": "~2.1405.0",
|
47
49
|
"axios": "^1.3.6",
|
48
50
|
"change-case": "^4.1.2",
|
49
|
-
"constructs": "10.2.
|
50
|
-
"
|
51
|
+
"constructs": "10.2.61",
|
52
|
+
"date-fns-tz": "~2.0.0",
|
53
|
+
"date-fns": "~2.30.0",
|
54
|
+
"eslint": "~8.43.0",
|
51
55
|
"eslint-config-prettier": "^8.8.0",
|
52
56
|
"eslint-plugin-deprecation": "~1.4.1",
|
53
57
|
"etag": "^1.8.1",
|
@@ -59,11 +63,11 @@
|
|
59
63
|
"lodash": "^4.17.21",
|
60
64
|
"node-ttl": "^0.2.0",
|
61
65
|
"pg-native": "^3.0.1",
|
62
|
-
"pg-promise": "^11.
|
66
|
+
"pg-promise": "^11.5.0",
|
63
67
|
"prettier": "^2.8.8",
|
64
68
|
"ramda": "~0.29.0",
|
65
69
|
"rimraf": "^5.0.1",
|
66
|
-
"sinon": "15.
|
70
|
+
"sinon": "15.2.0",
|
67
71
|
"ts-jest": "^29.1.0",
|
68
72
|
"typescript": "~4.9.5",
|
69
73
|
"velocityjs": "2.0.6"
|
@@ -79,7 +83,7 @@
|
|
79
83
|
"build": "tsc",
|
80
84
|
"lint": "eslint --cache .",
|
81
85
|
"eslint-report": "eslint . --format html",
|
82
|
-
"ci:eslint-report": "eslint . --format html -o report.html",
|
86
|
+
"ci:eslint-report": "eslint . --format html -o report.html || true",
|
83
87
|
"clean": "rimraf dist output",
|
84
88
|
"test": "jest --detectOpenHandles --forceExit --coverage --coverageDirectory=output/coverage/jest",
|
85
89
|
"test:watch": "jest --detectOpenHandles --forceExit --coverage --coverageDirectory=output/coverage/jest --watch"
|
package/src/utils/date-utils.ts
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
import { formatInTimeZone } from "date-fns-tz";
|
2
|
+
|
1
3
|
/**
|
2
4
|
* Constant for the 1970-01-01T00:00:00Z epoch Date.
|
3
5
|
*/
|
4
6
|
export const EPOCH = new Date(Date.UTC(1970, 0, 1));
|
5
7
|
|
8
|
+
export const UTC = "UTC";
|
9
|
+
|
10
|
+
export const MYSQL_DATETIME_FORMAT = "yyyy-MM-dd HH:mm";
|
11
|
+
|
6
12
|
/**
|
7
13
|
* Counts difference in milliseconds between dates.
|
8
14
|
* @param start
|
@@ -38,3 +44,10 @@ export function dateFromIsoString(isoString: string): Date {
|
|
38
44
|
function isValidDate(d: unknown) {
|
39
45
|
return d instanceof Date && !isNaN(d.getTime());
|
40
46
|
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Formats a date in UTC in the given format, regardless of system time zone
|
50
|
+
*/
|
51
|
+
export function dateToUTCString(date: Date, format: string): string {
|
52
|
+
return formatInTimeZone(date, UTC, format);
|
53
|
+
}
|
package/src/utils/retry.ts
CHANGED
@@ -1,49 +1,198 @@
|
|
1
|
+
import { HttpError } from "../types/http-error";
|
2
|
+
import { AsyncTimeoutError } from "../types/async-timeout-error";
|
3
|
+
import { logger } from "../aws/runtime/dt-logger-default";
|
4
|
+
|
1
5
|
export enum RetryLogError {
|
2
6
|
LOG_ALL_AS_ERRORS,
|
3
7
|
LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS,
|
4
|
-
NO_LOGGING
|
8
|
+
NO_LOGGING,
|
5
9
|
}
|
6
10
|
|
11
|
+
export type TimeoutFn = (retryCount: number) => number;
|
12
|
+
export type RetryPredicate = (error: unknown) => boolean;
|
13
|
+
|
7
14
|
/**
|
8
|
-
* Utility
|
9
|
-
* @param asyncFn Function
|
10
|
-
* @param retries Amount of retries, default is 3. If set to <= 0, no retries will be done. Using non-finite numbers will throw an error. The maximum allowed retry count is 100.
|
11
|
-
* @param logError Logging options
|
12
|
-
* @return Promise return value
|
15
|
+
* Utility timeout functions for "retry" function.
|
13
16
|
*/
|
14
|
-
export
|
15
|
-
|
16
|
-
|
17
|
+
export const timeoutFunctions = (function () {
|
18
|
+
return {
|
19
|
+
noTimeout: (retryCount: number): number => {
|
20
|
+
return 0;
|
21
|
+
},
|
22
|
+
exponentialTimeout: (retryCount: number): number => {
|
23
|
+
return 2 ** retryCount * 1000;
|
24
|
+
},
|
25
|
+
};
|
26
|
+
})();
|
17
27
|
|
28
|
+
/**
|
29
|
+
* Utility retry predicates for "retry" function.
|
30
|
+
*/
|
31
|
+
export const retryPredicates = (function () {
|
32
|
+
const retryStatusCodes = new Set([
|
33
|
+
// service might return 403 for no apparent reason
|
34
|
+
403,
|
35
|
+
// Opensearch responds 429, if you make too many requests too fast
|
36
|
+
429,
|
37
|
+
]);
|
38
|
+
return {
|
39
|
+
retryBasedOnStatusCode: (error: unknown): boolean => {
|
40
|
+
if (error instanceof HttpError) {
|
41
|
+
return retryStatusCodes.has(error.statusCode);
|
42
|
+
}
|
43
|
+
return false;
|
44
|
+
},
|
45
|
+
alwaysRetry: (error: unknown): boolean => {
|
46
|
+
return true;
|
47
|
+
},
|
48
|
+
};
|
49
|
+
})();
|
50
|
+
|
51
|
+
function readPossibleErrorMessage(error: unknown): string {
|
52
|
+
if (error instanceof Error) {
|
53
|
+
return error.message;
|
54
|
+
}
|
55
|
+
return "Something else than an Error object was thrown";
|
56
|
+
}
|
57
|
+
|
58
|
+
// Tämä muuttuja on testejä varten määritelty täällä.
|
59
|
+
export let retryCount = 0;
|
60
|
+
|
61
|
+
async function retryRecursive<T>(
|
62
|
+
asyncFn: () => Promise<T>,
|
63
|
+
retries: number,
|
64
|
+
retryCountInj: number,
|
65
|
+
logError: RetryLogError,
|
66
|
+
timeoutBetweenRetries: TimeoutFn,
|
67
|
+
retryPredicate: RetryPredicate
|
68
|
+
): Promise<T> {
|
69
|
+
const asyncFnTimeout = 30 * 60 * 1000; // 30 minutes
|
18
70
|
if (!isFinite(retries)) {
|
19
|
-
throw new Error(
|
71
|
+
throw new Error("Only finite numbers are supported");
|
20
72
|
}
|
21
73
|
if (retries > 100) {
|
22
|
-
throw new Error(
|
74
|
+
throw new Error("Exceeded the maximum retry count of 100");
|
23
75
|
}
|
24
76
|
try {
|
25
|
-
|
77
|
+
// NOTE, a Promise cannot be cancelled. So if the asyncFn calls multiple async/await paris and the first one takes 31 minutes to complete,
|
78
|
+
// then the rest of async/await pairs will be called even though AysncTimeoutError is allready thrown.
|
79
|
+
const result: T = await Promise.race([
|
80
|
+
asyncFn(),
|
81
|
+
new Promise<never>((_, reject) =>
|
82
|
+
setTimeout(
|
83
|
+
() => reject(new AsyncTimeoutError()),
|
84
|
+
asyncFnTimeout
|
85
|
+
)
|
86
|
+
),
|
87
|
+
]);
|
88
|
+
return result;
|
26
89
|
} catch (error) {
|
27
90
|
const remainingRetries = retries - 1;
|
28
91
|
|
29
|
-
const errorMessage = 'method=retry error';
|
30
92
|
if (logError === RetryLogError.LOG_ALL_AS_ERRORS) {
|
31
|
-
|
32
|
-
|
93
|
+
logger.error({
|
94
|
+
message: readPossibleErrorMessage(error),
|
95
|
+
method: "retry.retryRecursive",
|
96
|
+
});
|
97
|
+
} else if (
|
98
|
+
logError === RetryLogError.LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS
|
99
|
+
) {
|
33
100
|
if (remainingRetries < 0) {
|
34
|
-
|
101
|
+
logger.error({
|
102
|
+
message: readPossibleErrorMessage(error),
|
103
|
+
method: "retry.retryRecursive",
|
104
|
+
});
|
35
105
|
} else {
|
36
|
-
|
106
|
+
logger.warn({
|
107
|
+
message: readPossibleErrorMessage(error),
|
108
|
+
method: "retry.retryRecursive",
|
109
|
+
});
|
37
110
|
}
|
38
111
|
}
|
39
112
|
|
40
113
|
if (remainingRetries < 0) {
|
41
|
-
|
42
|
-
|
114
|
+
logger.warn({
|
115
|
+
message: "No retries left",
|
116
|
+
method: "retry.retryRecursive",
|
117
|
+
});
|
118
|
+
throw new Error("No retries left");
|
119
|
+
}
|
120
|
+
logger.warn({
|
121
|
+
message: `Retrying with remaining retries ${remainingRetries}`,
|
122
|
+
method: "retry.retryRecursive",
|
123
|
+
});
|
124
|
+
if (retryPredicate(error)) {
|
125
|
+
retryCountInj++;
|
126
|
+
retryCount = retryCountInj;
|
127
|
+
const milliseconds = timeoutBetweenRetries(retryCountInj);
|
128
|
+
if (milliseconds > 0) {
|
129
|
+
await new Promise((resolve) =>
|
130
|
+
setTimeout(resolve, milliseconds)
|
131
|
+
);
|
132
|
+
}
|
133
|
+
return retryRecursive(
|
134
|
+
asyncFn,
|
135
|
+
remainingRetries,
|
136
|
+
retryCountInj,
|
137
|
+
logError,
|
138
|
+
timeoutBetweenRetries,
|
139
|
+
retryPredicate
|
140
|
+
);
|
141
|
+
} else {
|
142
|
+
throw new Error("Retry predicate failed");
|
43
143
|
}
|
44
|
-
console.warn('method=retry invocation failed, retrying with remaining retries %d', remainingRetries);
|
45
|
-
return retry(asyncFn,
|
46
|
-
remainingRetries,
|
47
|
-
logError);
|
48
144
|
}
|
49
145
|
}
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Utility function for retrying async functions.
|
149
|
+
* @param asyncFn Function
|
150
|
+
* @param retries Amount of retries, default is 3. If set to <= 0, no retries will be done. Using non-finite numbers will throw an error. The maximum allowed retry count is 100.
|
151
|
+
* @param logError Logging options
|
152
|
+
* @param timeoutBetweenRetries A function that returns the timeout between retries in milliseconds. Default is a function returning 0. The function is called with the current retry count.
|
153
|
+
* @param retryPredicate A function that returns true if the error should be retried. Default is a function that always returns true. The function is called with the error object.
|
154
|
+
* @return Promise return value
|
155
|
+
*/
|
156
|
+
export async function retry<T>(
|
157
|
+
asyncFn: () => Promise<T>,
|
158
|
+
retries = 3,
|
159
|
+
logError = RetryLogError.LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS,
|
160
|
+
timeoutBetweenRetries: TimeoutFn = timeoutFunctions.noTimeout,
|
161
|
+
retryPredicate: RetryPredicate = retryPredicates.alwaysRetry
|
162
|
+
): Promise<T> {
|
163
|
+
retryCount = 0;
|
164
|
+
|
165
|
+
logger.debug({
|
166
|
+
message: `Retrying with ${retries} retries`,
|
167
|
+
method: "retry.retry",
|
168
|
+
});
|
169
|
+
return retryRecursive(
|
170
|
+
asyncFn,
|
171
|
+
retries,
|
172
|
+
0,
|
173
|
+
logError,
|
174
|
+
timeoutBetweenRetries,
|
175
|
+
retryPredicate
|
176
|
+
);
|
177
|
+
}
|
178
|
+
|
179
|
+
function wrapArgsToFn<T>(
|
180
|
+
fn: (...args: unknown[]) => Promise<T>,
|
181
|
+
...args: unknown[]
|
182
|
+
): () => Promise<T> {
|
183
|
+
return async () => await fn(...args);
|
184
|
+
}
|
185
|
+
|
186
|
+
export async function retryRequest<T>(
|
187
|
+
request: (...args: unknown[]) => Promise<T>,
|
188
|
+
...args: unknown[]
|
189
|
+
): Promise<T> {
|
190
|
+
const asyncFn = wrapArgsToFn(request, ...args);
|
191
|
+
return retry(
|
192
|
+
asyncFn,
|
193
|
+
5,
|
194
|
+
RetryLogError.LOG_LAST_RETRY_AS_ERROR_OTHERS_AS_WARNS,
|
195
|
+
timeoutFunctions.exponentialTimeout,
|
196
|
+
retryPredicates.retryBasedOnStatusCode
|
197
|
+
);
|
198
|
+
}
|