backfill_start_version.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. const commandLineArgs = require('command-line-args')
  2. const BPromise = require('bluebird')
  3. const timersPromises = require('node:timers/promises')
  4. const { knex, historyStore } = require('..')
  5. const MAX_POSTGRES_INTEGER = 2147483647
  6. const DEFAULT_BATCH_SIZE = 1000
  7. const DEFAULT_CONCURRENCY = 1
  8. const MAX_RETRIES = 10
  9. const RETRY_DELAY_MS = 5000
  10. async function main() {
  11. const options = parseOptions()
  12. let batchStart = options.minId
  13. while (batchStart <= options.maxId) {
  14. const chunks = await getChunks(batchStart, options.maxId, options.batchSize)
  15. if (chunks.length === 0) {
  16. // No results. We're done.
  17. break
  18. }
  19. const batchEnd = chunks[chunks.length - 1].id
  20. await processBatch(chunks, options)
  21. console.log(`Processed chunks ${batchStart} to ${batchEnd}`)
  22. batchStart = batchEnd + 1
  23. }
  24. }
  25. function parseOptions() {
  26. const args = commandLineArgs([
  27. { name: 'min-id', type: Number, defaultValue: 1 },
  28. {
  29. name: 'max-id',
  30. type: Number,
  31. defaultValue: MAX_POSTGRES_INTEGER,
  32. },
  33. { name: 'batch-size', type: Number, defaultValue: DEFAULT_BATCH_SIZE },
  34. { name: 'concurrency', type: Number, defaultValue: DEFAULT_CONCURRENCY },
  35. ])
  36. return {
  37. minId: args['min-id'],
  38. maxId: args['max-id'],
  39. batchSize: args['batch-size'],
  40. concurrency: args.concurrency,
  41. }
  42. }
  43. async function getChunks(minId, maxId, batchSize) {
  44. const chunks = await knex('chunks')
  45. .where('id', '>=', minId)
  46. .andWhere('id', '<=', maxId)
  47. .orderBy('id')
  48. .limit(batchSize)
  49. return chunks
  50. }
  51. async function processBatch(chunks, options) {
  52. let retries = 0
  53. while (true) {
  54. const results = await BPromise.map(chunks, processChunk, {
  55. concurrency: options.concurrency,
  56. })
  57. const failedChunks = results
  58. .filter(result => !result.success)
  59. .map(result => result.chunk)
  60. if (failedChunks.length === 0) {
  61. // All chunks processed. Carry on.
  62. break
  63. }
  64. // Some projects failed. Retry.
  65. retries += 1
  66. if (retries > MAX_RETRIES) {
  67. console.log('Too many retries processing chunks. Giving up.')
  68. process.exit(1)
  69. }
  70. console.log(
  71. `Retrying chunks: ${failedChunks.map(chunk => chunk.id).join(', ')}`
  72. )
  73. await timersPromises.setTimeout(RETRY_DELAY_MS)
  74. chunks = failedChunks
  75. }
  76. }
  77. async function processChunk(chunk) {
  78. try {
  79. const rawHistory = await historyStore.loadRaw(
  80. chunk.doc_id.toString(),
  81. chunk.id
  82. )
  83. const startVersion = chunk.end_version - rawHistory.changes.length
  84. await knex('chunks')
  85. .where('id', chunk.id)
  86. .update({ start_version: startVersion })
  87. return { chunk, success: true }
  88. } catch (err) {
  89. console.error(`Failed to process chunk ${chunk.id}:`, err.stack)
  90. return { chunk, success: false }
  91. }
  92. }
  93. main()
  94. .then(() => {
  95. process.exit()
  96. })
  97. .catch(err => {
  98. console.error(err)
  99. process.exit(1)
  100. })