CompileManager.js 11 KB

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