Sfoglia il codice sorgente

[web] resync_projects: use the secondaries for all reads (#33684)

* [docstore] add useSecondary flag to projectHasRanges

The rev-check for unarchiving always consults with the primary.

Two extra changes:
- Add a projection argument to peekDoc in order to skip lines download
   from projectHasRanges.
- Add one retry to peekDoc to reduce chances of surfacing a rev-check
   violation.

* [web] resync_projects: use the secondaries for all reads

* [web] add default value for useSecondary

* [docstore] add default value for useSecondary

* [k8s] docstore: set MONGO_HAS_SECONDARIES=true

GitOrigin-RevId: f15ec4fdc1cabe74c1eab87bec85f28d6f7a587d
Jakob Ackermann 3 mesi fa
parent
commit
75a12dda17

+ 40 - 16
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)

+ 13 - 2
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 })
 }
 

+ 8 - 2
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

+ 8 - 1
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,

+ 1 - 0
services/docstore/config/settings.defaults.cjs

@@ -17,6 +17,7 @@ const Settings = {
     options: {
       monitorCommands: true,
     },
+    hasSecondaries: process.env.MONGO_HAS_SECONDARIES === 'true',
   },
 
   docstore: {

+ 3 - 1
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

+ 5 - 5
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<boolean>}
  */
 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) {