FlushManager.js 5.1 KB

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