FlushManager.js 5.3 KB

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