CompileManager.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. max-len,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS103: Rewrite code to no longer use __guard__
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let CompileManager
  17. const Settings = require('settings-sharelatex')
  18. const RedisWrapper = require('../../infrastructure/RedisWrapper')
  19. const rclient = RedisWrapper.client('clsi_recently_compiled')
  20. const ProjectGetter = require('../Project/ProjectGetter')
  21. const ProjectRootDocManager = require('../Project/ProjectRootDocManager')
  22. const UserGetter = require('../User/UserGetter')
  23. const ClsiManager = require('./ClsiManager')
  24. const Metrics = require('@overleaf/metrics')
  25. const rateLimiter = require('../../infrastructure/RateLimiter')
  26. module.exports = CompileManager = {
  27. compile(project_id, user_id, options, _callback) {
  28. if (options == null) {
  29. options = {}
  30. }
  31. if (_callback == null) {
  32. _callback = function(error) {}
  33. }
  34. const timer = new Metrics.Timer('editor.compile')
  35. const callback = function(...args) {
  36. timer.done()
  37. return _callback(...Array.from(args || []))
  38. }
  39. return CompileManager._checkIfRecentlyCompiled(
  40. project_id,
  41. user_id,
  42. function(error, recentlyCompiled) {
  43. if (error != null) {
  44. return callback(error)
  45. }
  46. if (recentlyCompiled) {
  47. return callback(null, 'too-recently-compiled', [])
  48. }
  49. return CompileManager._checkIfAutoCompileLimitHasBeenHit(
  50. options.isAutoCompile,
  51. 'everyone',
  52. function(err, canCompile) {
  53. if (!canCompile) {
  54. return callback(null, 'autocompile-backoff', [])
  55. }
  56. return ProjectRootDocManager.ensureRootDocumentIsSet(
  57. project_id,
  58. function(error) {
  59. if (error != null) {
  60. return callback(error)
  61. }
  62. return CompileManager.getProjectCompileLimits(
  63. project_id,
  64. function(error, limits) {
  65. if (error != null) {
  66. return callback(error)
  67. }
  68. for (let key in limits) {
  69. const value = limits[key]
  70. options[key] = value
  71. }
  72. // Put a lower limit on autocompiles for free users, based on compileGroup
  73. return CompileManager._checkCompileGroupAutoCompileLimit(
  74. options.isAutoCompile,
  75. limits.compileGroup,
  76. function(err, canCompile) {
  77. if (!canCompile) {
  78. return callback(null, 'autocompile-backoff', [])
  79. }
  80. // only pass user_id down to clsi if this is a per-user compile
  81. const compileAsUser = Settings.disablePerUserCompiles
  82. ? undefined
  83. : user_id
  84. return ClsiManager.sendRequest(
  85. project_id,
  86. compileAsUser,
  87. options,
  88. function(
  89. error,
  90. status,
  91. outputFiles,
  92. clsiServerId,
  93. validationProblems
  94. ) {
  95. if (error != null) {
  96. return callback(error)
  97. }
  98. return callback(
  99. null,
  100. status,
  101. outputFiles,
  102. clsiServerId,
  103. limits,
  104. validationProblems
  105. )
  106. }
  107. )
  108. }
  109. )
  110. }
  111. )
  112. }
  113. )
  114. }
  115. )
  116. }
  117. )
  118. },
  119. stopCompile(project_id, user_id, callback) {
  120. if (callback == null) {
  121. callback = function(error) {}
  122. }
  123. return CompileManager.getProjectCompileLimits(project_id, function(
  124. error,
  125. limits
  126. ) {
  127. if (error != null) {
  128. return callback(error)
  129. }
  130. return ClsiManager.stopCompile(project_id, user_id, limits, callback)
  131. })
  132. },
  133. deleteAuxFiles(project_id, user_id, callback) {
  134. if (callback == null) {
  135. callback = function(error) {}
  136. }
  137. return CompileManager.getProjectCompileLimits(project_id, function(
  138. error,
  139. limits
  140. ) {
  141. if (error != null) {
  142. return callback(error)
  143. }
  144. return ClsiManager.deleteAuxFiles(project_id, user_id, limits, callback)
  145. })
  146. },
  147. getProjectCompileLimits(project_id, callback) {
  148. if (callback == null) {
  149. callback = function(error, limits) {}
  150. }
  151. return ProjectGetter.getProject(project_id, { owner_ref: 1 }, function(
  152. error,
  153. project
  154. ) {
  155. if (error != null) {
  156. return callback(error)
  157. }
  158. return UserGetter.getUser(
  159. project.owner_ref,
  160. { alphaProgram: 1, betaProgram: 1, features: 1 },
  161. function(err, owner) {
  162. if (error != null) {
  163. return callback(error)
  164. }
  165. let ownerFeatures = (owner && owner.features) || {}
  166. // put alpha users into their own compile group
  167. if (owner && owner.alphaProgram) {
  168. ownerFeatures.compileGroup = 'alpha'
  169. }
  170. return callback(null, {
  171. timeout:
  172. ownerFeatures.compileTimeout ||
  173. Settings.defaultFeatures.compileTimeout,
  174. compileGroup:
  175. ownerFeatures.compileGroup ||
  176. Settings.defaultFeatures.compileGroup
  177. })
  178. }
  179. )
  180. })
  181. },
  182. COMPILE_DELAY: 1, // seconds
  183. _checkIfRecentlyCompiled(project_id, user_id, callback) {
  184. if (callback == null) {
  185. callback = function(error, recentlyCompiled) {}
  186. }
  187. const key = `compile:${project_id}:${user_id}`
  188. return rclient.set(key, true, 'EX', this.COMPILE_DELAY, 'NX', function(
  189. error,
  190. ok
  191. ) {
  192. if (error != null) {
  193. return callback(error)
  194. }
  195. if (ok === 'OK') {
  196. return callback(null, false)
  197. } else {
  198. return callback(null, true)
  199. }
  200. })
  201. },
  202. _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup, callback) {
  203. if (callback == null) {
  204. callback = function(err, canCompile) {}
  205. }
  206. if (!isAutoCompile) {
  207. return callback(null, true)
  208. }
  209. if (compileGroup === 'standard') {
  210. // apply extra limits to the standard compile group
  211. return CompileManager._checkIfAutoCompileLimitHasBeenHit(
  212. isAutoCompile,
  213. compileGroup,
  214. callback
  215. )
  216. } else {
  217. Metrics.inc(`auto-compile-${compileGroup}`)
  218. return callback(null, true)
  219. }
  220. }, // always allow priority group users to compile
  221. _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup, callback) {
  222. if (callback == null) {
  223. callback = function(err, canCompile) {}
  224. }
  225. if (!isAutoCompile) {
  226. return callback(null, true)
  227. }
  228. Metrics.inc(`auto-compile-${compileGroup}`)
  229. const opts = {
  230. endpointName: 'auto_compile',
  231. timeInterval: 20,
  232. subjectName: compileGroup,
  233. throttle: Settings.rateLimit.autoCompile[compileGroup] || 25
  234. }
  235. return rateLimiter.addCount(opts, function(err, canCompile) {
  236. if (err != null) {
  237. canCompile = false
  238. }
  239. if (!canCompile) {
  240. Metrics.inc(`auto-compile-${compileGroup}-limited`)
  241. }
  242. return callback(err, canCompile)
  243. })
  244. },
  245. wordCount(project_id, user_id, file, callback) {
  246. if (callback == null) {
  247. callback = function(error) {}
  248. }
  249. return CompileManager.getProjectCompileLimits(project_id, function(
  250. error,
  251. limits
  252. ) {
  253. if (error != null) {
  254. return callback(error)
  255. }
  256. return ClsiManager.wordCount(project_id, user_id, file, limits, callback)
  257. })
  258. }
  259. }