DeleteQueueManager.js 5.1 KB

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