From 68bde6e73c8176984b7bd6304a0438bbea48012f Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Wed, 11 Sep 2024 22:48:08 +0800 Subject: [PATCH] feat(mongoose): force `secondaryPreferred` if no secondaries in cluster - Trigger early load of mongoose before any other import and implicit load of mongoose model - Add a global schema plugin that inspects schema for read preference - if `secondary`, check the cluster using mongoose connection - if cluster without secondaries, force read pref to be `secondaryPreferred` --- replacements/src/app/loaders/mongoose.ts | 66 +++++++++++++++++------- replacements/src/app/server.ts | 27 ++++++++++ 2 files changed, 73 insertions(+), 20 deletions(-) create mode 100755 replacements/src/app/server.ts diff --git a/replacements/src/app/loaders/mongoose.ts b/replacements/src/app/loaders/mongoose.ts index 740e903..81ba048 100644 --- a/replacements/src/app/loaders/mongoose.ts +++ b/replacements/src/app/loaders/mongoose.ts @@ -1,9 +1,18 @@ import { MongoMemoryServer } from 'mongodb-memory-server-core' -import mongoose, { Connection } from 'mongoose' +import mongoose, { Connection, type Schema } from 'mongoose' import config from '../config/config' import { createLoggerWithLabel } from '../config/logger' +const readPrefSecondarySchemas = [] as Schema[] + +// Invoke this at module load time +mongoose.plugin((schema) => { + if (schema.get('read') === 'secondary') { + readPrefSecondarySchemas.push(schema) + } +}) + const logger = createLoggerWithLabel(module) export default async (): Promise => { @@ -90,27 +99,44 @@ export default async (): Promise => { }) }) - // Seed db with initial agency if we have none + mongoose.set('debug', (collectionName, method, query, doc) => { + logger.info({ + message: `${collectionName}.${method}`, + meta: { + action: 'mongoose', + query, + doc, + }, + }) + }) + + // Inspect cluster topology from client + const client = mongoose.connection.getClient() + const { + description: { servers, type }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } = client.topology + if ( - process.env.INIT_AGENCY_DOMAIN && - process.env.INIT_AGENCY_SHORTNAME && - process.env.INIT_AGENCY_FULLNAME + type === 'ReplicaSetWithPrimary' && + !servers + .values() + .some(({ type }: { type: string }) => type === 'RSSecondary') ) { - const Agency = mongoose.model('Agency') - const agencyCount = await Agency.count() - if (agencyCount === 0) { - await mongoose.connection.collection(Agency.collection.name).updateOne( - { shortName: process.env.INIT_AGENCY_SHORTNAME }, - { - $setOnInsert: { - shortName: process.env.INIT_AGENCY_SHORTNAME, - fullName: process.env.INIT_AGENCY_FULLNAME, - emailDomain: [process.env.INIT_AGENCY_DOMAIN], - logo: '/logo192.png', - }, - }, - { upsert: true }, - ) + // There are no secondary nodes in ReplicaSetWithPrimary cluster. + // Queries with `secondary` read preferences will fail, so rewrite + // those to be `secondaryPreferred`. + logger.warn({ + message: + 'ReplicaSetWithPrimary cluster has no secondary nodes. ' + + 'Forcing secondary read preference to secondaryPreferred.', + meta: { + action: 'schema', + }, + }) + for (const schema of readPrefSecondarySchemas) { + schema.set('read', 'secondaryPreferred') } } diff --git a/replacements/src/app/server.ts b/replacements/src/app/server.ts new file mode 100755 index 0000000..9c034dc --- /dev/null +++ b/replacements/src/app/server.ts @@ -0,0 +1,27 @@ +import './loaders/datadog-tracer' +import './loaders/mongoose' + +import config from './config/config' +import { createLoggerWithLabel } from './config/logger' +import loadApp from './loaders' + +const logger = createLoggerWithLabel(module) + +const initServer = async () => { + const app = await loadApp() + + // Configure aws-sdk based on environment + // sdk is later used to upload images to S3 + await config.configureAws() + + app.listen(config.port) + + logger.info({ + message: `[${config.nodeEnv}] Connected to port ${config.port}`, + meta: { + action: 'initServer', + }, + }) +} + +void initServer()