CompileManager.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. let CompileManager
  2. const Settings = require('@overleaf/settings')
  3. const RedisWrapper = require('../../infrastructure/RedisWrapper')
  4. const rclient = RedisWrapper.client('clsi_recently_compiled')
  5. const ProjectGetter = require('../Project/ProjectGetter')
  6. const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
  7. const UserGetter = require('../User/UserGetter')
  8. const ClsiManager = require('./ClsiManager')
  9. const Metrics = require('@overleaf/metrics')
  10. const { RateLimiter } = require('../../infrastructure/RateLimiter')
  11. const SplitTestHandler = require('../SplitTests/SplitTestHandler')
  12. const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache')
  13. const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF = new Date('2023-09-18T11:00:00.000Z')
  14. const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE = new Date(
  15. '2023-10-10T11:00:00.000Z'
  16. )
  17. module.exports = CompileManager = {
  18. NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF,
  19. NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE,
  20. compile(projectId, userId, options = {}, _callback) {
  21. const timer = new Metrics.Timer('editor.compile')
  22. const callback = function (...args) {
  23. timer.done()
  24. _callback(...args)
  25. }
  26. CompileManager._checkIfRecentlyCompiled(
  27. projectId,
  28. userId,
  29. function (error, recentlyCompiled) {
  30. if (error) {
  31. return callback(error)
  32. }
  33. if (recentlyCompiled) {
  34. return callback(null, 'too-recently-compiled', [])
  35. }
  36. CompileManager._checkIfAutoCompileLimitHasBeenHit(
  37. options.isAutoCompile,
  38. 'everyone',
  39. function (err, canCompile) {
  40. if (err || !canCompile) {
  41. return callback(null, 'autocompile-backoff', [])
  42. }
  43. ProjectRootDocManager.ensureRootDocumentIsSet(
  44. projectId,
  45. function (error) {
  46. if (error) {
  47. return callback(error)
  48. }
  49. CompileManager.getProjectCompileLimits(
  50. projectId,
  51. function (error, limits) {
  52. if (error) {
  53. return callback(error)
  54. }
  55. for (const key in limits) {
  56. const value = limits[key]
  57. options[key] = value
  58. }
  59. if (options.timeout !== 20) {
  60. // temporary override to force the new compile timeout
  61. if (options.forceNewCompileTimeout === 'active') {
  62. options.timeout = 20
  63. } else if (
  64. options.forceNewCompileTimeout === 'changing'
  65. ) {
  66. options.timeout = 60
  67. }
  68. }
  69. // Put a lower limit on autocompiles for free users, based on compileGroup
  70. CompileManager._checkCompileGroupAutoCompileLimit(
  71. options.isAutoCompile,
  72. limits.compileGroup,
  73. function (err, canCompile) {
  74. if (err || !canCompile) {
  75. return callback(null, 'autocompile-backoff', [])
  76. }
  77. // only pass userId down to clsi if this is a per-user compile
  78. const compileAsUser = Settings.disablePerUserCompiles
  79. ? undefined
  80. : userId
  81. ClsiManager.sendRequest(
  82. projectId,
  83. compileAsUser,
  84. options,
  85. function (
  86. error,
  87. status,
  88. outputFiles,
  89. clsiServerId,
  90. validationProblems,
  91. stats,
  92. timings,
  93. outputUrlPrefix
  94. ) {
  95. if (error) {
  96. return callback(error)
  97. }
  98. callback(
  99. null,
  100. status,
  101. outputFiles,
  102. clsiServerId,
  103. limits,
  104. validationProblems,
  105. stats,
  106. timings,
  107. outputUrlPrefix
  108. )
  109. }
  110. )
  111. }
  112. )
  113. }
  114. )
  115. }
  116. )
  117. }
  118. )
  119. }
  120. )
  121. },
  122. stopCompile(projectId, userId, callback) {
  123. CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
  124. if (error) {
  125. return callback(error)
  126. }
  127. ClsiManager.stopCompile(projectId, userId, limits, callback)
  128. })
  129. },
  130. deleteAuxFiles(projectId, userId, clsiserverid, callback) {
  131. CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
  132. if (error) {
  133. return callback(error)
  134. }
  135. ClsiManager.deleteAuxFiles(
  136. projectId,
  137. userId,
  138. limits,
  139. clsiserverid,
  140. callback
  141. )
  142. })
  143. },
  144. getProjectCompileLimits(projectId, callback) {
  145. ProjectGetter.getProject(
  146. projectId,
  147. { owner_ref: 1 },
  148. function (error, project) {
  149. if (error) {
  150. return callback(error)
  151. }
  152. UserGetter.getUser(
  153. project.owner_ref,
  154. {
  155. _id: 1,
  156. alphaProgram: 1,
  157. analyticsId: 1,
  158. betaProgram: 1,
  159. features: 1,
  160. splitTests: 1,
  161. signUpDate: 1, // for compile-timeout-20s
  162. },
  163. function (err, owner) {
  164. if (err) {
  165. return callback(err)
  166. }
  167. const ownerFeatures = (owner && owner.features) || {}
  168. // put alpha users into their own compile group
  169. if (owner && owner.alphaProgram) {
  170. ownerFeatures.compileGroup = 'alpha'
  171. }
  172. UserAnalyticsIdCache.callbacks.get(
  173. owner._id,
  174. function (err, analyticsId) {
  175. if (err) {
  176. return callback(err)
  177. }
  178. const limits = {
  179. timeout:
  180. ownerFeatures.compileTimeout ||
  181. Settings.defaultFeatures.compileTimeout,
  182. compileGroup:
  183. ownerFeatures.compileGroup ||
  184. Settings.defaultFeatures.compileGroup,
  185. ownerAnalyticsId: analyticsId,
  186. }
  187. CompileManager._getCompileBackendClassDetails(
  188. owner,
  189. limits.compileGroup,
  190. (
  191. err,
  192. { compileBackendClass, showFasterCompilesFeedbackUI }
  193. ) => {
  194. if (err) return callback(err)
  195. limits.compileBackendClass = compileBackendClass
  196. limits.showFasterCompilesFeedbackUI =
  197. showFasterCompilesFeedbackUI
  198. if (compileBackendClass === 'n2d' && limits.timeout <= 60) {
  199. // project owners with faster compiles but with <= 60 compile timeout (default)
  200. // will have a 20s compile timeout
  201. // The compile-timeout-20s split test exists to enable a gradual rollout
  202. SplitTestHandler.getAssignmentForMongoUser(
  203. owner,
  204. 'compile-timeout-20s',
  205. (err, assignment) => {
  206. if (err) return callback(err)
  207. // users who were on the 'default' servers at time of original rollout
  208. // will have a later cutoff date for the 20s timeout in the next phase
  209. // we check the backend class at version 8 (baseline)
  210. const backendClassHistory =
  211. owner.splitTests?.['compile-backend-class-n2d'] ||
  212. []
  213. const backendClassBaselineVariant =
  214. backendClassHistory.find(version => {
  215. return version.versionNumber === 8
  216. })?.variantName
  217. const timeoutEnforcedCutoff =
  218. backendClassBaselineVariant === 'default'
  219. ? NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE
  220. : NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF
  221. if (assignment?.variant === '20s') {
  222. if (owner.signUpDate > timeoutEnforcedCutoff) {
  223. limits.timeout = 20
  224. callback(null, limits)
  225. } else {
  226. SplitTestHandler.getAssignmentForMongoUser(
  227. owner,
  228. 'compile-timeout-20s-existing-users',
  229. (err, assignmentExistingUsers) => {
  230. if (err) return callback(err)
  231. if (
  232. assignmentExistingUsers?.variant === '20s'
  233. ) {
  234. limits.timeout = 20
  235. }
  236. callback(null, limits)
  237. }
  238. )
  239. }
  240. } else {
  241. callback(null, limits)
  242. }
  243. }
  244. )
  245. } else {
  246. callback(null, limits)
  247. }
  248. }
  249. )
  250. }
  251. )
  252. }
  253. )
  254. }
  255. )
  256. },
  257. COMPILE_DELAY: 1, // seconds
  258. _checkIfRecentlyCompiled(projectId, userId, callback) {
  259. const key = `compile:${projectId}:${userId}`
  260. rclient.set(
  261. key,
  262. true,
  263. 'EX',
  264. this.COMPILE_DELAY,
  265. 'NX',
  266. function (error, ok) {
  267. if (error) {
  268. return callback(error)
  269. }
  270. if (ok === 'OK') {
  271. callback(null, false)
  272. } else {
  273. callback(null, true)
  274. }
  275. }
  276. )
  277. },
  278. _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup, callback) {
  279. if (!isAutoCompile) {
  280. return callback(null, true)
  281. }
  282. if (compileGroup === 'standard') {
  283. // apply extra limits to the standard compile group
  284. CompileManager._checkIfAutoCompileLimitHasBeenHit(
  285. isAutoCompile,
  286. compileGroup,
  287. callback
  288. )
  289. } else {
  290. Metrics.inc(`auto-compile-${compileGroup}`)
  291. callback(null, true)
  292. }
  293. }, // always allow priority group users to compile
  294. _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup, callback) {
  295. if (!isAutoCompile) {
  296. return callback(null, true)
  297. }
  298. Metrics.inc(`auto-compile-${compileGroup}`)
  299. const rateLimiter = getAutoCompileRateLimiter(compileGroup)
  300. rateLimiter
  301. .consume('global')
  302. .then(() => {
  303. callback(null, true)
  304. })
  305. .catch(() => {
  306. // Don't differentiate between errors and rate limits. Silently trigger
  307. // the rate limit if there's an error consuming the points.
  308. Metrics.inc(`auto-compile-${compileGroup}-limited`)
  309. callback(null, false)
  310. })
  311. },
  312. _getCompileBackendClassDetails(owner, compileGroup, callback) {
  313. const { defaultBackendClass } = Settings.apis.clsi
  314. if (compileGroup === 'standard') {
  315. return SplitTestHandler.getAssignmentForMongoUser(
  316. owner,
  317. 'compile-backend-class-n2d',
  318. (err, assignment) => {
  319. if (err) return callback(err, {})
  320. const { variant } = assignment
  321. callback(null, {
  322. compileBackendClass:
  323. variant === 'default' ? defaultBackendClass : variant,
  324. showFasterCompilesFeedbackUI: false,
  325. })
  326. }
  327. )
  328. }
  329. SplitTestHandler.getAssignmentForMongoUser(
  330. owner,
  331. 'compile-backend-class',
  332. (err, assignment) => {
  333. if (err) return callback(err, {})
  334. const { analytics, variant } = assignment
  335. const activeForUser = analytics?.segmentation?.splitTest != null
  336. callback(null, {
  337. compileBackendClass:
  338. variant === 'default' ? defaultBackendClass : variant,
  339. showFasterCompilesFeedbackUI: activeForUser,
  340. })
  341. }
  342. )
  343. },
  344. wordCount(projectId, userId, file, clsiserverid, callback) {
  345. CompileManager.getProjectCompileLimits(projectId, function (error, limits) {
  346. if (error) {
  347. return callback(error)
  348. }
  349. ClsiManager.wordCount(
  350. projectId,
  351. userId,
  352. file,
  353. limits,
  354. clsiserverid,
  355. callback
  356. )
  357. })
  358. },
  359. }
  360. const autoCompileRateLimiters = new Map()
  361. function getAutoCompileRateLimiter(compileGroup) {
  362. let rateLimiter = autoCompileRateLimiters.get(compileGroup)
  363. if (rateLimiter == null) {
  364. rateLimiter = new RateLimiter(`auto-compile:${compileGroup}`, {
  365. points: Settings.rateLimit.autoCompile[compileGroup] || 25,
  366. duration: 20,
  367. })
  368. autoCompileRateLimiters.set(compileGroup, rateLimiter)
  369. }
  370. return rateLimiter
  371. }