Skip to content

Commit 561941f

Browse files
Merge pull request #31 from gofynd/enhancement/detail-api-retry-mechanism
Add retry mechanism in case of timeout has occurred when new cluster …
2 parents 9903cdd + 0dba5e9 commit 561941f

File tree

8 files changed

+264
-28
lines changed

8 files changed

+264
-28
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77

8+
---
9+
## [v0.6.0] - 2024-01-09
10+
### Added
11+
- Added retry mechanism for APIs getting used inside extension library if Fynd Platform server is down.
812
---
913
## [v0.5.4] - 2023-03-03
1014
### Changed

express/constants.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ exports.ASSOCIATION_CRITERIA = {
66
ALL: "ALL",
77
SPECIFIC: "SPECIFIC-EVENTS", // to be set when saleschannel specific events are subscribed and sales channel present
88
EMPTY: "EMPTY" // to be set when saleschannel specific events are subscribed but not sales channel present
9-
}
9+
}
10+
exports.TIMEOUT_STATUS = 504;
11+
exports.SERVICE_UNAVAILABLE = 503;
12+
exports.BAD_GATEWAY = 502;

express/extension.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { WebhookRegistry } = require('./webhook');
77
const logger = require('./logger');
88
const { fdkAxios } = require('@gofynd/fdk-client-javascript/sdk/common/AxiosHelper');
99
const { version } = require('./../package.json');
10+
const { RetryManger } = require("./retry_manager")
1011

1112
class Extension {
1213
constructor() {
@@ -19,11 +20,17 @@ class Extension {
1920
this.cluster = "https://api.fynd.com";
2021
this.webhookRegistry = null;
2122
this._isInitialized = false;
23+
this._retryManager = new RetryManger();
2224
}
2325

2426
async initialize(data) {
2527

28+
if (this._isInitialized) {
29+
return;
30+
}
31+
2632
this._isInitialized = false;
33+
this.configData = data;
2734

2835
this.storage = data.storage;
2936

@@ -50,22 +57,22 @@ class Extension {
5057
}
5158
this.cluster = data.cluster;
5259
}
53-
this.webhookRegistry = new WebhookRegistry();
60+
this.webhookRegistry = new WebhookRegistry(this._retryManager);
5461

55-
let extensionData = await this.getExtensionDetails();
62+
await this.getExtensionDetails();
5663

5764
if (data.base_url && !validator.isURL(data.base_url)) {
5865
throw new FdkInvalidExtensionConfig("Invalid base_url value. Invalid value: " + data.base_url);
5966
}
6067
else if (!data.base_url) {
61-
data.base_url = extensionData.base_url;
68+
data.base_url = this.extensionData.base_url;
6269
}
6370
this.base_url = data.base_url;
6471

6572
if (data.scopes) {
66-
data.scopes = this.verifyScopes(data.scopes, extensionData);
73+
data.scopes = this.verifyScopes(data.scopes, this.extensionData);
6774
}
68-
this.scopes = data.scopes || extensionData.scope;
75+
this.scopes = data.scopes || this.extensionData.scope;
6976

7077
logger.debug(`Extension initialized`);
7178

@@ -96,9 +103,9 @@ class Extension {
96103
return this.access_mode === 'online';
97104
}
98105

99-
getPlatformConfig(companyId) {
106+
async getPlatformConfig(companyId) {
100107
if (!this._isInitialized){
101-
throw new FdkInvalidExtensionConfig('Extension not initialized due to invalid data')
108+
await this.initialize(this.configData);
102109
}
103110
let platformConfig = new PlatformConfig({
104111
companyId: parseInt(companyId),
@@ -113,11 +120,11 @@ class Extension {
113120

114121
async getPlatformClient(companyId, session) {
115122
if (!this._isInitialized){
116-
throw new FdkInvalidExtensionConfig('Extension not initialized due to invalid data')
123+
await this.initialize(this.configData);
117124
}
118125
const SessionStorage = require('./session/session_storage');
119126

120-
let platformConfig = this.getPlatformConfig(companyId);
127+
let platformConfig = await this.getPlatformConfig(companyId);
121128
platformConfig.oauthClient.setToken(session);
122129
platformConfig.oauthClient.token_expires_at = session.access_token_validity;
123130

@@ -140,14 +147,22 @@ class Extension {
140147
}
141148

142149
async getExtensionDetails() {
150+
151+
let url = `${this.cluster}/service/panel/partners/v1.0/extensions/details/${this.api_key}`;
152+
const uniqueKey = `${url}`;
153+
154+
const retryInfo = this._retryManager.retryInfoMap.get(uniqueKey);
155+
if (retryInfo && !retryInfo.isRetry) {
156+
this._retryManager.resetRetryState(uniqueKey);
157+
}
158+
143159
try {
144-
let url = `${this.cluster}/service/panel/partners/v1.0/extensions/details/${this.api_key}`;
145160
const token = Buffer.from(
146161
`${this.api_key}:${this.api_secret}`,
147162
"utf8"
148163
).toString("base64");
149164
const rawRequest = {
150-
method: "get",
165+
method: "GET",
151166
url: url,
152167
headers: {
153168
Authorization: `Basic ${token}`,
@@ -157,8 +172,17 @@ class Extension {
157172
};
158173
let extensionData = await fdkAxios.request(rawRequest);
159174
logger.debug(`Extension details received: ${logger.safeStringify(extensionData)}`);
160-
return extensionData;
175+
this.extensionData = extensionData;
161176
} catch (err) {
177+
178+
if (
179+
RetryManger.shouldRetryOnError(err)
180+
&& !this._retryManager.isRetryInProgress(uniqueKey)
181+
) {
182+
logger.debug(`API call failed. Starting retry for ${uniqueKey}`)
183+
return await this._retryManager.retry(uniqueKey, this.getExtensionDetails.bind(this));
184+
}
185+
162186
throw new FdkInvalidExtensionConfig("Invalid api_key or api_secret. Reason:" + err.message);
163187
}
164188
}

express/retry_manager.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const { BAD_GATEWAY, SERVICE_UNAVAILABLE, TIMEOUT_STATUS } = require("./constants");
2+
const logger = require('./logger');
3+
4+
5+
/**
6+
* @typedef {Object} RetryInfo
7+
* @property {boolean} isRetryInProgress
8+
* @property {boolean} isRetry
9+
* @property {number} retryCount
10+
* @property {NodeJS.Timeout | null} retryTimer
11+
* @property {function} fn
12+
* @property {any[]} args
13+
*/
14+
15+
/**
16+
* @typedef {Map<string, RetryInfo>} RetryInfoMap
17+
*/
18+
19+
class RetryManger {
20+
21+
constructor() {
22+
/**
23+
* @type {RetryInfoMap}
24+
*/
25+
this.retryInfoMap = new Map();
26+
}
27+
28+
static shouldRetryOnError(err) {
29+
const statusCode = (err.response && err.response.status) || err.code;
30+
return [BAD_GATEWAY, SERVICE_UNAVAILABLE, TIMEOUT_STATUS].includes(statusCode)
31+
}
32+
33+
34+
async retry(uniqueKey, fn, ...args) {
35+
36+
if (!this.retryInfoMap.has(uniqueKey)) {
37+
this.retryInfoMap.set(uniqueKey, {
38+
fn: fn,
39+
args: args,
40+
retryCount: 0,
41+
retryTimer: null,
42+
isRetryInProgress: false
43+
})
44+
}
45+
46+
const retryInfo = this.retryInfoMap.get(uniqueKey);
47+
48+
retryInfo.isRetryInProgress = true;
49+
retryInfo.retryCount++;
50+
51+
await (new Promise((resolve, reject) => {
52+
retryInfo.retryTimer = setTimeout(resolve, this._getNextRetrySeconds(retryInfo.retryCount));
53+
}))
54+
55+
return await this._makeRetry(uniqueKey);
56+
}
57+
58+
59+
_getNextRetrySeconds(retryCount) {
60+
let nextRetrySeconds = 30 * 1000; // 30 seconds
61+
62+
if (retryCount > 3) {
63+
const MAX_MINUTES_TO_WAIT = 3;
64+
const MINUTES_TO_WAIT = Math.min((retryCount - 3), MAX_MINUTES_TO_WAIT);
65+
nextRetrySeconds = 1000 * 60 * MINUTES_TO_WAIT;
66+
}
67+
68+
return nextRetrySeconds;
69+
}
70+
71+
72+
73+
async _makeRetry(uniqueKey) {
74+
75+
const retryInfo = this.retryInfoMap.get(uniqueKey);
76+
77+
clearTimeout(retryInfo.retryTimer);
78+
79+
try {
80+
logger.debug(`Retrying api call for ${uniqueKey}. count: ${retryInfo.retryCount}`);
81+
retryInfo.isRetry = true;
82+
const data = await retryInfo.fn(...retryInfo.args);
83+
logger.debug(`api call succeeded. stopping retry for ${uniqueKey}`);
84+
this.resetRetryState(uniqueKey);
85+
return data;
86+
87+
} catch(error) {
88+
retryInfo.isRetry = false;
89+
logger.debug(`API call failed on retry ${retryInfo.retryCount}: ${error.message}`);
90+
return await this.retry(uniqueKey, retryInfo.fn, ...retryInfo.args);
91+
92+
}
93+
}
94+
95+
96+
resetRetryState(uniqueKey) {
97+
98+
const retryInfo = this.retryInfoMap.get(uniqueKey);
99+
100+
if (retryInfo.retryTimer) {
101+
clearTimeout(retryInfo.retryTimer);
102+
}
103+
104+
retryInfo.isRetry = false;
105+
retryInfo.isRetryInProgress = false;
106+
retryInfo.retryCount = 0;
107+
}
108+
109+
isRetryInProgress(uniqueKey) {
110+
return this.retryInfoMap.get(uniqueKey)? this.retryInfoMap.get(uniqueKey).isRetryInProgress: false;
111+
}
112+
}
113+
114+
module.exports = {
115+
RetryManger
116+
}

express/routes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function setupRoutes(ext) {
1919
// ?company_id=1&client_id=123313112122
2020
try {
2121
let companyId = parseInt(req.query.company_id);
22-
let platformConfig = ext.getPlatformConfig(companyId);
22+
let platformConfig = await ext.getPlatformConfig(companyId);
2323
let session;
2424

2525
session = new Session(Session.generateSessionId(true));
@@ -88,7 +88,7 @@ function setupRoutes(ext) {
8888
}
8989
const companyId = req.fdkSession.company_id
9090

91-
const platformConfig = ext.getPlatformConfig(req.fdkSession.company_id);
91+
const platformConfig = await ext.getPlatformConfig(req.fdkSession.company_id);
9292
await platformConfig.oauthClient.verifyCallback(req.query);
9393

9494
let token = platformConfig.oauthClient.raw_token;
@@ -161,7 +161,7 @@ function setupRoutes(ext) {
161161

162162
logger.debug(`Extension auto install started for company: ${company_id} on company creation.`);
163163

164-
let platformConfig = ext.getPlatformConfig(company_id);
164+
let platformConfig = await ext.getPlatformConfig(company_id);
165165
let sid = Session.generateSessionId(false, {
166166
cluster: ext.cluster,
167167
companyId: company_id

0 commit comments

Comments
 (0)