[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
This commit is contained in:
Jakob Ackermann
2025-05-08 17:01:48 +02:00
committed by Copybot
parent dc73a18ca4
commit 8d4f258494
3 changed files with 109 additions and 58 deletions

View File

@@ -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'
)
zone,
outputFiles,
compileGroup,
clsiServerId,
clsiCacheShard,
options,
} = await ClsiCacheManager.getLatestCompileResult(projectId, userId)
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
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
}

View File

@@ -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,
}

View File

@@ -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,