| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- let CompileManager
- const Settings = require('@overleaf/settings')
- const RedisWrapper = require('../../infrastructure/RedisWrapper')
- const rclient = RedisWrapper.client('clsi_recently_compiled')
- const ProjectGetter = require('../Project/ProjectGetter')
- const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
- const UserGetter = require('../User/UserGetter')
- const ClsiManager = require('./ClsiManager')
- const Metrics = require('@overleaf/metrics')
- const { RateLimiter } = require('../../infrastructure/RateLimiter')
- const SplitTestHandler = require('../SplitTests/SplitTestHandler')
- const { getAnalyticsIdFromMongoUser } = require('../Analytics/AnalyticsHelper')
- const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF = new Date('2023-09-18T11:00:00.000Z')
- module.exports = CompileManager = {
- NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF,
- compile(projectId, userId, options = {}, _callback) {
- const timer = new Metrics.Timer('editor.compile')
- const callback = function (...args) {
- timer.done()
- _callback(...args)
- }
- CompileManager._checkIfRecentlyCompiled(
- projectId,
- userId,
- function (error, recentlyCompiled) {
- if (error) {
- return callback(error)
- }
- if (recentlyCompiled) {
- return callback(null, 'too-recently-compiled', [])
- }
- CompileManager._checkIfAutoCompileLimitHasBeenHit(
- options.isAutoCompile,
- 'everyone',
- function (err, canCompile) {
- if (err || !canCompile) {
- return callback(null, 'autocompile-backoff', [])
- }
- ProjectRootDocManager.ensureRootDocumentIsSet(
- projectId,
- function (error) {
- if (error) {
- return callback(error)
- }
- CompileManager.getProjectCompileLimits(
- projectId,
- function (error, limits) {
- if (error) {
- return callback(error)
- }
- for (const key in limits) {
- const value = limits[key]
- options[key] = value
- }
- if (options.timeout !== 20) {
- // temporary override to force the new compile timeout
- if (options.forceNewCompileTimeout === 'active') {
- options.timeout = 20
- } else if (
- options.forceNewCompileTimeout === 'changing'
- ) {
- options.timeout = 60
- }
- }
- // Put a lower limit on autocompiles for free users, based on compileGroup
- CompileManager._checkCompileGroupAutoCompileLimit(
- options.isAutoCompile,
- limits.compileGroup,
- function (err, canCompile) {
- if (err || !canCompile) {
- return callback(null, 'autocompile-backoff', [])
- }
- // only pass userId down to clsi if this is a per-user compile
- const compileAsUser = Settings.disablePerUserCompiles
- ? undefined
- : userId
- ClsiManager.sendRequest(
- projectId,
- compileAsUser,
- options,
- function (
- error,
- status,
- outputFiles,
- clsiServerId,
- validationProblems,
- stats,
- timings,
- outputUrlPrefix
- ) {
- if (error) {
- return callback(error)
- }
- callback(
- null,
- status,
- outputFiles,
- clsiServerId,
- limits,
- validationProblems,
- stats,
- timings,
- outputUrlPrefix
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- }
- )
- },
- stopCompile(projectId, userId, callback) {
- CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
- if (error) {
- return callback(error)
- }
- ClsiManager.stopCompile(projectId, userId, limits, callback)
- })
- },
- deleteAuxFiles(projectId, userId, clsiserverid, callback) {
- CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
- if (error) {
- return callback(error)
- }
- ClsiManager.deleteAuxFiles(
- projectId,
- userId,
- limits,
- clsiserverid,
- callback
- )
- })
- },
- getProjectCompileLimits(projectId, callback) {
- ProjectGetter.getProject(
- projectId,
- { owner_ref: 1 },
- function (error, project) {
- if (error) {
- return callback(error)
- }
- UserGetter.getUser(
- project.owner_ref,
- {
- _id: 1,
- alphaProgram: 1,
- analyticsId: 1,
- betaProgram: 1,
- features: 1,
- splitTests: 1,
- signUpDate: 1, // for compile-timeout-20s
- },
- function (err, owner) {
- if (err) {
- return callback(err)
- }
- const ownerFeatures = (owner && owner.features) || {}
- // put alpha users into their own compile group
- if (owner && owner.alphaProgram) {
- ownerFeatures.compileGroup = 'alpha'
- }
- const limits = {
- timeout:
- ownerFeatures.compileTimeout ||
- Settings.defaultFeatures.compileTimeout,
- compileGroup:
- ownerFeatures.compileGroup ||
- Settings.defaultFeatures.compileGroup,
- ownerAnalyticsId: getAnalyticsIdFromMongoUser(owner),
- }
- CompileManager._getCompileBackendClassDetails(
- owner,
- limits.compileGroup,
- (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => {
- if (err) return callback(err)
- limits.compileBackendClass = compileBackendClass
- limits.showFasterCompilesFeedbackUI =
- showFasterCompilesFeedbackUI
- if (compileBackendClass === 'n2d' && limits.timeout <= 60) {
- // project owners with faster compiles but with <= 60 compile timeout (default)
- // will have a 20s compile timeout
- // The compile-timeout-20s split test exists to enable a gradual rollout
- SplitTestHandler.getAssignmentForMongoUser(
- owner,
- 'compile-timeout-20s',
- (err, assignment) => {
- if (err) return callback(err)
- if (assignment?.variant === '20s') {
- if (
- owner.signUpDate > NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF
- ) {
- limits.timeout = 20
- }
- }
- callback(null, limits)
- }
- )
- } else {
- callback(null, limits)
- }
- }
- )
- }
- )
- }
- )
- },
- COMPILE_DELAY: 1, // seconds
- _checkIfRecentlyCompiled(projectId, userId, callback) {
- const key = `compile:${projectId}:${userId}`
- rclient.set(
- key,
- true,
- 'EX',
- this.COMPILE_DELAY,
- 'NX',
- function (error, ok) {
- if (error) {
- return callback(error)
- }
- if (ok === 'OK') {
- callback(null, false)
- } else {
- callback(null, true)
- }
- }
- )
- },
- _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup, callback) {
- if (!isAutoCompile) {
- return callback(null, true)
- }
- if (compileGroup === 'standard') {
- // apply extra limits to the standard compile group
- CompileManager._checkIfAutoCompileLimitHasBeenHit(
- isAutoCompile,
- compileGroup,
- callback
- )
- } else {
- Metrics.inc(`auto-compile-${compileGroup}`)
- callback(null, true)
- }
- }, // always allow priority group users to compile
- _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup, callback) {
- if (!isAutoCompile) {
- return callback(null, true)
- }
- Metrics.inc(`auto-compile-${compileGroup}`)
- const rateLimiter = getAutoCompileRateLimiter(compileGroup)
- rateLimiter
- .consume('global')
- .then(() => {
- callback(null, true)
- })
- .catch(() => {
- // Don't differentiate between errors and rate limits. Silently trigger
- // the rate limit if there's an error consuming the points.
- Metrics.inc(`auto-compile-${compileGroup}-limited`)
- callback(null, false)
- })
- },
- _getCompileBackendClassDetails(owner, compileGroup, callback) {
- const { defaultBackendClass } = Settings.apis.clsi
- if (compileGroup === 'standard') {
- return SplitTestHandler.getAssignmentForMongoUser(
- owner,
- 'compile-backend-class-n2d',
- (err, assignment) => {
- if (err) return callback(err, {})
- const { variant } = assignment
- callback(null, {
- compileBackendClass:
- variant === 'default' ? defaultBackendClass : variant,
- showFasterCompilesFeedbackUI: false,
- })
- }
- )
- }
- SplitTestHandler.getAssignmentForMongoUser(
- owner,
- 'compile-backend-class',
- (err, assignment) => {
- if (err) return callback(err, {})
- const { analytics, variant } = assignment
- const activeForUser = analytics?.segmentation?.splitTest != null
- callback(null, {
- compileBackendClass:
- variant === 'default' ? defaultBackendClass : variant,
- showFasterCompilesFeedbackUI: activeForUser,
- })
- }
- )
- },
- wordCount(projectId, userId, file, clsiserverid, callback) {
- CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
- if (error) {
- return callback(error)
- }
- ClsiManager.wordCount(
- projectId,
- userId,
- file,
- limits,
- clsiserverid,
- callback
- )
- })
- },
- }
- const autoCompileRateLimiters = new Map()
- function getAutoCompileRateLimiter(compileGroup) {
- let rateLimiter = autoCompileRateLimiters.get(compileGroup)
- if (rateLimiter == null) {
- rateLimiter = new RateLimiter(`auto-compile:${compileGroup}`, {
- points: Settings.rateLimit.autoCompile[compileGroup] || 25,
- duration: 20,
- })
- autoCompileRateLimiters.set(compileGroup, rateLimiter)
- }
- return rateLimiter
- }
|