CompileManager.js 9.6 KB

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