Skip to content

Commit 3184eec

Browse files
committed
feat: ✨ support connection retries with exponential backoff for redis and databases
1 parent 25a3444 commit 3184eec

5 files changed

Lines changed: 94 additions & 25 deletions

File tree

src/component/connector/mongoose.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const util = require("node:util");
22
const mongoose = require("mongoose");
33
const debug = require("../../lib/debug");
4+
const { sleep } = require("../../lib/util");
45
const validateConnectionOptions = require("./connection-validator");
56

67

@@ -44,11 +45,12 @@ module.exports = class MongooseStore {
4445
* @return {resource} a (mongoose) connection instance
4546
*/
4647
async connect() {
48+
let dsn;
49+
let attempts = 0;
4750
const options = this.#options;
51+
const maxAttempts = this.#options.maxConnectionAttempts || 5;
4852
const { host, port, username, password, dbName, enableDebugging } = options;
4953

50-
let dsn;
51-
5254
if(options.url?.trim()?.length > 0) {
5355
dsn = options.url;
5456
} else {
@@ -67,16 +69,28 @@ module.exports = class MongooseStore {
6769

6870
mongoose.set("debug", enableDebugging);
6971

70-
try {
71-
debug("Connecting to MongoDB...");
72+
while(attempts < maxAttempts) {
73+
try {
74+
debug("Connecting to MongoDB...");
7275

73-
this.#db = mongoose.createConnection(dsn, {});
76+
this.#db = mongoose.createConnection(dsn, {});
7477

75-
debug("MongoDB connection established");
78+
debug("MongoDB connection established");
7679

77-
return this.#db;
78-
} catch(e) {
79-
debug(`Mongoose connection error: ${util.inspect(e)}`);
80+
return this.#db;
81+
} catch(e) {
82+
attempts++;
83+
84+
debug(`Mongoose connection error: ${util.inspect(e)}`);
85+
debug(`Retrying connection to MongoDB (${attempts}/${maxAttempts}) attempts`);
86+
87+
if(attempts === maxAttempts) {
88+
debug(`Failed to connect to MongoDB after ${maxAttempts} attempts. Exiting...`);
89+
process.exit(1);
90+
}
91+
92+
await sleep(1000 * attempts); // Exponential backoff
93+
}
8094
}
8195
}
8296

@@ -145,6 +159,10 @@ module.exports = class MongooseStore {
145159

146160
debug("Mongoose connection options validated.");
147161

148-
return { ...validatedOptions, url: options?.url };
162+
return {
163+
...validatedOptions,
164+
url: options?.url,
165+
maxConnectionAttempts: options?.maxConnectionAttempts,
166+
};
149167
}
150168
};

src/component/connector/redis.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const redis = require("redis");
22
const util = require("node:util");
33
const debug = require("../../lib/debug");
4+
const { sleep } = require("../../lib/util");
45
const validateConnectionOptions = require("./connection-validator");
56

67

@@ -45,14 +46,30 @@ module.exports = class RedisStore {
4546
);
4647
}
4748

48-
try {
49-
debug("Connecting to Redis...");
49+
let attempts = 0;
50+
const client = this.getClient();
51+
const maxAttempts = this.#options.maxConnectionAttempts || 5;
5052

51-
await this.getClient()?.connect();
53+
while(attempts < maxAttempts) {
54+
try {
55+
debug("Connecting to Redis...");
5256

53-
debug("Redis connection established.");
54-
} catch(e) {
55-
debug(`Redis connection error: ${util.inspect(e)}`);
57+
await client.connect();
58+
59+
debug("Redis connection established.");
60+
} catch(e) {
61+
attempts++;
62+
63+
debug(`Redis connection error: ${util.inspect(e)}`);
64+
debug(`Retrying connection to Redis (${attempts}/${maxAttempts}) attempts`);
65+
66+
if(attempts === maxAttempts) {
67+
debug(`Failed to connect to Redis after ${maxAttempts} attempts. Exiting...`);
68+
process.exit(1);
69+
}
70+
71+
await sleep(1000 * attempts); // Exponential backoff
72+
}
5673
}
5774
}
5875

@@ -199,6 +216,10 @@ module.exports = class RedisStore {
199216

200217
debug("Redis connection options validated.");
201218

202-
return { ...validatedOptions, url: options?.url };
219+
return {
220+
...validatedOptions,
221+
url: options?.url,
222+
maxConnectionAttempts: options?.maxConnectionAttempts,
223+
};
203224
}
204225
};

src/component/connector/sequelize.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const util = require("node:util");
22
const { Sequelize } = require("sequelize");
33
const debug = require("../../lib/debug");
4+
const { sleep } = require("../../lib/util");
45
const validateConnectionOptions = require("./connection-validator");
56

67

@@ -110,18 +111,32 @@ module.exports = class SequelizeStore {
110111
* @return {resource} a (mongoose) connection instance
111112
*/
112113
async connect() {
114+
let attempts = 0;
115+
const maxAttempts = this.#options.maxConnectionAttempts || 5;
113116
const dbEngine = this.#dbEngine;
114117

115-
try {
116-
this.#debug(`Connecting to ${dbEngine}...`);
118+
while(attempts < maxAttempts) {
119+
try {
120+
this.#debug(`Connecting to ${dbEngine}...`);
117121

118-
await this.#db.authenticate();
122+
await this.#db.authenticate();
119123

120-
this.#connected = true;
124+
this.#connected = true;
121125

122-
this.#debug(`${dbEngine} connection established`);
123-
} catch(e) {
124-
this.#debug(`Sequelize connection error: ${util.inspect(e)}`);
126+
this.#debug(`${dbEngine} connection established`);
127+
} catch(e) {
128+
attempts++;
129+
130+
this.#debug(`Sequelize connection error: ${util.inspect(e)}`);
131+
this.#debug(`Retrying connection to ${dbEngine} (${attempts}/${maxAttempts}) attempts`);
132+
133+
if(attempts === maxAttempts) {
134+
this.#debug(`Failed to connect to ${dbEngine} after ${maxAttempts} attempts. Exiting...`);
135+
process.exit(1);
136+
}
137+
138+
await sleep(1000 * attempts); // Exponential backoff
139+
}
125140
}
126141

127142
return this.#db;
@@ -187,7 +202,11 @@ module.exports = class SequelizeStore {
187202

188203
this.#debug("Sequelize connection options validated.");
189204

190-
return validatedOptions;
205+
return {
206+
...validatedOptions,
207+
url: options.url,
208+
maxConnectionAttempts: options.maxConnectionAttempts
209+
};
191210
}
192211

193212
#debug(message) {

src/factory/cache/redis-cache.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const RedisStore = require("../../component/connector/redis");
2020
* @param {String} [options.credentials.url]: full DSN of the Redis server
2121
* If the [options.credentials.url] is set, it is used instead
2222
* and the other credential options are ignored.
23+
* @param {Number} [options.maxConnectionAttempts]: The maximum number of times
24+
* to attempt connecting before exiting.
2325
* @return {Object} with methods: set(), get(), unset(), contains(), and client().
2426
*/
2527
module.exports = function createRedisStore(options) {

src/lib/util.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use strict";
2+
3+
module.exports = {
4+
sleep,
5+
};
6+
7+
function sleep(ms) {
8+
return new Promise(resolve => setTimeout(resolve, ms));
9+
}

0 commit comments

Comments
 (0)