Просмотр исходного кода

[web] retry fetching initial compile from cache response (#25436)

* [web] move building of compile from cache response into manager

* [web] retry fetching initial compile from cache response

GitOrigin-RevId: b4dc89f1b91d99e869c0c7789881dc72d8a5761f
Jakob Ackermann 1 год назад
Родитель
Сommit
8d4f258494

+ 11 - 57
services/web/app/src/Features/Compile/ClsiCacheController.js

@@ -1,8 +1,7 @@
-const { NotFoundError } = require('../Errors/Errors')
+const { NotFoundError, ResourceGoneError } = require('../Errors/Errors')
 const {
   fetchStreamWithResponse,
   RequestFailedError,
-  fetchJson,
 } = require('@overleaf/fetch-utils')
 const Path = require('path')
 const { pipeline } = require('stream/promises')
@@ -110,61 +109,14 @@ async function getLatestBuildFromCache(req, res) {
   const userId = CompileController._getUserIdForCompile(req)
   try {
     const {
-      internal: { location: metaLocation },
-      external: { isUpToDate, allFiles, zone, shard },
-    } = await ClsiCacheManager.getLatestBuildFromCache(
-      projectId,
-      userId,
-      'output.overleaf.json'
-    )
-
-    if (!isUpToDate) return res.sendStatus(410)
-
-    const meta = await fetchJson(metaLocation, {
-      signal: AbortSignal.timeout(5 * 1000),
-    })
-
-    const [, editorId, buildId] = metaLocation.match(
-      /\/build\/([a-f0-9-]+?)-([a-f0-9]+-[a-f0-9]+)\//
-    )
-
-    let baseURL = `/project/${projectId}`
-    if (userId) {
-      baseURL += `/user/${userId}`
-    }
-
-    const { ranges, contentId, clsiServerId, compileGroup, size, options } =
-      meta
+      zone,
+      outputFiles,
+      compileGroup,
+      clsiServerId,
+      clsiCacheShard,
+      options,
+    } = await ClsiCacheManager.getLatestCompileResult(projectId, userId)
 
-    const outputFiles = allFiles
-      .filter(
-        path => path !== 'output.overleaf.json' && path !== 'output.tar.gz'
-      )
-      .map(path => {
-        const f = {
-          url: `${baseURL}/build/${editorId}-${buildId}/output/${path}`,
-          downloadURL: `/download/project/${projectId}/build/${editorId}-${buildId}/output/cached/${path}`,
-          build: buildId,
-          path,
-          type: path.split('.').pop(),
-        }
-        if (path === 'output.pdf') {
-          Object.assign(f, {
-            size,
-            editorId,
-          })
-          if (clsiServerId !== shard) {
-            // Enable PDF caching and attempt to download from VM first.
-            // (clsi VMs do not have the editorId in the path on disk, omit it).
-            Object.assign(f, {
-              url: `${baseURL}/build/${buildId}/output/output.pdf`,
-              ranges,
-              contentId,
-            })
-          }
-        }
-        return f
-      })
     let { pdfCachingMinChunkSize, pdfDownloadDomain } =
       await CompileController._getSplitTestOptions(req, res)
     pdfDownloadDomain += `/zone/${zone}`
@@ -174,7 +126,7 @@ async function getLatestBuildFromCache(req, res) {
       outputFiles,
       compileGroup,
       clsiServerId,
-      clsiCacheShard: shard,
+      clsiCacheShard,
       pdfDownloadDomain,
       pdfCachingMinChunkSize,
       options,
@@ -182,6 +134,8 @@ async function getLatestBuildFromCache(req, res) {
   } catch (err) {
     if (err instanceof NotFoundError) {
       res.sendStatus(404)
+    } else if (err instanceof ResourceGoneError) {
+      res.sendStatus(410)
     } else {
       throw err
     }

+ 95 - 1
services/web/app/src/Features/Compile/ClsiCacheManager.js

@@ -1,11 +1,12 @@
 const _ = require('lodash')
-const { NotFoundError } = require('../Errors/Errors')
+const { NotFoundError, ResourceGoneError } = require('../Errors/Errors')
 const ClsiCacheHandler = require('./ClsiCacheHandler')
 const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
 const ProjectGetter = require('../Project/ProjectGetter')
 const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 const UserGetter = require('../User/UserGetter')
 const Settings = require('@overleaf/settings')
+const { fetchJson, RequestFailedError } = require('@overleaf/fetch-utils')
 
 /**
  * Get the most recent build and metadata
@@ -50,6 +51,98 @@ async function getLatestBuildFromCache(projectId, userId, filename, signal) {
   }
 }
 
+class MetaFileExpiredError extends NotFoundError {}
+
+async function getLatestCompileResult(projectId, userId) {
+  const signal = AbortSignal.timeout(15_000)
+  for (let attempt = 0; attempt < 3; attempt++) {
+    try {
+      return await tryGetLatestCompileResult(projectId, userId, signal)
+    } catch (err) {
+      if (err instanceof MetaFileExpiredError) {
+        continue
+      }
+      throw err
+    }
+  }
+  throw new NotFoundError()
+}
+
+async function tryGetLatestCompileResult(projectId, userId, signal) {
+  const {
+    internal: { location: metaLocation },
+    external: { isUpToDate, allFiles, zone, shard: clsiCacheShard },
+  } = await getLatestBuildFromCache(
+    projectId,
+    userId,
+    'output.overleaf.json',
+    signal
+  )
+  if (!isUpToDate) throw new ResourceGoneError()
+
+  let meta
+  try {
+    meta = await fetchJson(metaLocation, {
+      signal: AbortSignal.timeout(5 * 1000),
+    })
+  } catch (err) {
+    if (err instanceof RequestFailedError && err.response.status === 404) {
+      throw new MetaFileExpiredError(
+        'build expired between listing and reading'
+      )
+    }
+    throw err
+  }
+
+  const [, editorId, buildId] = metaLocation.match(
+    /\/build\/([a-f0-9-]+?)-([a-f0-9]+-[a-f0-9]+)\//
+  )
+  const { ranges, contentId, clsiServerId, compileGroup, size, options } = meta
+
+  let baseURL = `/project/${projectId}`
+  if (userId) {
+    baseURL += `/user/${userId}`
+  }
+
+  const outputFiles = allFiles
+    .filter(path => path !== 'output.overleaf.json' && path !== 'output.tar.gz')
+    .map(path => {
+      const f = {
+        url: `${baseURL}/build/${editorId}-${buildId}/output/${path}`,
+        downloadURL: `/download/project/${projectId}/build/${editorId}-${buildId}/output/cached/${path}`,
+        build: buildId,
+        path,
+        type: path.split('.').pop(),
+      }
+      if (path === 'output.pdf') {
+        Object.assign(f, {
+          size,
+          editorId,
+        })
+        if (clsiServerId !== clsiCacheShard) {
+          // Enable PDF caching and attempt to download from VM first.
+          // (clsi VMs do not have the editorId in the path on disk, omit it).
+          Object.assign(f, {
+            url: `${baseURL}/build/${buildId}/output/output.pdf`,
+            ranges,
+            contentId,
+          })
+        }
+      }
+      return f
+    })
+
+  return {
+    allFiles,
+    zone,
+    outputFiles,
+    compileGroup,
+    clsiServerId,
+    clsiCacheShard,
+    options,
+  }
+}
+
 /**
  * Collect metadata and prepare the clsi-cache for the given project.
  *
@@ -109,5 +202,6 @@ async function prepareClsiCache(
 
 module.exports = {
   getLatestBuildFromCache,
+  getLatestCompileResult,
   prepareClsiCache,
 }

+ 3 - 0
services/web/app/src/Features/Errors/Errors.js

@@ -41,6 +41,8 @@ class ServiceNotConfiguredError extends BackwardCompatibleError {}
 
 class TooManyRequestsError extends BackwardCompatibleError {}
 
+class ResourceGoneError extends BackwardCompatibleError {}
+
 class DuplicateNameError extends OError {}
 
 class InvalidNameError extends BackwardCompatibleError {}
@@ -319,6 +321,7 @@ module.exports = {
   ForbiddenError,
   ServiceNotConfiguredError,
   TooManyRequestsError,
+  ResourceGoneError,
   DuplicateNameError,
   InvalidNameError,
   UnsupportedFileTypeError,