ProjectManager.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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 getProjectRanges(projectId) {
  106. const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
  107. const docs = []
  108. for (const docId of docIds) {
  109. const ranges = await RedisManager.promises.getDocRanges(docId)
  110. docs.push({ id: docId, ranges })
  111. }
  112. return docs
  113. }
  114. async function clearProjectState(projectId) {
  115. await RedisManager.promises.clearProjectState(projectId)
  116. }
  117. async function updateProjectWithLocks(
  118. projectId,
  119. projectHistoryId,
  120. userId,
  121. updates,
  122. projectVersion,
  123. source
  124. ) {
  125. const timer = new Metrics.Timer('projectManager.updateProject')
  126. let projectSubversion = 0 // project versions can have multiple operations
  127. let projectOpsLength = 0
  128. for (const update of updates) {
  129. update.version = `${projectVersion}.${projectSubversion++}`
  130. switch (update.type) {
  131. case 'add-doc':
  132. projectOpsLength =
  133. await ProjectHistoryRedisManager.promises.queueAddEntity(
  134. projectId,
  135. projectHistoryId,
  136. 'doc',
  137. update.id,
  138. userId,
  139. update,
  140. source
  141. )
  142. break
  143. case 'rename-doc':
  144. if (!update.newPathname) {
  145. // an empty newPathname signifies a delete, so there is no need to
  146. // update the pathname in redis
  147. projectOpsLength =
  148. await ProjectHistoryRedisManager.promises.queueRenameEntity(
  149. projectId,
  150. projectHistoryId,
  151. 'doc',
  152. update.id,
  153. userId,
  154. update,
  155. source
  156. )
  157. } else {
  158. // rename the doc in redis before queuing the update
  159. await DocumentManager.promises.renameDocWithLock(
  160. projectId,
  161. update.id,
  162. userId,
  163. update,
  164. projectHistoryId
  165. )
  166. projectOpsLength =
  167. await ProjectHistoryRedisManager.promises.queueRenameEntity(
  168. projectId,
  169. projectHistoryId,
  170. 'doc',
  171. update.id,
  172. userId,
  173. update,
  174. source
  175. )
  176. }
  177. break
  178. case 'add-file':
  179. projectOpsLength =
  180. await ProjectHistoryRedisManager.promises.queueAddEntity(
  181. projectId,
  182. projectHistoryId,
  183. 'file',
  184. update.id,
  185. userId,
  186. update,
  187. source
  188. )
  189. break
  190. case 'rename-file':
  191. projectOpsLength =
  192. await ProjectHistoryRedisManager.promises.queueRenameEntity(
  193. projectId,
  194. projectHistoryId,
  195. 'file',
  196. update.id,
  197. userId,
  198. update,
  199. source
  200. )
  201. break
  202. default:
  203. throw new Error(`Unknown update type: ${update.type}`)
  204. }
  205. }
  206. if (
  207. HistoryManager.shouldFlushHistoryOps(
  208. projectId,
  209. projectOpsLength,
  210. updates.length,
  211. HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
  212. )
  213. ) {
  214. HistoryManager.flushProjectChangesAsync(projectId)
  215. }
  216. timer.done()
  217. }
  218. const ProjectManager = {
  219. flushProjectWithLocks,
  220. flushAndDeleteProjectWithLocks,
  221. queueFlushAndDeleteProject,
  222. getProjectDocsTimestamps,
  223. getProjectDocsAndFlushIfOld,
  224. getProjectRanges,
  225. clearProjectState,
  226. updateProjectWithLocks,
  227. }
  228. module.exports = {
  229. ...callbackifyAll(ProjectManager),
  230. promises: ProjectManager,
  231. }