HistoryManager.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. const async = require('async')
  2. const logger = require('@overleaf/logger')
  3. const { promisifyAll } = require('@overleaf/promise-utils')
  4. const request = require('request')
  5. const Settings = require('@overleaf/settings')
  6. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  7. const metrics = require('./Metrics')
  8. const HistoryManager = {
  9. // flush changes in the background
  10. flushProjectChangesAsync(projectId) {
  11. HistoryManager.flushProjectChanges(
  12. projectId,
  13. { background: true },
  14. function () {}
  15. )
  16. },
  17. // flush changes and callback (for when we need to know the queue is flushed)
  18. flushProjectChanges(projectId, options, callback) {
  19. if (callback == null) {
  20. callback = function () {}
  21. }
  22. if (options.skip_history_flush) {
  23. logger.debug({ projectId }, 'skipping flush of project history')
  24. return callback()
  25. }
  26. metrics.inc('history-flush', 1, { status: 'project-history' })
  27. const url = `${Settings.apis.project_history.url}/project/${projectId}/flush`
  28. const qs = {}
  29. if (options.background) {
  30. qs.background = true
  31. } // pass on the background flush option if present
  32. logger.debug({ projectId, url, qs }, 'flushing doc in project history api')
  33. request.post({ url, qs }, function (error, res, body) {
  34. if (error) {
  35. logger.error({ error, projectId }, 'project history api request failed')
  36. callback(error)
  37. } else if (res.statusCode < 200 && res.statusCode >= 300) {
  38. logger.error(
  39. { projectId },
  40. `project history api returned a failure status code: ${res.statusCode}`
  41. )
  42. callback(error)
  43. } else {
  44. callback()
  45. }
  46. })
  47. },
  48. FLUSH_DOC_EVERY_N_OPS: 100,
  49. FLUSH_PROJECT_EVERY_N_OPS: 500,
  50. recordAndFlushHistoryOps(projectId, ops, projectOpsLength) {
  51. if (ops == null) {
  52. ops = []
  53. }
  54. if (ops.length === 0) {
  55. return
  56. }
  57. // record updates for project history
  58. if (
  59. HistoryManager.shouldFlushHistoryOps(
  60. projectId,
  61. projectOpsLength,
  62. ops.length,
  63. HistoryManager.FLUSH_PROJECT_EVERY_N_OPS
  64. )
  65. ) {
  66. // Do this in the background since it uses HTTP and so may be too
  67. // slow to wait for when processing a doc update.
  68. logger.debug(
  69. { projectOpsLength, projectId },
  70. 'flushing project history api'
  71. )
  72. HistoryManager.flushProjectChangesAsync(projectId)
  73. }
  74. },
  75. shouldFlushHistoryOps(projectId, length, opsLength, threshold) {
  76. if (Settings.shortHistoryQueues.includes(projectId)) return true
  77. if (!length) {
  78. return false
  79. } // don't flush unless we know the length
  80. // We want to flush every 100 ops, i.e. 100, 200, 300, etc
  81. // Find out which 'block' (i.e. 0-99, 100-199) we were in before and after pushing these
  82. // ops. If we've changed, then we've gone over a multiple of 100 and should flush.
  83. // (Most of the time, we will only hit 100 and then flushing will put us back to 0)
  84. const previousLength = length - opsLength
  85. const prevBlock = Math.floor(previousLength / threshold)
  86. const newBlock = Math.floor(length / threshold)
  87. return newBlock !== prevBlock
  88. },
  89. MAX_PARALLEL_REQUESTS: 4,
  90. resyncProjectHistory(
  91. projectId,
  92. projectHistoryId,
  93. docs,
  94. files,
  95. opts,
  96. callback
  97. ) {
  98. ProjectHistoryRedisManager.queueResyncProjectStructure(
  99. projectId,
  100. projectHistoryId,
  101. docs,
  102. files,
  103. opts,
  104. function (error) {
  105. if (error) {
  106. return callback(error)
  107. }
  108. if (opts.resyncProjectStructureOnly) return callback()
  109. const DocumentManager = require('./DocumentManager')
  110. const resyncDoc = (doc, cb) => {
  111. DocumentManager.resyncDocContentsWithLock(
  112. projectId,
  113. doc.doc,
  114. doc.path,
  115. opts,
  116. cb
  117. )
  118. }
  119. async.eachLimit(
  120. docs,
  121. HistoryManager.MAX_PARALLEL_REQUESTS,
  122. resyncDoc,
  123. callback
  124. )
  125. }
  126. )
  127. },
  128. }
  129. module.exports = HistoryManager
  130. module.exports.promises = promisifyAll(HistoryManager, {
  131. without: [
  132. 'flushProjectChangesAsync',
  133. 'recordAndFlushHistoryOps',
  134. 'shouldFlushHistoryOps',
  135. ],
  136. })