FlushManager.js 5.6 KB

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