backup_sample.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. // @ts-check
  2. import { ObjectId } from 'mongodb'
  3. import { READ_PREFERENCE_SECONDARY } from '@overleaf/mongo-utils/batchedUpdate.js'
  4. import { db, client } from '../lib/mongodb.js'
  5. const projectsCollection = db.collection('projects')
  6. // Enable caching for ObjectId.toString()
  7. ObjectId.cacheHexString = true
  8. // Configuration
  9. const SAMPLE_SIZE_PER_ITERATION = process.argv[2]
  10. ? parseInt(process.argv[2], 10)
  11. : 10000
  12. const TARGET_ERROR_PERCENTAGE = process.argv[3]
  13. ? parseFloat(process.argv[3])
  14. : 5.0
  15. let gracefulShutdownInitiated = false
  16. process.on('SIGINT', handleSignal)
  17. process.on('SIGTERM', handleSignal)
  18. function handleSignal() {
  19. gracefulShutdownInitiated = true
  20. console.warn('graceful shutdown initiated')
  21. }
  22. async function takeSample(sampleSize) {
  23. const results = await projectsCollection
  24. .aggregate(
  25. [
  26. { $sample: { size: sampleSize } },
  27. {
  28. $match: { 'overleaf.backup.lastBackedUpVersion': { $exists: true } },
  29. },
  30. {
  31. $count: 'total',
  32. },
  33. ],
  34. { readPreference: READ_PREFERENCE_SECONDARY }
  35. )
  36. .toArray()
  37. const count = results[0]?.total || 0
  38. return { totalSampled: sampleSize, backedUp: count }
  39. }
  40. function calculateStatistics(
  41. cumulativeSampled,
  42. cumulativeBackedUp,
  43. totalPopulation
  44. ) {
  45. const proportion = Math.max(1, cumulativeBackedUp) / cumulativeSampled
  46. // Standard error with finite population correction
  47. const fpc = Math.sqrt(
  48. (totalPopulation - cumulativeSampled) / (totalPopulation - 1)
  49. )
  50. const stdError =
  51. Math.sqrt((proportion * (1 - proportion)) / cumulativeSampled) * fpc
  52. // 95% confidence interval is approximately ±1.96 standard errors
  53. const marginOfError = 1.96 * stdError
  54. return {
  55. proportion,
  56. percentage: (proportion * 100).toFixed(2),
  57. marginOfError,
  58. errorPercentage: (marginOfError * 100).toFixed(2),
  59. lowerBound: ((proportion - marginOfError) * 100).toFixed(2),
  60. upperBound: ((proportion + marginOfError) * 100).toFixed(2),
  61. sampleSize: cumulativeSampled,
  62. populationSize: totalPopulation,
  63. }
  64. }
  65. async function main() {
  66. console.log('Date:', new Date().toISOString())
  67. const totalCount = await projectsCollection.estimatedDocumentCount({
  68. readPreference: READ_PREFERENCE_SECONDARY,
  69. })
  70. console.log(
  71. `Total projects in collection (estimated): ${totalCount.toLocaleString()}`
  72. )
  73. console.log(`Target margin of error: ${TARGET_ERROR_PERCENTAGE}%`)
  74. let cumulativeSampled = 0
  75. let cumulativeBackedUp = 0
  76. let currentError = Infinity
  77. let iteration = 0
  78. console.log('Iteration | Total Sampled | % Backed Up | Margin of Error')
  79. console.log('----------|---------------|-------------|----------------')
  80. while (currentError > TARGET_ERROR_PERCENTAGE) {
  81. if (gracefulShutdownInitiated) {
  82. console.log('Graceful shutdown initiated. Exiting sampling loop.')
  83. break
  84. }
  85. iteration++
  86. const { totalSampled, backedUp } = await takeSample(
  87. SAMPLE_SIZE_PER_ITERATION
  88. )
  89. cumulativeSampled += totalSampled
  90. cumulativeBackedUp += backedUp
  91. const stats = calculateStatistics(
  92. cumulativeSampled,
  93. cumulativeBackedUp,
  94. totalCount
  95. )
  96. currentError = parseFloat(stats.errorPercentage)
  97. console.log(
  98. `${iteration.toString().padStart(9)} | ` +
  99. `${cumulativeSampled.toString().padStart(13)} | ` +
  100. `${stats.percentage.padStart(10)}% | ` +
  101. `\u00B1${stats.errorPercentage}%`
  102. )
  103. // Small delay between iterations
  104. await new Promise(resolve => setTimeout(resolve, 100))
  105. }
  106. const finalStats = calculateStatistics(
  107. cumulativeSampled,
  108. cumulativeBackedUp,
  109. totalCount
  110. )
  111. console.log(
  112. `Projects sampled: ${cumulativeSampled.toLocaleString()} out of ${totalCount.toLocaleString()}`
  113. )
  114. console.log(
  115. `Estimated percentage with lastBackedUpVersion: ${finalStats.percentage}%`
  116. )
  117. console.log(
  118. `95% Confidence Interval: ${finalStats.lowerBound}% - ${finalStats.upperBound}%`
  119. )
  120. console.log(`Final Margin of Error: \u00B1${finalStats.errorPercentage}%`)
  121. }
  122. main()
  123. .then(() => console.log('Done.'))
  124. .catch(err => {
  125. console.error('Error:', err)
  126. process.exitCode = 1
  127. })
  128. .finally(() => {
  129. client.close().catch(err => console.error('Error closing MongoDB:', err))
  130. })