PackWorker.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /* eslint-disable
  2. no-unused-vars,
  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. * DS103: Rewrite code to no longer use __guard__
  11. * DS205: Consider reworking code to avoid use of IIFEs
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let LIMIT, pending
  16. let projectId, docId
  17. const { callbackify } = require('util')
  18. const Settings = require('@overleaf/settings')
  19. const async = require('async')
  20. const _ = require('underscore')
  21. const { db, ObjectId, waitForDb, closeDb } = require('./mongodb')
  22. const fs = require('fs')
  23. const Metrics = require('@overleaf/metrics')
  24. Metrics.initialize('track-changes')
  25. const logger = require('@overleaf/logger')
  26. logger.initialize('track-changes-packworker')
  27. if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
  28. logger.initializeErrorReporting(Settings.sentry.dsn)
  29. }
  30. const DAYS = 24 * 3600 * 1000
  31. const LockManager = require('./LockManager')
  32. const PackManager = require('./PackManager')
  33. // this worker script is forked by the main process to look for
  34. // document histories which can be archived
  35. const source = process.argv[2]
  36. const DOCUMENT_PACK_DELAY = Number(process.argv[3]) || 1000
  37. const TIMEOUT = Number(process.argv[4]) || 30 * 60 * 1000
  38. let COUNT = 0 // number processed
  39. let TOTAL = 0 // total number to process
  40. if (!source.match(/^[0-9]+$/)) {
  41. const file = fs.readFileSync(source)
  42. const result = (() => {
  43. const result1 = []
  44. for (const line of Array.from(file.toString().split('\n'))) {
  45. ;[projectId, docId] = Array.from(line.split(' '))
  46. result1.push({ doc_id: docId, project_id: projectId })
  47. }
  48. return result1
  49. })()
  50. pending = _.filter(result, row =>
  51. __guard__(row != null ? row.doc_id : undefined, x =>
  52. x.match(/^[a-f0-9]{24}$/)
  53. )
  54. )
  55. } else {
  56. LIMIT = Number(process.argv[2]) || 1000
  57. }
  58. let shutDownRequested = false
  59. const shutDownTimer = setTimeout(function () {
  60. logger.debug('pack timed out, requesting shutdown')
  61. // start the shutdown on the next pack
  62. shutDownRequested = true
  63. // do a hard shutdown after a further 5 minutes
  64. const hardTimeout = setTimeout(function () {
  65. logger.error('HARD TIMEOUT in pack archive worker')
  66. return process.exit()
  67. }, 5 * 60 * 1000)
  68. return hardTimeout.unref()
  69. }, TIMEOUT)
  70. logger.debug(
  71. `checking for updates, limit=${LIMIT}, delay=${DOCUMENT_PACK_DELAY}, timeout=${TIMEOUT}`
  72. )
  73. const finish = function () {
  74. if (shutDownTimer != null) {
  75. logger.debug('cancelling timeout')
  76. clearTimeout(shutDownTimer)
  77. }
  78. logger.debug('closing db')
  79. callbackify(closeDb)(function () {
  80. logger.debug('closing LockManager Redis Connection')
  81. return LockManager.close(function () {
  82. logger.debug(
  83. { processedCount: COUNT, allCount: TOTAL },
  84. 'ready to exit from pack archive worker'
  85. )
  86. const hardTimeout = setTimeout(function () {
  87. logger.error('hard exit from pack archive worker')
  88. return process.exit(1)
  89. }, 5 * 1000)
  90. return hardTimeout.unref()
  91. })
  92. })
  93. }
  94. process.on('exit', code => logger.debug({ code }, 'pack archive worker exited'))
  95. const processUpdates = pending =>
  96. async.eachSeries(
  97. pending,
  98. function (result, callback) {
  99. let _id
  100. ;({ _id, project_id: projectId, doc_id: docId } = result)
  101. COUNT++
  102. logger.debug({ projectId, docId }, `processing ${COUNT}/${TOTAL}`)
  103. if (projectId == null || docId == null) {
  104. logger.debug(
  105. { projectId, docId },
  106. 'skipping pack, missing project/doc id'
  107. )
  108. return callback()
  109. }
  110. const handler = function (err, result) {
  111. if (err != null && err.code === 'InternalError' && err.retryable) {
  112. logger.warn(
  113. { err, result },
  114. 'ignoring S3 error in pack archive worker'
  115. )
  116. // Ignore any s3 errors due to random problems
  117. err = null
  118. }
  119. if (err != null) {
  120. logger.error({ err, result }, 'error in pack archive worker')
  121. return callback(err)
  122. }
  123. if (shutDownRequested) {
  124. logger.warn('shutting down pack archive worker')
  125. return callback(new Error('shutdown'))
  126. }
  127. return setTimeout(() => callback(err, result), DOCUMENT_PACK_DELAY)
  128. }
  129. if (_id == null) {
  130. return PackManager.pushOldPacks(projectId, docId, handler)
  131. } else {
  132. return PackManager.processOldPack(projectId, docId, _id, handler)
  133. }
  134. },
  135. function (err, results) {
  136. if (err != null && err.message !== 'shutdown') {
  137. logger.error({ err }, 'error in pack archive worker processUpdates')
  138. }
  139. return finish()
  140. }
  141. )
  142. // find the packs which can be archived
  143. const ObjectIdFromDate = function (date) {
  144. const id = Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000'
  145. return ObjectId(id)
  146. }
  147. // new approach, two passes
  148. // find packs to be marked as finalised:true, those which have a newer pack present
  149. // then only consider finalised:true packs for archiving
  150. waitForDb()
  151. .then(() => {
  152. if (pending != null) {
  153. logger.debug(`got ${pending.length} entries from ${source}`)
  154. processUpdates(pending)
  155. } else {
  156. processFromOneWeekAgo()
  157. }
  158. })
  159. .catch(err => {
  160. logger.fatal({ err }, 'cannot connect to mongo, exiting')
  161. process.exit(1)
  162. })
  163. function processFromOneWeekAgo() {
  164. const oneWeekAgo = new Date(Date.now() - 7 * DAYS)
  165. db.docHistory
  166. .find(
  167. {
  168. expiresAt: { $exists: false },
  169. project_id: { $exists: true },
  170. v_end: { $exists: true },
  171. _id: { $lt: ObjectIdFromDate(oneWeekAgo) },
  172. last_checked: { $lt: oneWeekAgo },
  173. },
  174. { projection: { _id: 1, doc_id: 1, project_id: 1 } }
  175. )
  176. .sort({
  177. last_checked: 1,
  178. })
  179. .limit(LIMIT)
  180. .toArray(function (err, results) {
  181. if (err != null) {
  182. logger.debug({ err }, 'error checking for updates')
  183. finish()
  184. return
  185. }
  186. pending = _.uniq(results, false, result => result.doc_id.toString())
  187. TOTAL = pending.length
  188. logger.debug(`found ${TOTAL} documents to archive`)
  189. return processUpdates(pending)
  190. })
  191. }
  192. function __guard__(value, transform) {
  193. return typeof value !== 'undefined' && value !== null
  194. ? transform(value)
  195. : undefined
  196. }