Skip to content

Instantly share code, notes, and snippets.

@kevboutin
Last active September 8, 2024 16:42
Show Gist options
  • Save kevboutin/cc4e49b08a1ff763e5094dfb2a6dea6a to your computer and use it in GitHub Desktop.
Save kevboutin/cc4e49b08a1ff763e5094dfb2a6dea6a to your computer and use it in GitHub Desktop.
The DatabaseService class
const mongoose = require('mongoose').set('debug', process.env.NODE_ENV !== 'production');
/** @constant {boolean} */
const DB_BUFFER_COMMANDS = process.env.DB_BUFFER_COMMANDS === 'true';
/**
* Database options.
* @typedef {Object.<string, boolean|number|string>} DatabaseOptions
*/
/**
* Default database options.
* @type {Object.<string, boolean|number|string>}
*/
const defaultMongoOpts = {
/*
bufferCommands should be true and bufferMaxEntries should be -1 or non zero to
make autoReconnect behave correctly. Currently there is a bug when setting bufferCommands true
thus commenting it for now https://github.com/Automattic/mongoose/issues/9218
*/
bufferCommands: DB_BUFFER_COMMANDS,
readPreference: process.env.DB_READ_PREF || 'secondaryPreferred',
minPoolSize: parseInt(process.env.DB_MIN_POOL_SIZE, 10) || 2,
maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE, 10) || 6,
// The connectTimeoutMS sets the number of milliseconds a socket will stay inactive before closing
// during the connection phase of the driver
connectTimeoutMS: parseInt(process.env.DB_CONNECT_TIMEOUT, 10) || 2000,
// The socketTimeoutMS sets the number of milliseconds a socket will stay inactive after the driver
// has successfully connected before closing.
socketTimeoutMS: parseInt(process.env.DB_SOCKET_TIMEOUT, 10) || 120000,
// The maximum number of milliseconds that a connection can remain idle in the pool before being removed and closed.
maxIdleTimeMS: parseInt(process.env.DB_MAX_IDLE_TIME, 10) || 3550000,
};
/**
* @class DatabaseService
*/
class DatabaseService {
/**
* Creates a DatabaseService.
*
* @constructor
* @param {string} dbUri The database URI.
* @param {DatabaseOptions} databaseOpts The DatabaseOptions.
* @param {string} dbName The database name.
*/
constructor({ dbUri, databaseOpts, dbName }) {
this.dbUri = dbUri;
this.dbName = dbName;
this.databaseOpts = { ...defaultMongoOpts, ...databaseOpts };
this.connection = null;
this.initing = false;
}
/**
* Get the database name.
*
* @return {string} The database name.
*/
getDatabaseName() {
return this.dbName;
}
/**
* Set the database name. This will force a connection object change.
*
* @param {string} dbName The database name.
*/
setDatabaseName(dbName) {
this.dbName = dbName;
// The useDb() function will update the connection object.
if (this.connection) this.connection = this.connection.useDb(dbName, { useCache: true });
}
/**
* Returns a promise that returns a new connection. If a connection was already created, it will return a
* cached connection. Skip cached connection by setting requireNewConnection to true.
*
* @param {Object} context The context.
* @param {boolean} requireNewConnection True to return a new connection. This is usually needed when
* multiple concurrent transactions are needed. The connection needs to be managed by the
* caller (by closing it appropriately) since the DatabaseService loses all reference to it.
* @throws {Object} Throws an error if connection fails to open.
* @return {Promise<object>} The connection.
*/
async createConnection(context, requireNewConnection = false) {
const secureUri = this.dbUri.replace(/\/\/.*@/, '//***:***@');
if (requireNewConnection) {
console.log(`createConnection: Creating a new database connection to ${secureUri}.`);
const connection = await mongoose
.createConnection(`${this.dbUri}${this.dbName}`, this.databaseOpts)
.asPromise();
connection.on('disconnected', async () => {
console.log(
`Lost connection to ${secureUri} so closing database connection as part of cleanup.`,
);
await this.closeConnection(connection);
});
connection.on('error', (err) => {
console.log(`${TAG}::Error occurred with established connection.`, err);
});
return connection;
}
if (this.connection === null && !this.initing) {
this.initing = true;
console.log(`createConnection: Creating new connection and caching it to ${secureUri}.`);
this.connection = await mongoose
.createConnection(`${this.dbUri}${this.dbName}`, this.databaseOpts)
.asPromise();
const connectedDbName = this.connection.db.databaseName || null;
console.log(
`createConnection: connection was created. Total of ${mongoose.connections.length} connections to host ${this.connection.host} with db ${connectedDbName}.`,
);
this.initing = false;
this.connection.on('disconnected', async () => {
console.log(
`Lost connection to ${secureUri} so closing database connection as part of cleanup.`,
);
await this.closeConnection(this.connection);
});
this.connection.on('error', (err) => {
console.log(`${TAG}::Error occurred with established connection.`, err);
});
return this.connection;
}
if (await !this.isConnectionAlive(this.connection)) {
console.log(
`createConnection: Connection is not alive, creating new connection and caching it to ${secureUri}.`,
);
this.connection = await mongoose
.createConnection(`${this.dbUri}${this.dbName}`, this.databaseOpts)
.asPromise();
const connectedDbName = this.connection.db.databaseName || null;
console.log(
`createConnection: connection was created. Total of ${mongoose.connections.length} connections to host ${this.connection.host} with db ${connectedDbName}.`,
);
this.connection.on('disconnected', async () => {
console.log(
`${TAG}::Lost connection to ${secureUri} so closing database connection as part of cleanup.`,
);
await this.closeConnection(this.connection);
});
this.connection.on('error', (err) => {
console.log(`${TAG}::Error occurred with established connection.`, err);
});
return this.connection;
}
const connectedDbName = this.connection.db.databaseName || null;
console.log(
`Using cached connection. Total of ${mongoose.connections.length} connections to host ${this.connection.host} with db ${connectedDbName}.`,
);
return this.connection;
}
/**
* Determines if the database connection is alive/active.
*
* @param {Object} conn The database connection.
* @return {boolean} True if the connection is still considered active.
*/
async isConnectionAlive(conn) {
if (!conn) return false;
console.log(`Database connection readyState:`, conn.readyState);
// Return false if not connected or connecting
if (conn.readyState !== 1 && conn.readyState !== 2) return false;
try {
const adminUtil = conn.db.admin();
const result = await adminUtil.ping();
console.log("Ping result: ", result); // { ok: 1 }
return !!result?.ok === 1;
} catch (error) {
console.log("Error with ping: ", error);
return false;
}
}
/**
* Returns a DB connection by calling createConnection. If a function is passed, it will pass the connection
* to the function and run the function.
*
* @param {Object} context The context.
* @param {function} fn Function worker that accepts a connection. This is optional.
* @param {boolean} requireNewConnection True if we force a connection to be created.
* @return {Promise<object>} The result of the fn or the connection itself.
*/
async getConnection(context, fn, requireNewConnection = false) {
// If function is not passed, return the promise
if (!fn) {
return await this.createConnection(context, requireNewConnection);
}
// If function is passed, call the function by passing the connection to it
return await this.createConnection(context, requireNewConnection).then((conn) => fn(conn));
}
/**
* If connection is passed, close connection; else close the cached connection.
*
* @param {Object} connection An optional connection to close
* @return {Promise<Object>}
*/
async closeConnection(connection) {
if (connection) {
return await connection.close();
}
if (this.connection !== null) {
return await this.connection.close();
}
return null;
}
}
module.exports = DatabaseService;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment