ProjectManager.js 8.0 KB

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