Files
overleaf-cep/services/clsi/app/js/CompileController.js
Jakob Ackermann b538d56591 [clsi-cache] backend (#24388)
* [clsi-cache] initial revision of the clsi-cache service

* [clsi] send output files to clsi-cache and import from clsi-cache

* [web] pass editorId to clsi

* [web] clear clsi-cache when clearing clsi cache

* [web] add split-tests for controlling clsi-cache rollout

* [web] populate clsi-cache when cloning/creating project from template

* [clsi-cache] produce less noise when populating cache hits 404

* [clsi-cache] push docker image to AR

* [clsi-cache] push docker image to AR

* [clsi-cache] allow compileGroup in job payload

* [clsi-cache] set X-Zone header from latest endpoint

* [clsi-cache] use method POST for /enqueue endpoint

* [web] populate clsi-cache in zone b with template data

* [clsi-cache] limit number of editors per project/user folder to 10

* [web] clone: populate the clsi-cache unless the TeXLive release changed

* [clsi-cache] keep user folder when clearing cache as anonymous user

* [clsi] download old output.tar.gz when synctex finds empty compile dir

* [web] fix lint

* [clsi-cache] multi-zonal lookup of single build output

* [clsi-cache] add more validation and limits

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [clsi] do not include clsi-cache tar-ball in output.zip

* [clsi-cache] fix reference after remaining constant

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

* [web] consolidate validation of filename into ClsiCacheHandler

* [clsi-cache] extend metrics and event tracking

- break down most of the clsi metrics by label
  - compile=initial - new compile dir without previous output files
  - compile=recompile - recompile in existing compile dir
  - compile=from-cache - compile using previous clsi-cache
- extend segmentation on compile-result-backend event
  - isInitialCompile=true - found new compile dir at start of request
  - restoredClsiCache=true - restored compile dir from clsi-cache

* [clsi] rename metrics labels for download of clsi-cache

This is in preparation for synctex changes.

* [clsi] use constant for limit of entries in output.tar.gz

Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>

* [clsi-cache] fix cloning of project cache

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>
GitOrigin-RevId: 4901a65497af13be1549af7f38ceee3188fcf881
2025-04-10 08:05:17 +00:00

258 lines
7.2 KiB
JavaScript

const RequestParser = require('./RequestParser')
const CompileManager = require('./CompileManager')
const Settings = require('@overleaf/settings')
const Metrics = require('./Metrics')
const ProjectPersistenceManager = require('./ProjectPersistenceManager')
const logger = require('@overleaf/logger')
const Errors = require('./Errors')
const { notifyCLSICacheAboutBuild } = require('./CLSICacheHandler')
let lastSuccessfulCompileTimestamp = 0
function timeSinceLastSuccessfulCompile() {
return Date.now() - lastSuccessfulCompileTimestamp
}
function compile(req, res, next) {
const timer = new Metrics.Timer('compile-request')
RequestParser.parse(req.body, function (error, request) {
if (error) {
return next(error)
}
timer.opts = request.metricsOpts
request.project_id = req.params.project_id
if (req.params.user_id != null) {
request.user_id = req.params.user_id
}
ProjectPersistenceManager.markProjectAsJustAccessed(
request.project_id,
function (error) {
if (error) {
return next(error)
}
CompileManager.doCompileWithLock(request, (error, result) => {
let { buildId, outputFiles, stats, timings } = result || {}
let code, status
if (outputFiles == null) {
outputFiles = []
}
if (error instanceof Errors.AlreadyCompilingError) {
code = 423 // Http 423 Locked
status = 'compile-in-progress'
} else if (error instanceof Errors.FilesOutOfSyncError) {
code = 409 // Http 409 Conflict
status = 'retry'
logger.warn(
{
projectId: request.project_id,
userId: request.user_id,
},
'files out of sync, please retry'
)
} else if (
error?.code === 'EPIPE' ||
error instanceof Errors.TooManyCompileRequestsError
) {
// docker returns EPIPE when shutting down
code = 503 // send 503 Unavailable response
status = 'unavailable'
} else if (error?.terminated) {
status = 'terminated'
} else if (error?.validate) {
status = `validation-${error.validate}`
} else if (error?.timedout) {
status = 'timedout'
logger.debug(
{ err: error, projectId: request.project_id },
'timeout running compile'
)
} else if (error) {
status = 'error'
code = 500
logger.error(
{ err: error, projectId: request.project_id },
'error running compile'
)
} else {
if (
outputFiles.some(
file => file.path === 'output.pdf' && file.size > 0
)
) {
status = 'success'
lastSuccessfulCompileTimestamp = Date.now()
} else if (request.stopOnFirstError) {
status = 'stopped-on-first-error'
} else {
status = 'failure'
logger.warn(
{ projectId: request.project_id, outputFiles },
'project failed to compile successfully, no output.pdf generated'
)
}
// log an error if any core files are found
if (outputFiles.some(file => file.path === 'core')) {
logger.error(
{ projectId: request.project_id, req, outputFiles },
'core file found in output'
)
}
}
if (error) {
outputFiles = error.outputFiles || []
buildId = error.buildId
}
if (
status === 'success' &&
request.editorId &&
request.populateClsiCache
) {
notifyCLSICacheAboutBuild({
projectId: request.project_id,
userId: request.user_id,
buildId: outputFiles[0].build,
editorId: request.editorId,
outputFiles,
compileGroup: request.compileGroup,
})
}
timer.done()
res.status(code || 200).send({
compile: {
status,
error: error?.message || error,
stats,
timings,
buildId,
outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
outputFiles: outputFiles.map(file => ({
url:
`${Settings.apis.clsi.url}/project/${request.project_id}` +
(request.user_id != null ? `/user/${request.user_id}` : '') +
`/build/${file.build}/output/${file.path}`,
...file,
})),
},
})
})
}
)
})
}
function stopCompile(req, res, next) {
const { project_id: projectId, user_id: userId } = req.params
CompileManager.stopCompile(projectId, userId, function (error) {
if (error) {
return next(error)
}
res.sendStatus(204)
})
}
function clearCache(req, res, next) {
ProjectPersistenceManager.clearProject(
req.params.project_id,
req.params.user_id,
function (error) {
if (error) {
return next(error)
}
// No content
res.sendStatus(204)
}
)
}
function syncFromCode(req, res, next) {
const { file, editorId, buildId, compileFromClsiCache } = req.query
const line = parseInt(req.query.line, 10)
const column = parseInt(req.query.column, 10)
const { imageName } = req.query
const projectId = req.params.project_id
const userId = req.params.user_id
CompileManager.syncFromCode(
projectId,
userId,
file,
line,
column,
{ imageName, editorId, buildId, compileFromClsiCache },
function (error, pdfPositions) {
if (error) {
return next(error)
}
res.json({
pdf: pdfPositions,
})
}
)
}
function syncFromPdf(req, res, next) {
const page = parseInt(req.query.page, 10)
const h = parseFloat(req.query.h)
const v = parseFloat(req.query.v)
const { imageName, editorId, buildId, compileFromClsiCache } = req.query
const projectId = req.params.project_id
const userId = req.params.user_id
CompileManager.syncFromPdf(
projectId,
userId,
page,
h,
v,
{ imageName, editorId, buildId, compileFromClsiCache },
function (error, codePositions) {
if (error) {
return next(error)
}
res.json({
code: codePositions,
})
}
)
}
function wordcount(req, res, next) {
const file = req.query.file || 'main.tex'
const projectId = req.params.project_id
const userId = req.params.user_id
const { image } = req.query
logger.debug({ image, file, projectId }, 'word count request')
CompileManager.wordcount(
projectId,
userId,
file,
image,
function (error, result) {
if (error) {
return next(error)
}
res.json({
texcount: result,
})
}
)
}
function status(req, res, next) {
res.send('OK')
}
module.exports = {
compile,
stopCompile,
clearCache,
syncFromCode,
syncFromPdf,
wordcount,
status,
timeSinceLastSuccessfulCompile,
}