DeleteQueueManager.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. /* eslint-disable
  2. no-unused-vars,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS207: Consider shorter variations of null checks
  11. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  12. */
  13. let DeleteQueueManager
  14. const Settings = require('@overleaf/settings')
  15. const RedisManager = require('./RedisManager')
  16. const ProjectManager = require('./ProjectManager')
  17. const logger = require('@overleaf/logger')
  18. const metrics = require('./Metrics')
  19. // Maintain a sorted set of project flushAndDelete requests, ordered by timestamp
  20. // (ZADD), and process them from oldest to newest. A flushAndDelete request comes
  21. // from real-time and is triggered when a user leaves a project.
  22. //
  23. // The aim is to remove the project from redis 5 minutes after the last request
  24. // if there has been no activity (document updates) in that time. If there is
  25. // activity we can expect a further flushAndDelete request when the editing user
  26. // leaves the project.
  27. //
  28. // If a new flushAndDelete request comes in while an existing request is already
  29. // in the queue we update the timestamp as we can postpone flushing further.
  30. //
  31. // Documents are processed by checking the queue, seeing if the first entry is
  32. // older than 5 minutes, and popping it from the queue in that case.
  33. module.exports = DeleteQueueManager = {
  34. flushAndDeleteOldProjects(options, callback) {
  35. const startTime = Date.now()
  36. const cutoffTime =
  37. startTime - options.min_delete_age + 100 * (Math.random() - 0.5)
  38. let count = 0
  39. const flushProjectIfNotModified = (projectId, flushTimestamp, cb) =>
  40. ProjectManager.getProjectDocsTimestamps(
  41. projectId,
  42. function (err, timestamps) {
  43. if (err != null) {
  44. return callback(err)
  45. }
  46. if (timestamps.length === 0) {
  47. logger.debug(
  48. { projectId },
  49. 'skipping flush of queued project - no timestamps'
  50. )
  51. return cb()
  52. }
  53. // are any of the timestamps newer than the time the project was flushed?
  54. for (const timestamp of Array.from(timestamps)) {
  55. if (timestamp > flushTimestamp) {
  56. metrics.inc('queued-delete-skipped')
  57. logger.debug(
  58. { projectId, timestamps, flushTimestamp },
  59. 'found newer timestamp, will skip delete'
  60. )
  61. return cb()
  62. }
  63. }
  64. logger.debug({ projectId, flushTimestamp }, 'flushing queued project')
  65. return ProjectManager.flushAndDeleteProjectWithLocks(
  66. projectId,
  67. { skip_history_flush: false },
  68. function (err) {
  69. if (err != null) {
  70. logger.err({ projectId, err }, 'error flushing queued project')
  71. }
  72. metrics.inc('queued-delete-completed')
  73. return cb(null, true)
  74. }
  75. )
  76. }
  77. )
  78. function flushNextProject() {
  79. const now = Date.now()
  80. if (now - startTime > options.timeout) {
  81. logger.debug('hit time limit on flushing old projects')
  82. return callback(null, count)
  83. }
  84. if (count > options.limit) {
  85. logger.debug('hit count limit on flushing old projects')
  86. return callback(null, count)
  87. }
  88. return RedisManager.getNextProjectToFlushAndDelete(
  89. cutoffTime,
  90. function (err, projectId, flushTimestamp, queueLength) {
  91. if (err != null) {
  92. return callback(err, count)
  93. }
  94. if (projectId == null) {
  95. return callback(null, count)
  96. }
  97. logger.debug({ projectId, queueLength }, 'flushing queued project')
  98. metrics.globalGauge('queued-flush-backlog', queueLength)
  99. return flushProjectIfNotModified(
  100. projectId,
  101. flushTimestamp,
  102. function (err, flushed) {
  103. if (err) {
  104. // Do not stop processing the queue in case the flush fails.
  105. // Slowing down the processing can fill up redis.
  106. metrics.inc('queued-delete-error')
  107. }
  108. if (flushed) {
  109. count++
  110. }
  111. return flushNextProject()
  112. }
  113. )
  114. }
  115. )
  116. }
  117. return flushNextProject()
  118. },
  119. startBackgroundFlush() {
  120. const SHORT_DELAY = 10
  121. const LONG_DELAY = 1000
  122. function doFlush() {
  123. if (Settings.shuttingDown) {
  124. logger.info('discontinuing background flush due to shutdown')
  125. return
  126. }
  127. return DeleteQueueManager.flushAndDeleteOldProjects(
  128. {
  129. timeout: 1000,
  130. min_delete_age: 3 * 60 * 1000,
  131. limit: 1000, // high value, to ensure we always flush enough projects
  132. },
  133. (_err, flushed) =>
  134. setTimeout(doFlush, flushed > 10 ? SHORT_DELAY : LONG_DELAY)
  135. )
  136. }
  137. return doFlush()
  138. },
  139. }