ProjectManager.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. const RedisManager = require('./RedisManager')
  2. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  3. const DocumentManager = require('./DocumentManager')
  4. const HistoryManager = require('./HistoryManager')
  5. const async = require('async')
  6. const logger = require('logger-sharelatex')
  7. const Metrics = require('./Metrics')
  8. const Errors = require('./Errors')
  9. module.exports = {
  10. flushProjectWithLocks,
  11. flushAndDeleteProjectWithLocks,
  12. queueFlushAndDeleteProject,
  13. getProjectDocsTimestamps,
  14. getProjectDocsAndFlushIfOld,
  15. clearProjectState,
  16. updateProjectWithLocks,
  17. }
  18. function flushProjectWithLocks(projectId, _callback) {
  19. const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
  20. const callback = function (...args) {
  21. timer.done()
  22. _callback(...args)
  23. }
  24. RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
  25. if (error) {
  26. return callback(error)
  27. }
  28. const errors = []
  29. const jobs = docIds.map(docId => callback => {
  30. DocumentManager.flushDocIfLoadedWithLock(projectId, docId, error => {
  31. if (error instanceof Errors.NotFoundError) {
  32. logger.warn(
  33. { err: error, projectId, docId },
  34. 'found deleted doc when flushing'
  35. )
  36. callback()
  37. } else if (error) {
  38. logger.error({ err: error, projectId, docId }, 'error flushing doc')
  39. errors.push(error)
  40. callback()
  41. } else {
  42. callback()
  43. }
  44. })
  45. })
  46. logger.log({ projectId, docIds }, 'flushing docs')
  47. async.series(jobs, () => {
  48. if (errors.length > 0) {
  49. callback(new Error('Errors flushing docs. See log for details'))
  50. } else {
  51. callback(null)
  52. }
  53. })
  54. })
  55. }
  56. function flushAndDeleteProjectWithLocks(projectId, options, _callback) {
  57. const timer = new Metrics.Timer(
  58. 'projectManager.flushAndDeleteProjectWithLocks'
  59. )
  60. const callback = function (...args) {
  61. timer.done()
  62. _callback(...args)
  63. }
  64. RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
  65. if (error) {
  66. return callback(error)
  67. }
  68. const errors = []
  69. const jobs = docIds.map(docId => callback => {
  70. DocumentManager.flushAndDeleteDocWithLock(projectId, docId, {}, error => {
  71. if (error) {
  72. logger.error({ err: error, projectId, docId }, 'error deleting doc')
  73. errors.push(error)
  74. }
  75. callback()
  76. })
  77. })
  78. logger.log({ projectId, docIds }, 'deleting docs')
  79. async.series(jobs, () =>
  80. // When deleting the project here we want to ensure that project
  81. // history is completely flushed because the project may be
  82. // deleted in web after this call completes, and so further
  83. // attempts to flush would fail after that.
  84. HistoryManager.flushProjectChanges(projectId, options, error => {
  85. if (errors.length > 0) {
  86. callback(new Error('Errors deleting docs. See log for details'))
  87. } else if (error) {
  88. callback(error)
  89. } else {
  90. callback(null)
  91. }
  92. })
  93. )
  94. })
  95. }
  96. function queueFlushAndDeleteProject(projectId, callback) {
  97. RedisManager.queueFlushAndDeleteProject(projectId, error => {
  98. if (error) {
  99. logger.error(
  100. { projectId, error },
  101. 'error adding project to flush and delete queue'
  102. )
  103. return callback(error)
  104. }
  105. Metrics.inc('queued-delete')
  106. callback()
  107. })
  108. }
  109. function getProjectDocsTimestamps(projectId, callback) {
  110. RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
  111. if (error) {
  112. return callback(error)
  113. }
  114. if (docIds.length === 0) {
  115. return callback(null, [])
  116. }
  117. RedisManager.getDocTimestamps(docIds, (error, timestamps) => {
  118. if (error) {
  119. return callback(error)
  120. }
  121. callback(null, timestamps)
  122. })
  123. })
  124. }
  125. function getProjectDocsAndFlushIfOld(
  126. projectId,
  127. projectStateHash,
  128. excludeVersions,
  129. _callback
  130. ) {
  131. const timer = new Metrics.Timer('projectManager.getProjectDocsAndFlushIfOld')
  132. const callback = function (...args) {
  133. timer.done()
  134. _callback(...args)
  135. }
  136. RedisManager.checkOrSetProjectState(
  137. projectId,
  138. projectStateHash,
  139. (error, projectStateChanged) => {
  140. if (error) {
  141. logger.error(
  142. { err: error, projectId },
  143. 'error getting/setting project state in getProjectDocsAndFlushIfOld'
  144. )
  145. return callback(error)
  146. }
  147. // we can't return docs if project structure has changed
  148. if (projectStateChanged) {
  149. return callback(
  150. Errors.ProjectStateChangedError('project state changed')
  151. )
  152. }
  153. // project structure hasn't changed, return doc content from redis
  154. RedisManager.getDocIdsInProject(projectId, (error, docIds) => {
  155. if (error) {
  156. logger.error(
  157. { err: error, projectId },
  158. 'error getting doc ids in getProjectDocs'
  159. )
  160. return callback(error)
  161. }
  162. // get the doc lines from redis
  163. const jobs = docIds.map(docId => cb => {
  164. DocumentManager.getDocAndFlushIfOldWithLock(
  165. projectId,
  166. docId,
  167. (err, lines, version) => {
  168. if (err) {
  169. logger.error(
  170. { err, projectId, docId },
  171. 'error getting project doc lines in getProjectDocsAndFlushIfOld'
  172. )
  173. return cb(err)
  174. }
  175. const doc = { _id: docId, lines, v: version } // create a doc object to return
  176. cb(null, doc)
  177. }
  178. )
  179. })
  180. async.series(jobs, (error, docs) => {
  181. if (error) {
  182. return callback(error)
  183. }
  184. callback(null, docs)
  185. })
  186. })
  187. }
  188. )
  189. }
  190. function clearProjectState(projectId, callback) {
  191. RedisManager.clearProjectState(projectId, callback)
  192. }
  193. function updateProjectWithLocks(
  194. projectId,
  195. projectHistoryId,
  196. userId,
  197. updates,
  198. projectVersion,
  199. _callback
  200. ) {
  201. const timer = new Metrics.Timer('projectManager.updateProject')
  202. const callback = function (...args) {
  203. timer.done()
  204. _callback(...args)
  205. }
  206. let projectSubversion = 0 // project versions can have multiple operations
  207. let projectOpsLength = 0
  208. function handleUpdate(update, cb) {
  209. update.version = `${projectVersion}.${projectSubversion++}`
  210. switch (update.type) {
  211. case 'add-doc':
  212. ProjectHistoryRedisManager.queueAddEntity(
  213. projectId,
  214. projectHistoryId,
  215. 'doc',
  216. update.id,
  217. userId,
  218. update,
  219. (error, count) => {
  220. projectOpsLength = count
  221. cb(error)
  222. }
  223. )
  224. break
  225. case 'rename-doc':
  226. DocumentManager.renameDocWithLock(
  227. projectId,
  228. update.id,
  229. userId,
  230. update,
  231. projectHistoryId,
  232. (error, count) => {
  233. projectOpsLength = count
  234. cb(error)
  235. }
  236. )
  237. break
  238. case 'add-file':
  239. ProjectHistoryRedisManager.queueAddEntity(
  240. projectId,
  241. projectHistoryId,
  242. 'file',
  243. update.id,
  244. userId,
  245. update,
  246. (error, count) => {
  247. projectOpsLength = count
  248. cb(error)
  249. }
  250. )
  251. break
  252. case 'rename-file':
  253. ProjectHistoryRedisManager.queueRenameEntity(
  254. projectId,
  255. projectHistoryId,
  256. 'file',
  257. update.id,
  258. userId,
  259. update,
  260. (error, count) => {
  261. projectOpsLength = count
  262. cb(error)
  263. }
  264. )
  265. break
  266. default:
  267. cb(new Error(`Unknown update type: ${update.type}`))
  268. }
  269. }
  270. async.eachSeries(updates, handleUpdate, error => {
  271. if (error) {
  272. return callback(error)
  273. }
  274. if (
  275. HistoryManager.shouldFlushHistoryOps(
  276. projectOpsLength,
  277. updates.length,
  278. HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
  279. )
  280. ) {
  281. HistoryManager.flushProjectChangesAsync(projectId)
  282. }
  283. callback()
  284. })
  285. }