[web] reduce mongo lookups for compile requests (#34890)

* [web] _getUserCompileLimits: use owner.analyticsId directly

* [web] deduplicate project lookups for compile requests

* [web] add test case for prefetched project

GitOrigin-RevId: 0e140619c874accf8673153ed90af2ce70cf9234
This commit is contained in:
Jakob Ackermann
2026-06-24 08:06:18 +00:00
committed by Copybot
parent 795ba4cd41
commit a8b4f2dc55
4 changed files with 210 additions and 53 deletions
@@ -150,25 +150,22 @@ function collectMetricsOnBlgFiles(outputFiles) {
Metrics.count('blg_output_file', nested, 1, { path: 'nested' })
}
async function sendRequest(projectId, userId, options) {
if (options == null) {
options = {}
}
let result = await sendRequestOnce(projectId, userId, options)
async function sendRequest(project, projectId, userId, options) {
let result = await sendRequestOnce(project, projectId, userId, options)
if (result.status === 'missing-updates') {
// try again with updated baseline
result = await sendRequestOnce(projectId, userId, {
result = await sendRequestOnce(project, projectId, userId, {
...options,
baseHistoryVersion: result.baseHistoryVersion,
})
} else if (result.status === 'conflict') {
// Try again, with a full compile
result = await sendRequestOnce(projectId, userId, {
result = await sendRequestOnce(null, projectId, userId, {
...options,
syncType: 'full',
})
} else if (result.status === 'unavailable') {
result = await sendRequestOnce(projectId, userId, {
result = await sendRequestOnce(null, projectId, userId, {
...options,
syncType: 'full',
forceNewClsiServer: true,
@@ -177,10 +174,10 @@ async function sendRequest(projectId, userId, options) {
return result
}
async function sendRequestOnce(projectId, userId, options) {
async function sendRequestOnce(project, projectId, userId, options) {
let req
try {
req = await _buildRequest(projectId, userId, options)
req = await _buildRequest(project, projectId, userId, options)
} catch (err) {
if (err.message === 'no main file specified') {
return {
@@ -735,13 +732,15 @@ function _parseOutputFiles(projectId, rawOutputFiles = []) {
return outputFiles
}
async function _buildRequest(projectId, userId, options) {
const project = await ProjectGetter.promises.getProject(projectId, {
compiler: 1,
imageName: 1,
'overleaf.history.id': 1,
...(options.compileFromHistory ? {} : { rootDoc_id: 1, rootFolder: 1 }),
})
async function _buildRequest(project, projectId, userId, options) {
if (project === null) {
project = await ProjectGetter.promises.getProject(projectId, {
compiler: 1,
imageName: 1,
'overleaf.history.id': 1,
...(options.compileFromHistory ? {} : { rootDoc_id: 1, rootFolder: 1 }),
})
}
if (project == null) {
throw new Errors.NotFoundError(`project does not exist: ${projectId}`)
}
@@ -770,7 +769,7 @@ async function _buildRequest(projectId, userId, options) {
'failed to compose history-full request'
)
// fall back to old compile mode
return await _buildRequest(projectId, userId, {
return await _buildRequest(null, projectId, userId, {
...options,
compileFromHistory: false,
})
@@ -791,7 +790,7 @@ async function _buildRequest(projectId, userId, options) {
'failed to compose history-incremental request'
)
// fall back to old compile mode
return await _buildRequest(projectId, userId, {
return await _buildRequest(null, projectId, userId, {
...options,
compileFromHistory: false,
})
@@ -1175,7 +1174,7 @@ function _finaliseRequest(projectId, options, project, docs, files) {
}
async function buildDocumentConversionRequest(projectId, userId, options) {
return await _buildRequest(projectId, userId, {
return await _buildRequest(null, projectId, userId, {
...options,
// Use the history snapshot as populated on clsi-cache.
populateClsiCache: true,
@@ -1186,7 +1185,7 @@ async function buildDocumentConversionRequest(projectId, userId, options) {
async function wordCount(projectId, userId, file, limits, clsiserverid) {
const { compileBackendClass, compileGroup } = limits
const req = await _buildRequest(projectId, userId, limits)
const req = await _buildRequest(null, projectId, userId, limits)
const filename = file || req.compile.rootResourcePath
const url = _getCompilerUrl(
compileBackendClass,
@@ -7,8 +7,8 @@ import UserGetter from '../User/UserGetter.mjs'
import ClsiManager from './ClsiManager.mjs'
import Metrics from '@overleaf/metrics'
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
import UserAnalyticsDataCache from '../Analytics/UserAnalyticsDataCache.mjs'
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
import Errors from '../Errors/Errors.js'
let CompileManager
const rclient = RedisWrapper.client('clsi_recently_compiled')
@@ -63,8 +63,23 @@ async function compile(projectId, userId, options = {}) {
}
}
const limits =
await CompileManager.promises.getProjectCompileLimits(projectId)
// Generate the buildId ahead of fetching the project content from redis/mongo so that the buildId's timestamp is before any lastUpdated date.
options.buildId = generateBuildId()
const project = await ProjectGetter.promises.getProject(projectId, {
// _getProjectCompileLimits
owner_ref: 1,
fromV1TemplateId: 1,
// _build_request
compiler: 1,
imageName: 1,
'overleaf.history.id': 1,
...(options.compileFromHistory ? {} : { rootDoc_id: 1, rootFolder: 1 }),
})
if (project == null) {
throw new Errors.NotFoundError(`project does not exist: ${projectId}`)
}
const limits = await _getProjectCompileLimits(project)
for (const key in limits) {
const value = limits[key]
options[key] = value
@@ -82,9 +97,6 @@ async function compile(projectId, userId, options = {}) {
return { message: 'autocompile-backoff', outputFiles: [] }
}
// Generate the buildId ahead of fetching the project content from redis/mongo so that the buildId's timestamp is before any lastUpdated date.
options.buildId = generateBuildId()
// only pass userId down to clsi if this is a per-user compile
const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId
const {
@@ -99,7 +111,12 @@ async function compile(projectId, userId, options = {}) {
clsiCacheShard,
baseHistoryVersion,
instanceType,
} = await ClsiManager.promises.sendRequest(projectId, compileAsUser, options)
} = await ClsiManager.promises.sendRequest(
project,
projectId,
compileAsUser,
options
)
return {
status,
@@ -153,11 +170,6 @@ async function _getUserCompileLimits(userId) {
ownerFeatures.compileGroup = 'alpha'
}
const analyticsId = await UserAnalyticsDataCache.getAnalyticsId(
owner._id,
'_getUserCompileLimits'
)
const compileGroup =
ownerFeatures.compileGroup || Settings.defaultFeatures.compileGroup
const limits = {
@@ -168,7 +180,7 @@ async function _getUserCompileLimits(userId) {
compileGroup === 'standard'
? Settings.apis.clsi.standardCompileBackendClass
: Settings.apis.clsi.priorityCompileBackendClass,
ownerAnalyticsId: analyticsId,
ownerAnalyticsId: owner.analyticsId,
}
return limits