ProjectManager.js 9.1 KB

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