Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: retry initial MongoDB connections #5807

Merged
merged 1 commit into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"mongodb": "~3.3.3",
"node-fetch": "~2.6.0",
"object-hash": "~1.3.1",
"promise-retry": "^1.1.1",
"querystring": "~0.2.0",
"ramda": "~0.26.1",
"semver": "~6.3.0",
Expand Down
10 changes: 2 additions & 8 deletions src/core/ReactionAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Logger from "@reactioncommerce/logger";
import appEvents from "./util/appEvents.js";
import getAbsoluteUrl from "./util/getAbsoluteUrl.js";
import initReplicaSet from "./util/initReplicaSet.js";
import mongoConnectWithRetry from "./util/mongoConnectWithRetry.js";
import config from "./config.js";
import createApolloServer from "./createApolloServer.js";
import coreResolvers from "./graphql/resolvers/index.js";
Expand All @@ -31,8 +32,6 @@ const {

const debugLevels = ["DEBUG", "TRACE"];

const { MongoClient } = mongodb;

const optionsSchema = new SimpleSchema({
"httpServer": {
type: Object,
Expand Down Expand Up @@ -214,12 +213,7 @@ export default class ReactionAPI {
const dbUrl = mongoUrl.slice(0, lastSlash);
const dbName = mongoUrl.slice(lastSlash + 1);

const client = await MongoClient.connect(dbUrl, {
useNewUrlParser: true
// Uncomment this after this `mongodb` pkg bug is fixed:
// https://jira.mongodb.org/browse/NODE-2249
// useUnifiedTopology: true
});
const client = await mongoConnectWithRetry(dbUrl);

this.mongoClient = client;
this.setMongoDatabase(client.db(dbName));
Expand Down
33 changes: 5 additions & 28 deletions src/core/util/initReplicaSet.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { URL } from "url";
import mongodb from "mongodb";
import Logger from "@reactioncommerce/logger";

const { MongoClient } = mongodb;
import mongoConnectWithRetry from "./mongoConnectWithRetry.js";

/**
* @summary Sleep for some milliseconds
Expand Down Expand Up @@ -30,12 +28,7 @@ async function connect(parsedUrl) {
dbParsedUrl.pathname = "";
const dbUrl = dbParsedUrl.toString();

const client = await MongoClient.connect(dbUrl, {
useNewUrlParser: true
// Uncomment this after this `mongodb` pkg bug is fixed:
// https://jira.mongodb.org/browse/NODE-2249
// useUnifiedTopology: true
});
const client = await mongoConnectWithRetry(dbUrl);

return {
client,
Expand All @@ -51,31 +44,14 @@ async function connect(parsedUrl) {
* @returns {Promise} indication of success/failure
*/
export default async function initReplicaSet(mongoUrl) {
Logger.info("Initializing MongoDB replica set...");
const parsedUrl = new URL(mongoUrl);

// eventually we should set `stopped = true` when the developer interrupts
// the process
const stopped = false;

const canConnectTimestamp = Date.now();
let db;
let client;
while (!stopped) {
try {
// eslint-disable-next-line no-await-in-loop
({ client, db } = await connect(parsedUrl));
} catch (error) {
if (Date.now() - canConnectTimestamp > 60000) {
Logger.error(error);
throw new Error("Unable to connect to MongoDB server after 1 minute");
}
}

if (client) break;

// eslint-disable-next-line no-await-in-loop
await sleep(100);
}
const { client, db } = await connect(parsedUrl);

const replSetConfiguration = {
_id: "rs0",
Expand Down Expand Up @@ -132,4 +108,5 @@ export default async function initReplicaSet(mongoUrl) {
}

await client.close();
Logger.info("Finished MongoDB replica set initialization");
}
45 changes: 45 additions & 0 deletions src/core/util/mongoConnectWithRetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Logger from "@reactioncommerce/logger";
import mongodb from "mongodb";
import promiseRetry from "promise-retry";

const { MongoClient } = mongodb;

const mongoInitialConnectRetries = 10;

/**
* @summary The MongoDB driver will auto-reconnect but not on the first connect.
* If the first connection fails, it throws. Because we expect to be in a dynamic
* Docker environment in which containers may start and slightly different times,
* we want to try to reconnect for a bit, even on the first connect.
* @param {String} url MongoDB URL
* @return {Object} Client
*/
export default function mongoConnectWithRetry(url) {
return promiseRetry((retry, number) => {
if (number > 1) {
Logger.info(`Retrying connect to MongoDB... (${number - 1} of ${mongoInitialConnectRetries})`);
} else {
Logger.info("Connecting to MongoDB...");
}

return MongoClient.connect(url, {
useNewUrlParser: true
// Uncomment this after this `mongodb` pkg bug is fixed:
// https://jira.mongodb.org/browse/NODE-2249
// useUnifiedTopology: true
}).then((client) => {
Logger.info("Connected to MongoDB");
return client;
}).catch((error) => {
if (error.name === "MongoNetworkError") {
retry(error);
} else {
throw error;
}
});
}, {
factor: 1,
minTimeout: 3000,
retries: mongoInitialConnectRetries
}).catch((error) => { throw error; });
}
5 changes: 4 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ async function runApp() {
Logger.info(`GraphQL subscriptions ready at ${app.graphQLServerSubscriptionUrl} (port ${app.serverPort || "unknown"})`);
}

runApp().catch(Logger.error.bind(Logger));
runApp().catch((error) => {
Logger.error(error);
process.exit(1); // eslint-disable-line no-process-exit
});