HistoryManager.js 3.6 KB

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