FlushManager.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Fix any style issues and re-enable lint.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS101: Remove unnecessary use of Array.from
  6. * DS102: Remove unnecessary code created because of implicit returns
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  9. */
  10. import async from 'async'
  11. import logger from '@overleaf/logger'
  12. import OError from '@overleaf/o-error'
  13. import metrics from '@overleaf/metrics'
  14. import Settings from '@overleaf/settings'
  15. import _ from 'lodash'
  16. import * as RedisManager from './RedisManager.js'
  17. import * as UpdatesProcessor from './UpdatesProcessor.js'
  18. import * as ErrorRecorder from './ErrorRecorder.js'
  19. export function flushIfOld(projectId, cutoffTime, callback) {
  20. if (callback == null) {
  21. callback = function () {}
  22. }
  23. return RedisManager.getFirstOpTimestamp(
  24. projectId,
  25. function (err, firstOpTimestamp) {
  26. if (err != null) {
  27. return callback(OError.tag(err))
  28. }
  29. // In the normal case, the flush marker will be set with the
  30. // timestamp of the oldest operation in the queue by docupdater.
  31. // If the marker is not set for any reason, we flush it anyway
  32. // for safety.
  33. if (!firstOpTimestamp || firstOpTimestamp < cutoffTime) {
  34. logger.debug(
  35. { projectId, firstOpTimestamp, cutoffTime },
  36. 'flushing old project'
  37. )
  38. metrics.inc('flush-old-updates', 1, { status: 'flushed' })
  39. return UpdatesProcessor.processUpdatesForProject(projectId, callback)
  40. } else if (Settings.shortHistoryQueues.includes(projectId)) {
  41. logger.debug(
  42. { projectId, firstOpTimestamp, cutoffTime },
  43. 'flushing project with short queue'
  44. )
  45. metrics.inc('flush-old-updates', 1, { status: 'short-queue' })
  46. return UpdatesProcessor.processUpdatesForProject(projectId, callback)
  47. } else {
  48. metrics.inc('flush-old-updates', 1, { status: 'skipped' })
  49. return callback()
  50. }
  51. }
  52. )
  53. }
  54. export function flushOldOps(options, callback) {
  55. if (callback == null) {
  56. callback = function () {}
  57. }
  58. logger.debug({ options }, 'starting flush of old ops')
  59. // allow running flush in background for cron jobs
  60. if (options.background) {
  61. // return immediate response to client, then discard callback
  62. callback(null, { message: 'running flush in background' })
  63. callback = function () {}
  64. }
  65. return RedisManager.getProjectIdsWithHistoryOps(
  66. null,
  67. function (error, projectIds) {
  68. if (error != null) {
  69. return callback(OError.tag(error))
  70. }
  71. return ErrorRecorder.getFailedProjects(
  72. function (error, projectHistoryFailures) {
  73. if (error != null) {
  74. return callback(OError.tag(error))
  75. }
  76. // exclude failed projects already in projectHistoryFailures
  77. const failedProjects = new Set()
  78. for (const entry of Array.from(projectHistoryFailures)) {
  79. failedProjects.add(entry.project_id)
  80. }
  81. // randomise order so we get different projects if there is a limit
  82. projectIds = _.shuffle(projectIds)
  83. const maxAge = options.maxAge || 6 * 3600 // default to 6 hours
  84. const cutoffTime = new Date(Date.now() - maxAge * 1000)
  85. const startTime = new Date()
  86. let count = 0
  87. const jobs = projectIds.map(
  88. projectId =>
  89. function (cb) {
  90. const timeTaken = new Date() - startTime
  91. count++
  92. if (
  93. (options != null ? options.timeout : undefined) &&
  94. timeTaken > options.timeout
  95. ) {
  96. // finish early due to timeout, return an error to bail out of the async iteration
  97. logger.debug('background retries timed out')
  98. return cb(new OError('retries timed out'))
  99. }
  100. if (
  101. (options != null ? options.limit : undefined) &&
  102. count > options.limit
  103. ) {
  104. // finish early due to reaching limit, return an error to bail out of the async iteration
  105. logger.debug({ count }, 'background retries hit limit')
  106. return cb(new OError('hit limit'))
  107. }
  108. if (failedProjects.has(projectId)) {
  109. // skip failed projects
  110. return setTimeout(cb, options.queueDelay || 100) // pause between flushes
  111. }
  112. return flushIfOld(projectId, cutoffTime, function (err) {
  113. if (err != null) {
  114. logger.warn(
  115. { projectId, err },
  116. 'error flushing old project'
  117. )
  118. }
  119. return setTimeout(cb, options.queueDelay || 100)
  120. })
  121. }
  122. ) // pause between flushes
  123. return async.series(
  124. async.reflectAll(jobs),
  125. function (error, results) {
  126. const success = []
  127. const failure = []
  128. results.forEach((result, i) => {
  129. if (
  130. result.error != null &&
  131. !['retries timed out', 'hit limit'].includes(
  132. result?.error?.message
  133. )
  134. ) {
  135. // ignore expected errors
  136. return failure.push(projectIds[i])
  137. } else {
  138. return success.push(projectIds[i])
  139. }
  140. })
  141. return callback(error, { success, failure, failedProjects })
  142. }
  143. )
  144. }
  145. )
  146. }
  147. )
  148. }