ProjectManager.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS207: Consider shorter variations of null checks
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. let ProjectManager
  14. const RedisManager = require('./RedisManager')
  15. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  16. const DocumentManager = require('./DocumentManager')
  17. const HistoryManager = require('./HistoryManager')
  18. const async = require('async')
  19. const logger = require('logger-sharelatex')
  20. const Metrics = require('./Metrics')
  21. const Errors = require('./Errors')
  22. module.exports = ProjectManager = {
  23. flushProjectWithLocks(project_id, _callback) {
  24. if (_callback == null) {
  25. _callback = function (error) {}
  26. }
  27. const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
  28. const callback = function (...args) {
  29. timer.done()
  30. _callback(...args)
  31. }
  32. RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
  33. if (error != null) {
  34. return callback(error)
  35. }
  36. const jobs = []
  37. const errors = []
  38. for (const doc_id of doc_ids) {
  39. ;((doc_id) =>
  40. jobs.push((callback) =>
  41. DocumentManager.flushDocIfLoadedWithLock(
  42. project_id,
  43. doc_id,
  44. function (error) {
  45. if (error != null && error instanceof Errors.NotFoundError) {
  46. logger.warn(
  47. { err: error, project_id, doc_id },
  48. 'found deleted doc when flushing'
  49. )
  50. callback()
  51. } else if (error != null) {
  52. logger.error(
  53. { err: error, project_id, doc_id },
  54. 'error flushing doc'
  55. )
  56. errors.push(error)
  57. callback()
  58. } else {
  59. callback()
  60. }
  61. }
  62. )
  63. ))(doc_id)
  64. }
  65. logger.log({ project_id, doc_ids }, 'flushing docs')
  66. async.series(jobs, function () {
  67. if (errors.length > 0) {
  68. callback(new Error('Errors flushing docs. See log for details'))
  69. } else {
  70. callback(null)
  71. }
  72. })
  73. })
  74. },
  75. flushAndDeleteProjectWithLocks(project_id, options, _callback) {
  76. if (_callback == null) {
  77. _callback = function (error) {}
  78. }
  79. const timer = new Metrics.Timer(
  80. 'projectManager.flushAndDeleteProjectWithLocks'
  81. )
  82. const callback = function (...args) {
  83. timer.done()
  84. _callback(...args)
  85. }
  86. RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
  87. if (error != null) {
  88. return callback(error)
  89. }
  90. const jobs = []
  91. const errors = []
  92. for (const doc_id of doc_ids) {
  93. ;((doc_id) =>
  94. jobs.push((callback) =>
  95. DocumentManager.flushAndDeleteDocWithLock(
  96. project_id,
  97. doc_id,
  98. {},
  99. function (error) {
  100. if (error != null) {
  101. logger.error(
  102. { err: error, project_id, doc_id },
  103. 'error deleting doc'
  104. )
  105. errors.push(error)
  106. }
  107. callback()
  108. }
  109. )
  110. ))(doc_id)
  111. }
  112. logger.log({ project_id, doc_ids }, 'deleting docs')
  113. async.series(jobs, () =>
  114. // When deleting the project here we want to ensure that project
  115. // history is completely flushed because the project may be
  116. // deleted in web after this call completes, and so further
  117. // attempts to flush would fail after that.
  118. HistoryManager.flushProjectChanges(project_id, options, function (
  119. error
  120. ) {
  121. if (errors.length > 0) {
  122. callback(new Error('Errors deleting docs. See log for details'))
  123. } else if (error != null) {
  124. callback(error)
  125. } else {
  126. callback(null)
  127. }
  128. })
  129. )
  130. })
  131. },
  132. queueFlushAndDeleteProject(project_id, callback) {
  133. if (callback == null) {
  134. callback = function (error) {}
  135. }
  136. RedisManager.queueFlushAndDeleteProject(project_id, function (error) {
  137. if (error != null) {
  138. logger.error(
  139. { project_id, error },
  140. 'error adding project to flush and delete queue'
  141. )
  142. return callback(error)
  143. }
  144. Metrics.inc('queued-delete')
  145. callback()
  146. })
  147. },
  148. getProjectDocsTimestamps(project_id, callback) {
  149. if (callback == null) {
  150. callback = function (error) {}
  151. }
  152. RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
  153. if (error != null) {
  154. return callback(error)
  155. }
  156. if (!(doc_ids != null ? doc_ids.length : undefined)) {
  157. return callback(null, [])
  158. }
  159. RedisManager.getDocTimestamps(doc_ids, function (error, timestamps) {
  160. if (error != null) {
  161. return callback(error)
  162. }
  163. callback(null, timestamps)
  164. })
  165. })
  166. },
  167. getProjectDocsAndFlushIfOld(
  168. project_id,
  169. projectStateHash,
  170. excludeVersions,
  171. _callback
  172. ) {
  173. if (excludeVersions == null) {
  174. excludeVersions = {}
  175. }
  176. if (_callback == null) {
  177. _callback = function (error, docs) {}
  178. }
  179. const timer = new Metrics.Timer(
  180. 'projectManager.getProjectDocsAndFlushIfOld'
  181. )
  182. const callback = function (...args) {
  183. timer.done()
  184. _callback(...args)
  185. }
  186. RedisManager.checkOrSetProjectState(project_id, projectStateHash, function (
  187. error,
  188. projectStateChanged
  189. ) {
  190. if (error != null) {
  191. logger.error(
  192. { err: error, project_id },
  193. 'error getting/setting project state in getProjectDocsAndFlushIfOld'
  194. )
  195. return callback(error)
  196. }
  197. // we can't return docs if project structure has changed
  198. if (projectStateChanged) {
  199. return callback(
  200. Errors.ProjectStateChangedError('project state changed')
  201. )
  202. }
  203. // project structure hasn't changed, return doc content from redis
  204. RedisManager.getDocIdsInProject(project_id, function (error, doc_ids) {
  205. if (error != null) {
  206. logger.error(
  207. { err: error, project_id },
  208. 'error getting doc ids in getProjectDocs'
  209. )
  210. return callback(error)
  211. }
  212. const jobs = []
  213. for (const doc_id of doc_ids) {
  214. ;((doc_id) =>
  215. jobs.push((
  216. cb // get the doc lines from redis
  217. ) =>
  218. DocumentManager.getDocAndFlushIfOldWithLock(
  219. project_id,
  220. doc_id,
  221. function (err, lines, version) {
  222. if (err != null) {
  223. logger.error(
  224. { err, project_id, doc_id },
  225. 'error getting project doc lines in getProjectDocsAndFlushIfOld'
  226. )
  227. return cb(err)
  228. }
  229. const doc = { _id: doc_id, lines, v: version } // create a doc object to return
  230. cb(null, doc)
  231. }
  232. )
  233. ))(doc_id)
  234. }
  235. async.series(jobs, function (error, docs) {
  236. if (error != null) {
  237. return callback(error)
  238. }
  239. callback(null, docs)
  240. })
  241. })
  242. })
  243. },
  244. clearProjectState(project_id, callback) {
  245. if (callback == null) {
  246. callback = function (error) {}
  247. }
  248. RedisManager.clearProjectState(project_id, callback)
  249. },
  250. updateProjectWithLocks(
  251. project_id,
  252. projectHistoryId,
  253. user_id,
  254. docUpdates,
  255. fileUpdates,
  256. version,
  257. _callback
  258. ) {
  259. if (_callback == null) {
  260. _callback = function (error) {}
  261. }
  262. const timer = new Metrics.Timer('projectManager.updateProject')
  263. const callback = function (...args) {
  264. timer.done()
  265. _callback(...args)
  266. }
  267. const project_version = version
  268. let project_subversion = 0 // project versions can have multiple operations
  269. let project_ops_length = 0
  270. const handleDocUpdate = function (projectUpdate, cb) {
  271. const doc_id = projectUpdate.id
  272. projectUpdate.version = `${project_version}.${project_subversion++}`
  273. if (projectUpdate.docLines != null) {
  274. ProjectHistoryRedisManager.queueAddEntity(
  275. project_id,
  276. projectHistoryId,
  277. 'doc',
  278. doc_id,
  279. user_id,
  280. projectUpdate,
  281. function (error, count) {
  282. project_ops_length = count
  283. cb(error)
  284. }
  285. )
  286. } else {
  287. DocumentManager.renameDocWithLock(
  288. project_id,
  289. doc_id,
  290. user_id,
  291. projectUpdate,
  292. projectHistoryId,
  293. function (error, count) {
  294. project_ops_length = count
  295. cb(error)
  296. }
  297. )
  298. }
  299. }
  300. const handleFileUpdate = function (projectUpdate, cb) {
  301. const file_id = projectUpdate.id
  302. projectUpdate.version = `${project_version}.${project_subversion++}`
  303. if (projectUpdate.url != null) {
  304. ProjectHistoryRedisManager.queueAddEntity(
  305. project_id,
  306. projectHistoryId,
  307. 'file',
  308. file_id,
  309. user_id,
  310. projectUpdate,
  311. function (error, count) {
  312. project_ops_length = count
  313. cb(error)
  314. }
  315. )
  316. } else {
  317. ProjectHistoryRedisManager.queueRenameEntity(
  318. project_id,
  319. projectHistoryId,
  320. 'file',
  321. file_id,
  322. user_id,
  323. projectUpdate,
  324. function (error, count) {
  325. project_ops_length = count
  326. cb(error)
  327. }
  328. )
  329. }
  330. }
  331. async.eachSeries(docUpdates, handleDocUpdate, function (error) {
  332. if (error != null) {
  333. return callback(error)
  334. }
  335. async.eachSeries(fileUpdates, handleFileUpdate, function (error) {
  336. if (error != null) {
  337. return callback(error)
  338. }
  339. if (
  340. HistoryManager.shouldFlushHistoryOps(
  341. project_ops_length,
  342. docUpdates.length + fileUpdates.length,
  343. HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
  344. )
  345. ) {
  346. HistoryManager.flushProjectChangesAsync(project_id)
  347. }
  348. callback()
  349. })
  350. })
  351. },
  352. }