diff --git a/services/docstore/app/js/DocManager.js b/services/docstore/app/js/DocManager.js index 5a409255e5..e8540cf3cb 100644 --- a/services/docstore/app/js/DocManager.js +++ b/services/docstore/app/js/DocManager.js @@ -77,15 +77,17 @@ const DocManager = { }, // returns the doc without any version information - async _peekRawDoc(projectId, docId) { - const doc = await MongoManager.findDoc(projectId, docId, { - lines: true, - rev: true, - deleted: true, - version: true, - ranges: true, - inS3: true, - }) + async _peekRawDoc(projectId, docId, projection, useSecondary) { + const doc = await MongoManager.findDoc( + projectId, + docId, + { + ...projection, + rev: true, + inS3: true, + }, + useSecondary + ) if (doc == null) { throw new Errors.NotFoundError( @@ -97,6 +99,8 @@ const DocManager = { // skip the unarchiving to mongo when getting a doc const archivedDoc = await DocArchive.getDoc(projectId, docId) Object.assign(doc, archivedDoc) + // Always use the primary for the rev-check. + await MongoManager.checkRevUnchanged(doc) } return doc @@ -104,10 +108,21 @@ const DocManager = { // get the doc from mongo if possible, or from the persistent store otherwise, // without unarchiving it (avoids unnecessary writes to mongo) - async peekDoc(projectId, docId) { - const doc = await DocManager._peekRawDoc(projectId, docId) - await MongoManager.checkRevUnchanged(doc) - return doc + async peekDoc(projectId, docId, projection, useSecondary = false) { + try { + return await DocManager._peekRawDoc( + projectId, + docId, + projection, + useSecondary + ) + } catch (err) { + if (err instanceof Errors.DocModifiedError) { + // Try again once on rev mismatch. Always use the primary for retries. + return await DocManager._peekRawDoc(projectId, docId, projection, false) + } + throw err + } }, async getDocLines(projectId, docId) { @@ -181,11 +196,20 @@ const DocManager = { return Array.from(userIds) }, - async projectHasRanges(projectId) { - const docs = await MongoManager.getProjectsDocs(projectId, {}, { _id: 1 }) + async projectHasRanges(projectId, useSecondary) { + const docs = await MongoManager.getProjectsDocs( + projectId, + { useSecondary }, + { _id: 1 } + ) const docIds = docs.map(doc => doc._id) for (const docId of docIds) { - const doc = await DocManager.peekDoc(projectId, docId) + const doc = await DocManager.peekDoc( + projectId, + docId, + { ranges: true }, + useSecondary + ) if ( (doc.ranges?.comments != null && doc.ranges.comments.length > 0) || (doc.ranges?.changes != null && doc.ranges.changes.length > 0) diff --git a/services/docstore/app/js/HttpController.js b/services/docstore/app/js/HttpController.js index d7de51bf51..497cb37cff 100644 --- a/services/docstore/app/js/HttpController.js +++ b/services/docstore/app/js/HttpController.js @@ -22,7 +22,14 @@ async function getDoc(req, res) { async function peekDoc(req, res) { const { doc_id: docId, project_id: projectId } = req.params logger.debug({ projectId, docId }, 'peeking doc') - const doc = await DocManager.peekDoc(projectId, docId) + const doc = await DocManager.peekDoc(projectId, docId, { + deleted: true, + inS3: true, + lines: true, + ranges: true, + rev: 1, + version: true, + }) res.setHeader('x-doc-status', doc.inS3 ? 'archived' : 'active') res.json(_buildDocView(doc)) } @@ -121,7 +128,11 @@ async function getTrackedChangesUserIds(req, res) { async function projectHasRanges(req, res) { const { project_id: projectId } = req.params - const projectHasRanges = await DocManager.projectHasRanges(projectId) + const useSecondary = req.query.useSecondary === 'true' + const projectHasRanges = await DocManager.projectHasRanges( + projectId, + useSecondary + ) res.json({ projectHasRanges }) } diff --git a/services/docstore/app/js/MongoManager.js b/services/docstore/app/js/MongoManager.js index a6833c9a7f..2e6b340d7b 100644 --- a/services/docstore/app/js/MongoManager.js +++ b/services/docstore/app/js/MongoManager.js @@ -7,13 +7,18 @@ const { db, ObjectId, BSON } = mongodb const ARCHIVING_LOCK_DURATION_MS = Settings.archivingLockDurationMs -async function findDoc(projectId, docId, projection) { +function readPreference(useSecondary) { + if (useSecondary) return { readPreference: mongodb.READ_PREFERENCE_SECONDARY } + return {} +} + +async function findDoc(projectId, docId, projection, useSecondary = false) { const doc = await db.docs.findOne( { _id: new ObjectId(docId.toString()), project_id: new ObjectId(projectId.toString()), }, - { projection } + { projection, ...readPreference(useSecondary) } ) if (doc && projection.version && !doc.version) { doc.version = 0 @@ -45,6 +50,7 @@ async function getProjectsDocs(projectId, options, projection) { } const queryOptions = { projection, + ...readPreference(options.useSecondary), } if (options.limit) { queryOptions.limit = options.limit diff --git a/services/docstore/app/js/mongodb.js b/services/docstore/app/js/mongodb.js index 9bd0388f81..31530f0fc5 100644 --- a/services/docstore/app/js/mongodb.js +++ b/services/docstore/app/js/mongodb.js @@ -6,7 +6,7 @@ import Settings from '@overleaf/settings' import MongoUtils from '@overleaf/mongo-utils' import mongodb from 'mongodb-legacy' -const { MongoClient, ObjectId, BSON } = mongodb +const { MongoClient, ObjectId, BSON, ReadPreference } = mongodb const mongoClient = new MongoClient(Settings.mongo.url, Settings.mongo.options) const mongoDb = mongoClient.db() @@ -21,7 +21,14 @@ async function cleanupTestDatabase() { await MongoUtils.cleanupTestDatabase(mongoClient) } +const READ_PREFERENCE_PRIMARY = ReadPreference.primary.mode +const READ_PREFERENCE_SECONDARY = Settings.mongo.hasSecondaries + ? ReadPreference.secondary.mode + : ReadPreference.secondaryPreferred.mode + export default { + READ_PREFERENCE_PRIMARY, + READ_PREFERENCE_SECONDARY, db, mongoClient, ObjectId, diff --git a/services/docstore/config/settings.defaults.cjs b/services/docstore/config/settings.defaults.cjs index 018ecedc02..ee622b52e9 100644 --- a/services/docstore/config/settings.defaults.cjs +++ b/services/docstore/config/settings.defaults.cjs @@ -17,6 +17,7 @@ const Settings = { options: { monitorCommands: true, }, + hasSecondaries: process.env.MONGO_HAS_SECONDARIES === 'true', }, docstore: { diff --git a/services/web/app/src/Features/Docstore/DocstoreManager.mjs b/services/web/app/src/Features/Docstore/DocstoreManager.mjs index c151a10d51..bd4a594611 100644 --- a/services/web/app/src/Features/Docstore/DocstoreManager.mjs +++ b/services/web/app/src/Features/Docstore/DocstoreManager.mjs @@ -323,10 +323,12 @@ async function updateDoc( * Asks docstore whether any doc in the project has ranges * * @param {string} projectId + * @param {boolean} useSecondary */ -async function projectHasRanges(projectId) { +async function projectHasRanges(projectId, useSecondary = false) { const url = new URL(settings.apis.docstore.url) url.pathname = path.posix.join('project', projectId, 'has-ranges') + if (useSecondary) url.searchParams.set('useSecondary', 'true') try { const body = await fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) }) return body.projectHasRanges diff --git a/services/web/scripts/history/resync_projects.mjs b/services/web/scripts/history/resync_projects.mjs index c9b8030792..e2efded852 100644 --- a/services/web/scripts/history/resync_projects.mjs +++ b/services/web/scripts/history/resync_projects.mjs @@ -3,7 +3,6 @@ import minimist from 'minimist' import { scriptRunner } from '../lib/ScriptRunner.mjs' import logger from '@overleaf/logger' -import ProjectGetter from '../../app/src/Features/Project/ProjectGetter.mjs' import { db, ObjectId, @@ -268,7 +267,7 @@ async function hasHistoryMetadata(projectId) { if (await hasLinkedFileData(projectId)) { return true } - if (await DocstoreManager.promises.projectHasRanges(projectId)) { + if (await DocstoreManager.promises.projectHasRanges(projectId, true)) { return true } return false @@ -296,10 +295,11 @@ async function hasHistoryMetadata(projectId) { * @returns {Promise} */ async function hasLinkedFileData(projectId) { - const project = await ProjectGetter.promises.getProjectWithoutLock( - projectId, + const project = await db.projects.findOne( + { _id: new ObjectId(projectId) }, { - rootFolder: 1, + projection: { rootFolder: 1 }, + readPreference: READ_PREFERENCE_SECONDARY, } ) if (!project) {