Files
overleaf-cep/services/web/app/src/Features/Compile/ClsiCacheController.mjs
Jakob Ackermann 02391c6c51 [clsi] prepare for clsi-cache survey (#29274)
* [clsi] add stats and timings to compile response from clsi-cache

* [clsi] set downloadedFromCache when previously downloaded for synctex

Assumption: every compile will emit an output.log. When the output.log
is missing, but the output.synctex.gz exists, it must have been
downloaded from the cache.
GitOrigin-RevId: 41ea34880931e3c43dda3bc9eb26c0d02054894d
2025-10-23 08:05:43 +00:00

161 lines
4.1 KiB
JavaScript

import { NotFoundError, ResourceGoneError } from '../Errors/Errors.js'
import {
fetchStreamWithResponse,
RequestFailedError,
} from '@overleaf/fetch-utils'
import Path from 'node:path'
import { pipeline } from 'node:stream/promises'
import logger from '@overleaf/logger'
import ClsiCacheManager from './ClsiCacheManager.js'
import CompileController from './CompileController.mjs'
import { expressify } from '@overleaf/promise-utils'
import ClsiCacheHandler from './ClsiCacheHandler.js'
import ProjectGetter from '../Project/ProjectGetter.js'
import { MeteredStream } from '@overleaf/stream-utils'
import Metrics from '@overleaf/metrics'
/**
* Download a file from a specific build on the clsi-cache.
*
* @param req
* @param res
* @return {Promise<*>}
*/
async function downloadFromCache(req, res) {
const { Project_id: projectId, buildId, filename } = req.params
const userId = CompileController._getUserIdForCompile(req)
const signal = AbortSignal.timeout(60 * 1000)
let location, projectName
try {
;[{ location }, { name: projectName }] = await Promise.all([
ClsiCacheHandler.getOutputFile(
projectId,
userId,
buildId,
filename,
signal
),
ProjectGetter.promises.getProject(projectId, { name: 1 }),
])
} catch (err) {
if (err instanceof NotFoundError) {
// res.sendStatus() sends a description of the status as body.
// Using res.status().end() avoids sending that fake body.
return res.status(404).end()
} else {
throw err
}
}
const { stream, response } = await fetchStreamWithResponse(location, {
signal,
})
if (req.destroyed) {
// The client has disconnected already, avoid trying to write into the broken connection.
return
}
for (const key of ['Content-Length', 'Content-Type']) {
if (response.headers.has(key)) res.setHeader(key, response.headers.get(key))
}
const ext = Path.extname(filename)
res.attachment(
ext === '.pdf'
? `${CompileController._getSafeProjectName({ name: projectName })}.pdf`
: filename
)
try {
res.writeHead(response.status)
await pipeline(
stream,
new MeteredStream(Metrics, 'clsi_cache_egress', {
path: ClsiCacheHandler.getEgressLabel(filename),
}),
res
)
} catch (err) {
const reqAborted = Boolean(req.destroyed)
const streamingStarted = Boolean(res.headersSent)
if (!streamingStarted) {
if (err instanceof RequestFailedError) {
res.sendStatus(err.response.status)
} else {
res.sendStatus(500)
}
}
if (
streamingStarted &&
reqAborted &&
err.code === 'ERR_STREAM_PREMATURE_CLOSE'
) {
// Ignore noisy spurious error
return
}
logger.warn(
{
err,
projectId,
location,
filename,
reqAborted,
streamingStarted,
},
'CLSI-cache proxy error'
)
}
}
/**
* Prepare a compile response from the clsi-cache.
*
* @param req
* @param res
* @return {Promise<void>}
*/
async function getLatestBuildFromCache(req, res) {
const { Project_id: projectId } = req.params
const userId = CompileController._getUserIdForCompile(req)
try {
const {
zone,
outputFiles,
compileGroup,
clsiServerId,
clsiCacheShard,
options,
stats,
timings,
} = await ClsiCacheManager.getLatestCompileResult(projectId, userId)
let { pdfCachingMinChunkSize, pdfDownloadDomain } =
await CompileController._getSplitTestOptions(req, res)
pdfDownloadDomain += `/zone/${zone}`
res.json({
fromCache: true,
status: 'success',
outputFiles,
compileGroup,
clsiServerId,
clsiCacheShard,
pdfDownloadDomain,
pdfCachingMinChunkSize,
options,
stats,
timings,
})
} catch (err) {
if (err instanceof NotFoundError) {
res.sendStatus(404)
} else if (err instanceof ResourceGoneError) {
res.sendStatus(410)
} else {
throw err
}
}
}
export default {
downloadFromCache: expressify(downloadFromCache),
getLatestBuildFromCache: expressify(getLatestBuildFromCache),
}