[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
This commit is contained in:
committed by
Copybot
co-authored by
Eric Mc Sween
Brian Gough
Alf Eaton
parent
7920cd9d3d
commit
b538d56591
@@ -0,0 +1,127 @@
|
||||
const {
|
||||
fetchNothing,
|
||||
fetchRedirectWithResponse,
|
||||
RequestFailedError,
|
||||
} = require('@overleaf/fetch-utils')
|
||||
const logger = require('@overleaf/logger')
|
||||
const Settings = require('@overleaf/settings')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const { NotFoundError, InvalidNameError } = require('../Errors/Errors')
|
||||
|
||||
function validateFilename(filename) {
|
||||
if (
|
||||
![
|
||||
'output.blg',
|
||||
'output.log',
|
||||
'output.pdf',
|
||||
'output.overleaf.json',
|
||||
'output.tar.gz',
|
||||
].includes(filename) ||
|
||||
filename.endsWith('.blg')
|
||||
) {
|
||||
throw new InvalidNameError('bad filename')
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCache(projectId, userId) {
|
||||
let path = `/project/${projectId}`
|
||||
if (userId) {
|
||||
path += `/user/${userId}`
|
||||
}
|
||||
path += '/output'
|
||||
|
||||
await Promise.all(
|
||||
Settings.apis.clsiCache.instances.map(async ({ url, zone }) => {
|
||||
const u = new URL(url)
|
||||
u.pathname = path
|
||||
try {
|
||||
await fetchNothing(u, {
|
||||
method: 'DELETE',
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
})
|
||||
} catch (err) {
|
||||
throw OError.tag(err, 'clear clsi-cache', { url, zone })
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function getLatestOutputFile(
|
||||
projectId,
|
||||
userId,
|
||||
filename,
|
||||
signal = AbortSignal.timeout(15_000)
|
||||
) {
|
||||
validateFilename(filename)
|
||||
|
||||
let path = `/project/${projectId}`
|
||||
if (userId) {
|
||||
path += `/user/${userId}`
|
||||
}
|
||||
path += `/latest/output/${filename}`
|
||||
|
||||
for (const { url, zone } of Settings.apis.clsiCache.instances) {
|
||||
const u = new URL(url)
|
||||
u.pathname = path
|
||||
try {
|
||||
const {
|
||||
location,
|
||||
response: { headers },
|
||||
} = await fetchRedirectWithResponse(u, {
|
||||
signal,
|
||||
})
|
||||
// Success, return the cache entry.
|
||||
return {
|
||||
location,
|
||||
zone: headers.get('X-Zone'),
|
||||
lastModified: new Date(headers.get('X-Last-Modified')),
|
||||
size: parseInt(headers.get('X-Content-Length'), 10),
|
||||
allFiles: JSON.parse(headers.get('X-All-Files')),
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof RequestFailedError && err.response.status === 404) {
|
||||
break // No clsi-cache instance has cached something for this project/user.
|
||||
}
|
||||
logger.warn(
|
||||
{ err, projectId, userId, url, zone },
|
||||
'getLatestOutputFile from clsi-cache failed'
|
||||
)
|
||||
// This clsi-cache instance is down, try the next backend.
|
||||
}
|
||||
}
|
||||
throw new NotFoundError('nothing cached yet')
|
||||
}
|
||||
|
||||
async function prepareCacheSource(
|
||||
projectId,
|
||||
userId,
|
||||
{ sourceProjectId, templateId, templateVersionId, lastUpdated, zone, signal }
|
||||
) {
|
||||
const url = new URL(
|
||||
`/project/${projectId}/user/${userId}/import-from`,
|
||||
Settings.apis.clsiCache.instances.find(i => i.zone === zone).url
|
||||
)
|
||||
try {
|
||||
await fetchNothing(url, {
|
||||
method: 'POST',
|
||||
json: {
|
||||
sourceProjectId,
|
||||
lastUpdated,
|
||||
templateId,
|
||||
templateVersionId,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof RequestFailedError && err.response.status === 404) {
|
||||
throw new NotFoundError()
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clearCache,
|
||||
getLatestOutputFile,
|
||||
prepareCacheSource,
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
const { NotFoundError } = require('../Errors/Errors')
|
||||
const ClsiCacheHandler = require('./ClsiCacheHandler')
|
||||
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
|
||||
async function getLatestBuildFromCache(projectId, userId, filename, signal) {
|
||||
const [
|
||||
{ location, lastModified: lastCompiled, zone, size, allFiles },
|
||||
lastUpdatedInRedis,
|
||||
{ lastUpdated: lastUpdatedInMongo, name: projectName },
|
||||
] = await Promise.all([
|
||||
ClsiCacheHandler.getLatestOutputFile(projectId, userId, filename, signal),
|
||||
DocumentUpdaterHandler.promises.getProjectLastUpdatedAt(projectId),
|
||||
ProjectGetter.promises.getProject(projectId, { lastUpdated: 1, name: 1 }),
|
||||
])
|
||||
|
||||
const lastUpdated =
|
||||
lastUpdatedInRedis > lastUpdatedInMongo
|
||||
? lastUpdatedInRedis
|
||||
: lastUpdatedInMongo
|
||||
const isUpToDate = lastCompiled >= lastUpdated
|
||||
|
||||
return {
|
||||
internal: {
|
||||
location,
|
||||
zone,
|
||||
projectName,
|
||||
},
|
||||
external: {
|
||||
isUpToDate,
|
||||
lastUpdated,
|
||||
size,
|
||||
allFiles,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareClsiCache(
|
||||
projectId,
|
||||
userId,
|
||||
{ sourceProjectId, templateId, templateVersionId }
|
||||
) {
|
||||
const { variant } = await SplitTestHandler.promises.getAssignmentForUser(
|
||||
userId,
|
||||
'copy-clsi-cache'
|
||||
)
|
||||
if (variant !== 'enabled') return
|
||||
const signal = AbortSignal.timeout(5_000)
|
||||
let lastUpdated
|
||||
let zone = 'b' // populate template data on zone b
|
||||
if (sourceProjectId) {
|
||||
try {
|
||||
;({
|
||||
internal: { zone },
|
||||
external: { lastUpdated },
|
||||
} = await getLatestBuildFromCache(
|
||||
sourceProjectId,
|
||||
userId,
|
||||
'output.tar.gz',
|
||||
signal
|
||||
))
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) return // nothing cached yet
|
||||
throw err
|
||||
}
|
||||
}
|
||||
try {
|
||||
await ClsiCacheHandler.prepareCacheSource(projectId, userId, {
|
||||
sourceProjectId,
|
||||
templateId,
|
||||
templateVersionId,
|
||||
zone,
|
||||
lastUpdated,
|
||||
signal,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof NotFoundError) return // nothing cached yet/expired.
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLatestBuildFromCache,
|
||||
prepareClsiCache,
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const ClsiFormatChecker = require('./ClsiFormatChecker')
|
||||
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
const Metrics = require('@overleaf/metrics')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const ClsiCacheHandler = require('./ClsiCacheHandler')
|
||||
const { getBlobLocation } = require('../History/HistoryManager')
|
||||
|
||||
const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
|
||||
@@ -148,6 +149,13 @@ async function deleteAuxFiles(projectId, userId, options, clsiserverid) {
|
||||
clsiserverid
|
||||
)
|
||||
} finally {
|
||||
// always clear the clsi-cache
|
||||
try {
|
||||
await ClsiCacheHandler.clearCache(projectId, userId)
|
||||
} catch (err) {
|
||||
logger.warn({ err, projectId, userId }, 'purge clsi-cache failed')
|
||||
}
|
||||
|
||||
// always clear the project state from the docupdater, even if there
|
||||
// was a problem with the request to the clsi
|
||||
try {
|
||||
@@ -766,6 +774,7 @@ function _finaliseRequest(projectId, options, project, docs, files) {
|
||||
compile: {
|
||||
options: {
|
||||
buildId: options.buildId,
|
||||
editorId: options.editorId,
|
||||
compiler: project.compiler,
|
||||
timeout: options.timeout,
|
||||
imageName: project.imageName,
|
||||
@@ -775,6 +784,8 @@ function _finaliseRequest(projectId, options, project, docs, files) {
|
||||
syncType: options.syncType,
|
||||
syncState: options.syncState,
|
||||
compileGroup: options.compileGroup,
|
||||
compileFromClsiCache: options.compileFromClsiCache,
|
||||
populateClsiCache: options.populateClsiCache,
|
||||
enablePdfCaching:
|
||||
(Settings.enablePdfCaching && options.enablePdfCaching) || false,
|
||||
pdfCachingMinChunkSize: options.pdfCachingMinChunkSize,
|
||||
|
||||
@@ -66,11 +66,31 @@ const getSplitTestOptions = callbackify(async function (req, res) {
|
||||
} catch (e) {}
|
||||
const editorReq = { ...req, query }
|
||||
|
||||
// Lookup the clsi-cache flag in the backend.
|
||||
// We may need to turn off the feature on a short notice, without requiring
|
||||
// all users to reload their editor page to disable the feature.
|
||||
const { variant: compileFromClsiCacheVariant } =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
editorReq,
|
||||
res,
|
||||
'compile-from-clsi-cache'
|
||||
)
|
||||
const compileFromClsiCache = compileFromClsiCacheVariant === 'enabled'
|
||||
const { variant: populateClsiCacheVariant } =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
editorReq,
|
||||
res,
|
||||
'populate-clsi-cache'
|
||||
)
|
||||
const populateClsiCache = populateClsiCacheVariant === 'enabled'
|
||||
|
||||
const pdfDownloadDomain = Settings.pdfDownloadDomain
|
||||
|
||||
if (!req.query.enable_pdf_caching) {
|
||||
// The frontend does not want to do pdf caching.
|
||||
return {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
pdfDownloadDomain,
|
||||
enablePdfCaching: false,
|
||||
}
|
||||
@@ -88,12 +108,16 @@ const getSplitTestOptions = callbackify(async function (req, res) {
|
||||
if (!enablePdfCaching) {
|
||||
// Skip the lookup of the chunk size when caching is not enabled.
|
||||
return {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
pdfDownloadDomain,
|
||||
enablePdfCaching: false,
|
||||
}
|
||||
}
|
||||
const pdfCachingMinChunkSize = await getPdfCachingMinChunkSize(editorReq, res)
|
||||
return {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
pdfDownloadDomain,
|
||||
enablePdfCaching,
|
||||
pdfCachingMinChunkSize,
|
||||
@@ -112,6 +136,7 @@ module.exports = CompileController = {
|
||||
isAutoCompile,
|
||||
fileLineErrors,
|
||||
stopOnFirstError,
|
||||
editorId: req.body.editorId,
|
||||
}
|
||||
|
||||
if (req.body.rootDoc_id) {
|
||||
@@ -138,8 +163,15 @@ module.exports = CompileController = {
|
||||
|
||||
getSplitTestOptions(req, res, (err, splitTestOptions) => {
|
||||
if (err) return next(err)
|
||||
let { enablePdfCaching, pdfCachingMinChunkSize, pdfDownloadDomain } =
|
||||
splitTestOptions
|
||||
let {
|
||||
compileFromClsiCache,
|
||||
populateClsiCache,
|
||||
enablePdfCaching,
|
||||
pdfCachingMinChunkSize,
|
||||
pdfDownloadDomain,
|
||||
} = splitTestOptions
|
||||
options.compileFromClsiCache = compileFromClsiCache
|
||||
options.populateClsiCache = populateClsiCache
|
||||
options.enablePdfCaching = enablePdfCaching
|
||||
if (enablePdfCaching) {
|
||||
options.pdfCachingMinChunkSize = pdfCachingMinChunkSize
|
||||
@@ -193,6 +225,8 @@ module.exports = CompileController = {
|
||||
timeout: limits.timeout === 60 ? 'short' : 'long',
|
||||
server: clsiServerId?.includes('-c2d-') ? 'faster' : 'normal',
|
||||
isAutoCompile,
|
||||
isInitialCompile: stats.isInitialCompile === 1,
|
||||
restoredClsiCache: stats.restoredClsiCache === 1,
|
||||
stopOnFirstError,
|
||||
}
|
||||
)
|
||||
@@ -497,7 +531,7 @@ module.exports = CompileController = {
|
||||
|
||||
proxySyncPdf(req, res, next) {
|
||||
const projectId = req.params.Project_id
|
||||
const { page, h, v } = req.query
|
||||
const { page, h, v, editorId, buildId } = req.query
|
||||
if (!page?.match(/^\d+$/)) {
|
||||
return next(new Error('invalid page parameter'))
|
||||
}
|
||||
@@ -515,23 +549,29 @@ module.exports = CompileController = {
|
||||
getImageNameForProject(projectId, (error, imageName) => {
|
||||
if (error) return next(error)
|
||||
|
||||
const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
|
||||
CompileController.proxyToClsi(
|
||||
projectId,
|
||||
'sync-to-pdf',
|
||||
url,
|
||||
{ page, h, v, imageName },
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
getSplitTestOptions(req, res, (error, splitTestOptions) => {
|
||||
if (error) return next(error)
|
||||
const { compileFromClsiCache } = splitTestOptions
|
||||
|
||||
const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
|
||||
|
||||
CompileController.proxyToClsi(
|
||||
projectId,
|
||||
'sync-to-pdf',
|
||||
url,
|
||||
{ page, h, v, imageName, editorId, buildId, compileFromClsiCache },
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
proxySyncCode(req, res, next) {
|
||||
const projectId = req.params.Project_id
|
||||
const { file, line, column } = req.query
|
||||
const { file, line, column, editorId, buildId } = req.query
|
||||
if (file == null) {
|
||||
return next(new Error('missing file parameter'))
|
||||
}
|
||||
@@ -557,16 +597,29 @@ module.exports = CompileController = {
|
||||
getImageNameForProject(projectId, (error, imageName) => {
|
||||
if (error) return next(error)
|
||||
|
||||
const url = CompileController._getUrl(projectId, userId, 'sync/code')
|
||||
CompileController.proxyToClsi(
|
||||
projectId,
|
||||
'sync-to-code',
|
||||
url,
|
||||
{ file, line, column, imageName },
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
getSplitTestOptions(req, res, (error, splitTestOptions) => {
|
||||
if (error) return next(error)
|
||||
const { compileFromClsiCache } = splitTestOptions
|
||||
|
||||
const url = CompileController._getUrl(projectId, userId, 'sync/code')
|
||||
CompileController.proxyToClsi(
|
||||
projectId,
|
||||
'sync-to-code',
|
||||
url,
|
||||
{
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
imageName,
|
||||
editorId,
|
||||
buildId,
|
||||
compileFromClsiCache,
|
||||
},
|
||||
req,
|
||||
res,
|
||||
next
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
|
||||
const _ = require('lodash')
|
||||
const TagsHandler = require('../Tags/TagsHandler')
|
||||
const Features = require('../../infrastructure/Features')
|
||||
const ClsiCacheManager = require('../Compile/ClsiCacheManager')
|
||||
|
||||
module.exports = {
|
||||
duplicate: callbackify(duplicate),
|
||||
@@ -35,6 +36,7 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
originalProjectId,
|
||||
{
|
||||
compiler: true,
|
||||
imageName: true,
|
||||
rootFolder: true,
|
||||
rootDoc_id: true,
|
||||
fromV1TemplateId: true,
|
||||
@@ -73,6 +75,21 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
{ segmentation }
|
||||
)
|
||||
|
||||
let prepareClsiCacheInBackground = Promise.resolve()
|
||||
if (originalProject.imageName === newProject.imageName) {
|
||||
// Populate the clsi-cache unless the TeXLive release has changed.
|
||||
prepareClsiCacheInBackground = ClsiCacheManager.prepareClsiCache(
|
||||
newProject._id,
|
||||
owner._id,
|
||||
{ sourceProjectId: originalProjectId }
|
||||
).catch(err => {
|
||||
logger.warn(
|
||||
{ err, originalProjectId, projectId: newProject._id },
|
||||
'failed to prepare clsi-cache for cloned project'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await ProjectOptionsHandler.promises.setCompiler(
|
||||
newProject._id,
|
||||
@@ -120,6 +137,10 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await prepareClsiCacheInBackground
|
||||
} catch {}
|
||||
|
||||
return newProject
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const settings = require('@overleaf/settings')
|
||||
const crypto = require('crypto')
|
||||
const Errors = require('../Errors/Errors')
|
||||
const { pipeline } = require('stream/promises')
|
||||
const ClsiCacheManager = require('../Compile/ClsiCacheManager')
|
||||
|
||||
const TemplatesManager = {
|
||||
async createProjectFromV1Template(
|
||||
@@ -63,6 +64,17 @@ const TemplatesManager = {
|
||||
attributes
|
||||
)
|
||||
|
||||
const prepareClsiCacheInBackground = ClsiCacheManager.prepareClsiCache(
|
||||
project._id,
|
||||
userId,
|
||||
{ templateId, templateVersionId }
|
||||
).catch(err => {
|
||||
logger.warn(
|
||||
{ err, templateId, templateVersionId, projectId: project._id },
|
||||
'failed to prepare clsi-cache from template'
|
||||
)
|
||||
})
|
||||
|
||||
await TemplatesManager._setCompiler(project._id, compiler)
|
||||
await TemplatesManager._setImage(project._id, imageName)
|
||||
await TemplatesManager._setMainFile(project._id, mainFile)
|
||||
@@ -74,6 +86,8 @@ const TemplatesManager = {
|
||||
}
|
||||
await Project.updateOne({ _id: project._id }, update, {})
|
||||
|
||||
await prepareClsiCacheInBackground
|
||||
|
||||
return project
|
||||
} finally {
|
||||
await fs.promises.unlink(dumpPath)
|
||||
|
||||
@@ -242,6 +242,9 @@ module.exports = {
|
||||
submissionBackendClass:
|
||||
process.env.CLSI_SUBMISSION_BACKEND_CLASS || 'n2d',
|
||||
},
|
||||
clsiCache: {
|
||||
instances: JSON.parse(process.env.CLSI_CACHE_INSTANCES || '[]'),
|
||||
},
|
||||
project_history: {
|
||||
sendProjectStructureOps: true,
|
||||
url: `http://${process.env.PROJECT_HISTORY_HOST || '127.0.0.1'}:3054`,
|
||||
|
||||
@@ -144,6 +144,7 @@ function PdfSynctexControls() {
|
||||
|
||||
const {
|
||||
clsiServerId,
|
||||
pdfFile,
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
position,
|
||||
@@ -239,6 +240,8 @@ function PdfSynctexControls() {
|
||||
if (clsiServerId) {
|
||||
params += `&clsiserverid=${clsiServerId}`
|
||||
}
|
||||
if (pdfFile?.editorId) params += `&editorId=${pdfFile.editorId}`
|
||||
if (pdfFile?.build) params += `&buildId=${pdfFile.build}`
|
||||
|
||||
getJSON(`/project/${projectId}/sync/code?${params}`, { signal })
|
||||
.then(data => {
|
||||
@@ -253,6 +256,7 @@ function PdfSynctexControls() {
|
||||
})
|
||||
},
|
||||
[
|
||||
pdfFile,
|
||||
clsiServerId,
|
||||
isMounted,
|
||||
projectId,
|
||||
@@ -344,6 +348,8 @@ function PdfSynctexControls() {
|
||||
if (clsiServerId) {
|
||||
params.set('clsiserverid', clsiServerId)
|
||||
}
|
||||
if (pdfFile?.editorId) params.set('editorId', pdfFile.editorId)
|
||||
if (pdfFile?.build) params.set('buildId', pdfFile.build)
|
||||
|
||||
getJSON(`/project/${projectId}/sync/pdf?${params}`, { signal })
|
||||
.then(data => {
|
||||
@@ -358,6 +364,7 @@ function PdfSynctexControls() {
|
||||
})
|
||||
},
|
||||
[
|
||||
pdfFile,
|
||||
clsiServerId,
|
||||
projectId,
|
||||
signal,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isMainFile } from './editor-files'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
|
||||
import { debounce } from 'lodash'
|
||||
import { trackPdfDownload } from './metrics'
|
||||
import { EDITOR_SESSION_ID, trackPdfDownload } from './metrics'
|
||||
import { enablePdfCaching } from './pdf-caching-flags'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { signalWithTimeout } from '@/utils/abort-signal'
|
||||
@@ -109,6 +109,7 @@ export default class DocumentCompiler {
|
||||
// if there was previously a server error
|
||||
incrementalCompilesEnabled: !this.error,
|
||||
stopOnFirstError: options.stopOnFirstError,
|
||||
editorId: EDITOR_SESSION_ID,
|
||||
}
|
||||
|
||||
const data = await postJSON(
|
||||
|
||||
@@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging'
|
||||
const VERSION = 9
|
||||
|
||||
// editing session id
|
||||
const EDITOR_SESSION_ID = uuid()
|
||||
export const EDITOR_SESSION_ID = uuid()
|
||||
|
||||
const pdfCachingMetrics = {
|
||||
viewerId: EDITOR_SESSION_ID,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { enablePdfCaching } from './pdf-caching-flags'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { dirname, findEntityByPath } from '@/features/file-tree/util/path'
|
||||
import '@/utils/readable-stream-async-iterator-polyfill'
|
||||
import { EDITOR_SESSION_ID } from '@/features/pdf-preview/util/metrics'
|
||||
|
||||
// Warnings that may disappear after a second LaTeX pass
|
||||
const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/
|
||||
@@ -15,6 +16,8 @@ export function handleOutputFiles(outputFiles, projectId, data) {
|
||||
const outputFile = outputFiles.get('output.pdf')
|
||||
if (!outputFile) return null
|
||||
|
||||
outputFile.editorId = outputFile.editorId || EDITOR_SESSION_ID
|
||||
|
||||
// build the URL for viewing the PDF in the preview UI
|
||||
const params = new URLSearchParams({
|
||||
compileGroup: data.compileGroup,
|
||||
|
||||
@@ -144,6 +144,9 @@ describe('ClsiManager', function () {
|
||||
enablePdfCaching: true,
|
||||
clsiCookie: { key: 'clsiserver' },
|
||||
}
|
||||
this.ClsiCacheHandler = {
|
||||
clearCache: sinon.stub().resolves(),
|
||||
}
|
||||
this.Features = {
|
||||
hasFeature: sinon.stub().withArgs('project-history-blobs').returns(true),
|
||||
}
|
||||
@@ -172,6 +175,7 @@ describe('ClsiManager', function () {
|
||||
this.DocumentUpdaterHandler,
|
||||
'./ClsiCookieManager': () => this.ClsiCookieManager,
|
||||
'./ClsiStateManager': this.ClsiStateManager,
|
||||
'./ClsiCacheHandler': this.ClsiCacheHandler,
|
||||
'@overleaf/fetch-utils': this.FetchUtils,
|
||||
'./ClsiFormatChecker': this.ClsiFormatChecker,
|
||||
'@overleaf/metrics': this.Metrics,
|
||||
@@ -390,6 +394,8 @@ describe('ClsiManager', function () {
|
||||
incrementalCompilesEnabled: true,
|
||||
compileBackendClass: 'e2',
|
||||
compileGroup: 'priority',
|
||||
compileFromClsiCache: true,
|
||||
populateClsiCache: true,
|
||||
enablePdfCaching: true,
|
||||
pdfCachingMinChunkSize: 1337,
|
||||
}
|
||||
@@ -448,6 +454,8 @@ describe('ClsiManager', function () {
|
||||
syncType: 'incremental',
|
||||
syncState: '01234567890abcdef',
|
||||
compileGroup: 'priority',
|
||||
compileFromClsiCache: true,
|
||||
populateClsiCache: true,
|
||||
enablePdfCaching: true,
|
||||
pdfCachingMinChunkSize: 1337,
|
||||
metricsMethod: 'priority',
|
||||
@@ -945,6 +953,12 @@ describe('ClsiManager', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear the output.tar.gz files in clsi-cache', function () {
|
||||
this.ClsiCacheHandler.clearCache
|
||||
.calledWith(this.project._id, this.user_id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should clear the project state from the docupdater', function () {
|
||||
this.DocumentUpdaterHandler.promises.clearProjectState
|
||||
.calledWith(this.project._id)
|
||||
|
||||
@@ -244,9 +244,12 @@ describe('CompileController', function () {
|
||||
this.user_id,
|
||||
{
|
||||
isAutoCompile: false,
|
||||
compileFromClsiCache: false,
|
||||
populateClsiCache: false,
|
||||
enablePdfCaching: false,
|
||||
fileLineErrors: false,
|
||||
stopOnFirstError: false,
|
||||
editorId: undefined,
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -284,9 +287,12 @@ describe('CompileController', function () {
|
||||
this.user_id,
|
||||
{
|
||||
isAutoCompile: true,
|
||||
compileFromClsiCache: false,
|
||||
populateClsiCache: false,
|
||||
enablePdfCaching: false,
|
||||
fileLineErrors: false,
|
||||
stopOnFirstError: false,
|
||||
editorId: undefined,
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -305,10 +311,37 @@ describe('CompileController', function () {
|
||||
this.user_id,
|
||||
{
|
||||
isAutoCompile: false,
|
||||
compileFromClsiCache: false,
|
||||
populateClsiCache: false,
|
||||
enablePdfCaching: false,
|
||||
draft: true,
|
||||
fileLineErrors: false,
|
||||
stopOnFirstError: false,
|
||||
editorId: undefined,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an editor id', function () {
|
||||
beforeEach(function (done) {
|
||||
this.res.callback = done
|
||||
this.req.body = { editorId: 'the-editor-id' }
|
||||
this.CompileController.compile(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should pass the editor id to the compiler', function () {
|
||||
this.CompileManager.compile.should.have.been.calledWith(
|
||||
this.projectId,
|
||||
this.user_id,
|
||||
{
|
||||
isAutoCompile: false,
|
||||
compileFromClsiCache: false,
|
||||
populateClsiCache: false,
|
||||
enablePdfCaching: false,
|
||||
fileLineErrors: false,
|
||||
stopOnFirstError: false,
|
||||
editorId: 'the-editor-id',
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -542,14 +575,16 @@ describe('CompileController', function () {
|
||||
})
|
||||
})
|
||||
describe('proxySyncCode', function () {
|
||||
let file, line, column, imageName
|
||||
let file, line, column, imageName, editorId, buildId
|
||||
|
||||
beforeEach(function (done) {
|
||||
this.req.params = { Project_id: this.projectId }
|
||||
file = 'main.tex'
|
||||
line = String(Date.now())
|
||||
column = String(Date.now() + 1)
|
||||
this.req.query = { file, line, column }
|
||||
editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
|
||||
buildId = '195b4a3f9e7-03e5be430a9e7796'
|
||||
this.req.query = { file, line, column, editorId, buildId }
|
||||
|
||||
imageName = 'foo/bar:tag-0'
|
||||
this.ProjectGetter.getProject = sinon.stub().yields(null, { imageName })
|
||||
@@ -566,7 +601,15 @@ describe('CompileController', function () {
|
||||
this.projectId,
|
||||
'sync-to-code',
|
||||
`/project/${this.projectId}/user/${this.user_id}/sync/code`,
|
||||
{ file, line, column, imageName },
|
||||
{
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
imageName,
|
||||
editorId,
|
||||
buildId,
|
||||
compileFromClsiCache: false,
|
||||
},
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
@@ -575,14 +618,16 @@ describe('CompileController', function () {
|
||||
})
|
||||
|
||||
describe('proxySyncPdf', function () {
|
||||
let page, h, v, imageName
|
||||
let page, h, v, imageName, editorId, buildId
|
||||
|
||||
beforeEach(function (done) {
|
||||
this.req.params = { Project_id: this.projectId }
|
||||
page = String(Date.now())
|
||||
h = String(Math.random())
|
||||
v = String(Math.random())
|
||||
this.req.query = { page, h, v }
|
||||
editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
|
||||
buildId = '195b4a3f9e7-03e5be430a9e7796'
|
||||
this.req.query = { page, h, v, editorId, buildId }
|
||||
|
||||
imageName = 'foo/bar:tag-1'
|
||||
this.ProjectGetter.getProject = sinon.stub().yields(null, { imageName })
|
||||
@@ -599,7 +644,15 @@ describe('CompileController', function () {
|
||||
this.projectId,
|
||||
'sync-to-pdf',
|
||||
`/project/${this.projectId}/user/${this.user_id}/sync/pdf`,
|
||||
{ page, h, v, imageName },
|
||||
{
|
||||
page,
|
||||
h,
|
||||
v,
|
||||
imageName,
|
||||
editorId,
|
||||
buildId,
|
||||
compileFromClsiCache: false,
|
||||
},
|
||||
this.req,
|
||||
this.res,
|
||||
this.next
|
||||
|
||||
@@ -245,6 +245,9 @@ describe('ProjectDuplicator', function () {
|
||||
'../Tags/TagsHandler': this.TagsHandler,
|
||||
'../History/HistoryManager': this.HistoryManager,
|
||||
'../../infrastructure/Features': this.Features,
|
||||
'../Compile/ClsiCacheManager': {
|
||||
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,6 +121,9 @@ describe('TemplatesManager', function () {
|
||||
fs: this.fs,
|
||||
'../../models/Project': { Project: this.Project },
|
||||
'stream/promises': { pipeline: this.pipeline },
|
||||
'../Compile/ClsiCacheManager': {
|
||||
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
|
||||
},
|
||||
},
|
||||
}).promises
|
||||
return (this.zipUrl =
|
||||
|
||||
Reference in New Issue
Block a user