PackWorker.js 6.4 KB

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