ProjectManager.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. const RedisManager = require('./RedisManager')
  2. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  3. const DocumentManager = require('./DocumentManager')
  4. const HistoryManager = require('./HistoryManager')
  5. const logger = require('@overleaf/logger')
  6. const Metrics = require('./Metrics')
  7. const Errors = require('./Errors')
  8. const { callbackifyAll } = require('@overleaf/promise-utils')
  9. async function flushProjectWithLocks(projectId) {
  10. const timer = new Metrics.Timer('projectManager.flushProjectWithLocks')
  11. const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
  12. logger.debug({ projectId, docIds }, 'flushing docs')
  13. const errors = []
  14. for (const docId of docIds) {
  15. try {
  16. await DocumentManager.promises.flushDocIfLoadedWithLock(projectId, docId)
  17. } catch (error) {
  18. if (error instanceof Errors.NotFoundError) {
  19. logger.warn(
  20. { err: error, projectId, docId },
  21. 'found deleted doc when flushing'
  22. )
  23. } else {
  24. logger.error({ err: error, projectId, docId }, 'error flushing doc')
  25. errors.push(error)
  26. }
  27. }
  28. }
  29. timer.done()
  30. if (errors.length > 0) {
  31. throw new Error('Errors flushing docs. See log for details')
  32. }
  33. }
  34. async function flushAndDeleteProjectWithLocks(projectId, options) {
  35. const timer = new Metrics.Timer(
  36. 'projectManager.flushAndDeleteProjectWithLocks'
  37. )
  38. const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
  39. logger.debug({ projectId, docIds }, 'deleting docs')
  40. const errors = []
  41. for (const docId of docIds) {
  42. try {
  43. await DocumentManager.promises.flushAndDeleteDocWithLock(
  44. projectId,
  45. docId,
  46. {}
  47. )
  48. } catch (error) {
  49. logger.error({ err: error, projectId, docId }, 'error deleting doc')
  50. errors.push(error)
  51. }
  52. }
  53. // When deleting the project here we want to ensure that project
  54. // history is completely flushed because the project may be
  55. // deleted in web after this call completes, and so further
  56. // attempts to flush would fail after that.
  57. await HistoryManager.promises.flushProjectChanges(projectId, options)
  58. timer.done()
  59. if (errors.length > 0) {
  60. throw new Error('Errors deleting docs. See log for details')
  61. }
  62. }
  63. async function queueFlushAndDeleteProject(projectId) {
  64. await RedisManager.promises.queueFlushAndDeleteProject(projectId)
  65. Metrics.inc('queued-delete')
  66. }
  67. async function getProjectDocsTimestamps(projectId, callback) {
  68. const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
  69. if (docIds.length === 0) {
  70. return []
  71. }
  72. const timestamps = await RedisManager.promises.getDocTimestamps(docIds)
  73. return timestamps
  74. }
  75. async function getProjectDocsAndFlushIfOld(
  76. projectId,
  77. projectStateHash,
  78. excludeVersions
  79. ) {
  80. const timer = new Metrics.Timer('projectManager.getProjectDocsAndFlushIfOld')
  81. const projectStateChanged =
  82. await RedisManager.promises.checkOrSetProjectState(
  83. projectId,
  84. projectStateHash
  85. )
  86. // we can't return docs if project structure has changed
  87. if (projectStateChanged) {
  88. timer.done()
  89. throw new Errors.ProjectStateChangedError('project state changed')
  90. }
  91. // project structure hasn't changed, return doc content from redis
  92. const docs = []
  93. const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
  94. for (const docId of docIds) {
  95. const { lines, version } =
  96. await DocumentManager.promises.getDocAndFlushIfOldWithLock(
  97. projectId,
  98. docId
  99. )
  100. docs.push({ _id: docId, lines, v: version })
  101. }
  102. timer.done()
  103. return docs
  104. }
  105. async function clearProjectState(projectId) {
  106. await RedisManager.promises.clearProjectState(projectId)
  107. }
  108. async function updateProjectWithLocks(
  109. projectId,
  110. projectHistoryId,
  111. userId,
  112. updates,
  113. projectVersion,
  114. source
  115. ) {
  116. const timer = new Metrics.Timer('projectManager.updateProject')
  117. let projectSubversion = 0 // project versions can have multiple operations
  118. let projectOpsLength = 0
  119. for (const update of updates) {
  120. update.version = `${projectVersion}.${projectSubversion++}`
  121. switch (update.type) {
  122. case 'add-doc':
  123. projectOpsLength =
  124. await ProjectHistoryRedisManager.promises.queueAddEntity(
  125. projectId,
  126. projectHistoryId,
  127. 'doc',
  128. update.id,
  129. userId,
  130. update,
  131. source
  132. )
  133. break
  134. case 'rename-doc':
  135. if (!update.newPathname) {
  136. // an empty newPathname signifies a delete, so there is no need to
  137. // update the pathname in redis
  138. projectOpsLength =
  139. await ProjectHistoryRedisManager.promises.queueRenameEntity(
  140. projectId,
  141. projectHistoryId,
  142. 'doc',
  143. update.id,
  144. userId,
  145. update,
  146. source
  147. )
  148. } else {
  149. // rename the doc in redis before queuing the update
  150. await DocumentManager.promises.renameDocWithLock(
  151. projectId,
  152. update.id,
  153. userId,
  154. update,
  155. projectHistoryId
  156. )
  157. projectOpsLength =
  158. await ProjectHistoryRedisManager.promises.queueRenameEntity(
  159. projectId,
  160. projectHistoryId,
  161. 'doc',
  162. update.id,
  163. userId,
  164. update,
  165. source
  166. )
  167. }
  168. break
  169. case 'add-file':
  170. projectOpsLength =
  171. await ProjectHistoryRedisManager.promises.queueAddEntity(
  172. projectId,
  173. projectHistoryId,
  174. 'file',
  175. update.id,
  176. userId,
  177. update,
  178. source
  179. )
  180. break
  181. case 'rename-file':
  182. projectOpsLength =
  183. await ProjectHistoryRedisManager.promises.queueRenameEntity(
  184. projectId,
  185. projectHistoryId,
  186. 'file',
  187. update.id,
  188. userId,
  189. update,
  190. source
  191. )
  192. break
  193. default:
  194. throw new Error(`Unknown update type: ${update.type}`)
  195. }
  196. }
  197. if (
  198. HistoryManager.shouldFlushHistoryOps(
  199. projectId,
  200. projectOpsLength,
  201. updates.length,
  202. HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
  203. )
  204. ) {
  205. HistoryManager.flushProjectChangesAsync(projectId)
  206. }
  207. timer.done()
  208. }
  209. const ProjectManager = {
  210. flushProjectWithLocks,
  211. flushAndDeleteProjectWithLocks,
  212. queueFlushAndDeleteProject,
  213. getProjectDocsTimestamps,
  214. getProjectDocsAndFlushIfOld,
  215. clearProjectState,
  216. updateProjectWithLocks,
  217. }
  218. module.exports = {
  219. ...callbackifyAll(ProjectManager),
  220. promises: ProjectManager,
  221. }