batchedUpdate.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. const { ReadPreference, ObjectId } = require('mongodb')
  2. const { db, waitForDb } = require('../../app/src/infrastructure/mongodb')
  3. const ONE_MONTH_IN_MS = 1000 * 60 * 60 * 24 * 31
  4. let ID_EDGE_PAST
  5. const ID_EDGE_FUTURE = objectIdFromMs(Date.now() + 1000)
  6. let BATCH_DESCENDING
  7. let BATCH_SIZE
  8. let VERBOSE_LOGGING
  9. let BATCH_RANGE_START
  10. let BATCH_RANGE_END
  11. let BATCH_MAX_TIME_SPAN_IN_MS
  12. function refreshGlobalOptionsForBatchedUpdate(options = {}) {
  13. options = Object.assign({}, options, process.env)
  14. BATCH_DESCENDING = options.BATCH_DESCENDING === 'true'
  15. BATCH_SIZE = parseInt(options.BATCH_SIZE, 10) || 1000
  16. VERBOSE_LOGGING = options.VERBOSE_LOGGING === 'true'
  17. if (options.BATCH_LAST_ID) {
  18. BATCH_RANGE_START = ObjectId(options.BATCH_LAST_ID)
  19. } else if (options.BATCH_RANGE_START) {
  20. BATCH_RANGE_START = ObjectId(options.BATCH_RANGE_START)
  21. } else {
  22. if (BATCH_DESCENDING) {
  23. BATCH_RANGE_START = ID_EDGE_FUTURE
  24. } else {
  25. BATCH_RANGE_START = ID_EDGE_PAST
  26. }
  27. }
  28. BATCH_MAX_TIME_SPAN_IN_MS =
  29. parseInt(options.BATCH_MAX_TIME_SPAN_IN_MS, 10) || ONE_MONTH_IN_MS
  30. if (options.BATCH_RANGE_END) {
  31. BATCH_RANGE_END = ObjectId(options.BATCH_RANGE_END)
  32. } else {
  33. if (BATCH_DESCENDING) {
  34. BATCH_RANGE_END = ID_EDGE_PAST
  35. } else {
  36. BATCH_RANGE_END = ID_EDGE_FUTURE
  37. }
  38. }
  39. }
  40. async function getNextBatch({
  41. collection,
  42. query,
  43. start,
  44. end,
  45. projection,
  46. findOptions,
  47. }) {
  48. if (BATCH_DESCENDING) {
  49. query._id = {
  50. $gt: end,
  51. $lt: start,
  52. }
  53. } else {
  54. query._id = {
  55. $gt: start,
  56. $lt: end,
  57. }
  58. }
  59. return await collection
  60. .find(query, findOptions)
  61. .project(projection)
  62. .sort({ _id: BATCH_DESCENDING ? -1 : 1 })
  63. .limit(BATCH_SIZE)
  64. .toArray()
  65. }
  66. async function performUpdate(collection, nextBatch, update) {
  67. return collection.updateMany(
  68. { _id: { $in: nextBatch.map(entry => entry._id) } },
  69. update
  70. )
  71. }
  72. function objectIdFromMs(ms) {
  73. return ObjectId.createFromTime(ms / 1000)
  74. }
  75. function getMsFromObjectId(id) {
  76. return id.getTimestamp().getTime()
  77. }
  78. function getNextEnd(start) {
  79. let end
  80. if (BATCH_DESCENDING) {
  81. end = objectIdFromMs(getMsFromObjectId(start) - BATCH_MAX_TIME_SPAN_IN_MS)
  82. if (getMsFromObjectId(end) <= getMsFromObjectId(BATCH_RANGE_END)) {
  83. end = BATCH_RANGE_END
  84. }
  85. } else {
  86. end = objectIdFromMs(getMsFromObjectId(start) + BATCH_MAX_TIME_SPAN_IN_MS)
  87. if (getMsFromObjectId(end) >= getMsFromObjectId(BATCH_RANGE_END)) {
  88. end = BATCH_RANGE_END
  89. }
  90. }
  91. return end
  92. }
  93. async function getIdEdgePast(collection) {
  94. const [first] = await collection
  95. .find({})
  96. .project({ _id: 1 })
  97. .limit(1)
  98. .toArray()
  99. if (!first) return null
  100. // Go 1s further into the past in order to include the first entry via
  101. // first._id > ID_EDGE_PAST
  102. return objectIdFromMs(Math.max(0, getMsFromObjectId(first._id) - 1000))
  103. }
  104. async function batchedUpdate(
  105. collectionName,
  106. query,
  107. update,
  108. projection,
  109. findOptions,
  110. batchedUpdateOptions
  111. ) {
  112. await waitForDb()
  113. const collection = db[collectionName]
  114. ID_EDGE_PAST = await getIdEdgePast(collection)
  115. if (!ID_EDGE_PAST) {
  116. console.warn(`The collection ${collectionName} appears to be empty.`)
  117. return 0
  118. }
  119. refreshGlobalOptionsForBatchedUpdate(batchedUpdateOptions)
  120. findOptions = findOptions || {}
  121. findOptions.readPreference = ReadPreference.SECONDARY
  122. projection = projection || { _id: 1 }
  123. let nextBatch
  124. let updated = 0
  125. let start = BATCH_RANGE_START
  126. while (start !== BATCH_RANGE_END) {
  127. let end = getNextEnd(start)
  128. nextBatch = await getNextBatch({
  129. collection,
  130. query,
  131. start,
  132. end,
  133. projection,
  134. findOptions,
  135. })
  136. if (nextBatch.length > 0) {
  137. end = nextBatch[nextBatch.length - 1]._id
  138. updated += nextBatch.length
  139. if (VERBOSE_LOGGING) {
  140. console.log(
  141. `Running update on batch with ids ${JSON.stringify(
  142. nextBatch.map(entry => entry._id)
  143. )}`
  144. )
  145. } else {
  146. console.error(`Running update on batch ending ${end}`)
  147. }
  148. if (typeof update === 'function') {
  149. await update(nextBatch)
  150. } else {
  151. await performUpdate(collection, nextBatch, update)
  152. }
  153. }
  154. console.error(`Completed batch ending ${end}`)
  155. start = end
  156. }
  157. return updated
  158. }
  159. function batchedUpdateWithResultHandling(
  160. collection,
  161. query,
  162. update,
  163. projection,
  164. options
  165. ) {
  166. batchedUpdate(collection, query, update, projection, options)
  167. .then(processed => {
  168. console.error({ processed })
  169. process.exit(0)
  170. })
  171. .catch(error => {
  172. console.error({ error })
  173. process.exit(1)
  174. })
  175. }
  176. module.exports = {
  177. batchedUpdate,
  178. batchedUpdateWithResultHandling,
  179. }