backend/core/db/mongo/index.js
'use strict';
/* eslint-disable no-process-env */
const url = require('url');
const fs = require('fs');
const path = require('path');
const MongoClient = require('mongodb').MongoClient;
const mongoose = require('mongoose');
mongoose.Promise = require('q').Promise; // http://mongoosejs.com/docs/promises.html
const logger = require('../../../core').logger;
const config = require('../../../core').config;
const topic = require('../../../core').pubsub.local.topic('mongodb:connectionAvailable');
const configurationWatcher = require('./file-watcher');
const defaultConfig = config('default');
const ATTEMPTS_LIMIT = defaultConfig.db && defaultConfig.db.attemptsLimit ? defaultConfig.db.attemptsLimit : 100;
const MONGO_DEFAULT_HOST = 'localhost';
const MONGO_DEFAULT_PORT = 27017;
const MONGO_DEFAULT_DBNAME = 'esn';
let initialized = false;
let connected = false;
let dbConfigWatcher = null;
const models = {};
const schemas = {};
let reconnectAttemptCounter = 0;
let connectionLost = false;
module.exports = {
storeConfiguration,
validateConnection,
buildConnectionString,
getDefaultOptions,
init,
isInitalized,
isConnected,
models,
schemas,
mongoose
};
mongoose.connection.on('error', err => {
onConnectError(err);
initialized = false;
});
mongoose.connection.on('connected', err => {
logger.debug('Connected to MongoDB', err);
connected = true;
topic.publish();
reconnectAttemptCounter = 0;
connectionLost = false;
});
mongoose.connection.on('disconnected', () => {
if (!connectionLost) {
logger.debug('Connection to MongoDB has been lost');
connected = false;
connectionLost = true;
}
if (forceReconnect() && (reconnectAttemptCounter < ATTEMPTS_LIMIT)) {
logger.debug('Reconnecting to MongoDB');
reconnectAttemptCounter++;
setTimeout(mongooseConnect, _fibonacci(reconnectAttemptCounter) * 1000);
}
if (reconnectAttemptCounter === ATTEMPTS_LIMIT) {
logger.error(`Failed to connect to MongoDB ${ATTEMPTS_LIMIT} time - No more attempts - Please contact your administrator to restart the database server`);
}
});
fs.readdirSync(__dirname + '/models').forEach(filename => {
const stat = fs.statSync(__dirname + '/models/' + filename);
if (!stat.isFile()) { return; }
models[filename.replace('.js', '')] = require('./models/' + filename);
});
fs.readdirSync(__dirname + '/schemas').forEach(filename => {
const stat = fs.statSync(__dirname + '/schemas/' + filename);
if (!stat.isFile()) { return; }
schemas[filename.replace('.js', '')] = require('./schemas/' + filename);
});
function forceReconnect() {
if (process.env.MONGO_FORCE_RECONNECT) {
return process.env.MONGO_FORCE_RECONNECT;
}
return !!(defaultConfig.db && defaultConfig.db.forceReconnectOnDisconnect);
}
function onConnectError(err) {
if (!connectionLost || firstAttempt()) {
logger.error(`Failed to connect to MongoDB - Attempt #${reconnectAttemptCounter}/${ATTEMPTS_LIMIT} at ${Date()}: `, err);
} else {
logger.error(`Failed to connect to MongoDB - Attempt #${reconnectAttemptCounter}/${ATTEMPTS_LIMIT} at ${Date()} - Please contact your administrator to restart the database server`);
}
}
function _fibonacci(number) {
const sequence = [1, 1];
for (let i = 2; i < number; i++) {
sequence[i] = sequence[i - 1] + sequence[i - 2];
}
return sequence[number - 1];
}
function firstAttempt() {
return reconnectAttemptCounter === 1;
}
function getTimeout() {
return process.env.MONGO_TIMEOUT || 10000;
}
function getHost() {
return process.env.MONGO_HOST || MONGO_DEFAULT_HOST;
}
function getPort() {
return process.env.MONGO_PORT || MONGO_DEFAULT_PORT;
}
function getDbName() {
return process.env.MONGO_DBNAME || MONGO_DEFAULT_DBNAME;
}
function getUsername() {
return process.env.MONGO_USERNAME;
}
function getPassword() {
return process.env.MONGO_PASSWORD;
}
function openDatabase(connectionString, callback) {
MongoClient.connect(connectionString, (err, db) => {
if (err && db && ('close' in db)) {
db.close();
}
callback(err, db);
});
}
function insertDocument(db, collectionName, document, callback) {
const collection = db.collection(collectionName);
collection.insert(document, (err, coll) => {
if (err) {
db.close(() => {
//ignore error
});
}
return callback(err, coll);
});
}
function dropCollection(db, collectionName, callback) {
db.dropCollection(collectionName, err => {
db.close(() => {
//ignore error
});
return callback(err);
});
}
function buildConnectionString(hostname, port, dbname, username, password, connectionOptions) {
const timeout = getTimeout();
connectionOptions = connectionOptions || {
connectTimeoutMS: timeout,
socketTimeoutMS: timeout
};
const connectionHash = {
protocol: 'mongodb',
slashes: true,
hostname: hostname,
port: port,
pathname: '/' + dbname,
query: connectionOptions
};
if (username) {
connectionHash.auth = username + ':' + password;
}
return url.format(connectionHash);
}
function getConnectionStringFromEnvOrDefaults() {
return buildConnectionString(getHost(), getPort(), getDbName(), getUsername(), getPassword());
}
function getDbConfigurationFile() {
const root = path.resolve(__dirname + '/../../../..');
let dbConfigurationFile;
if (defaultConfig.core && defaultConfig.core.config && defaultConfig.core.config.db) {
dbConfigurationFile = path.resolve(root + '/' + defaultConfig.core.config.db);
} else {
dbConfigurationFile = root + '/config/db.json';
}
return dbConfigurationFile;
}
function storeConfiguration(configuration, callback) {
const dbConfigurationFile = getDbConfigurationFile();
const finalConfiguration = {};
finalConfiguration.connectionOptions = configuration.connectionOptions;
finalConfiguration.connectionString = buildConnectionString(configuration.hostname,
configuration.port,
configuration.dbname,
configuration.username,
configuration.password,
{});
fs.writeFile(dbConfigurationFile, JSON.stringify(finalConfiguration), err => {
if (err) {
logger.error('Cannot write database configuration file', dbConfigurationFile, err);
const error = new Error('Can not write database settings in ' + dbConfigurationFile);
return callback(error);
}
return callback(null, finalConfiguration);
});
}
/**
* Checks that we can connect to mongodb
*
* @param {string} hostname
* @param {string} port
* @param {string} dbname
* @param {string} username
* @param {string} password
* @param {function} callback
*/
function validateConnection(hostname, port, dbname, username, password, callback) {
const connectionString = buildConnectionString(hostname, port, dbname, username, password);
const collectionName = 'connectionTest';
const document = {test: true};
openDatabase(connectionString, (err, db) => {
if (err) {
return callback(err);
}
insertDocument(db, collectionName, document, err => {
if (err) {
return callback(err);
}
dropCollection(db, collectionName, callback);
});
});
}
function getDefaultOptions() {
const timeout = getTimeout();
return {
w: 1,
fsync: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
keepAlive: timeout,
connectTimeoutMS: timeout,
auto_reconnect: true,
poolSize: 10
};
}
function getConnectionStringAndOptions() {
let dbConfig;
try {
dbConfig = config('db');
} catch (e) {
return false;
}
if (!dbConfig) {
return false;
}
// Note: erasing dbConfig.connectionString here is a dirty hack to avoid presenting the setup wizard to the user.
// See https://ci.linagora.com/linagora/lgs/openpaas/esn/issues/2412
dbConfig.connectionString = dbConfig.connectionString || getConnectionStringFromEnvOrDefaults();
const connectionOptions = dbConfig.connectionOptions || getDefaultOptions();
return {
url: dbConfig.connectionString,
options: connectionOptions
};
}
function mongooseConnect(reinit) {
if (defaultConfig.db && defaultConfig.db.reconnectOnConfigurationChange) {
if (!dbConfigWatcher) {
dbConfigWatcher = configurationWatcher(logger, getDbConfigurationFile(), reinit);
}
dbConfigWatcher();
}
const connectionInfos = getConnectionStringAndOptions();
if (!connectionInfos) {
return false;
}
try {
logger.debug('launch mongoose.connect on ' + connectionInfos.url);
mongoose.connect(connectionInfos.url, connectionInfos.options);
} catch (e) {
onConnectError(e);
return false;
}
initialized = true;
return true;
}
function init() {
// Register health check service
const healthCheck = require('./health-check');
healthCheck.register();
function reinit() {
logger.info('Database configuration updated, reloading mongoose');
config.clear();
init();
}
if (initialized) {
mongoose.disconnect(() => {
initialized = false;
mongooseConnect(reinit);
});
return;
}
return mongooseConnect(reinit);
}
function isInitalized() {
return initialized;
}
function isConnected() {
return connected;
}