backup_sample.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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. $project: {
  29. _id: 0,
  30. hasBackup: {
  31. $ifNull: ['$overleaf.backup.lastBackedUpVersion', false],
  32. },
  33. },
  34. },
  35. {
  36. $group: {
  37. _id: null,
  38. totalSampled: { $sum: 1 },
  39. backedUp: {
  40. $sum: {
  41. $cond: ['$hasBackup', 1, 0],
  42. },
  43. },
  44. },
  45. },
  46. ],
  47. { readPreference: READ_PREFERENCE_SECONDARY }
  48. )
  49. .toArray()
  50. if (results.length === 0) {
  51. return { totalSampled: 0, backedUp: 0 }
  52. }
  53. return results[0]
  54. }
  55. function calculateStatistics(
  56. cumulativeSampled,
  57. cumulativeBackedUp,
  58. totalPopulation
  59. ) {
  60. const proportion = cumulativeBackedUp / cumulativeSampled
  61. // Standard error with finite population correction
  62. const fpc = Math.sqrt(
  63. (totalPopulation - cumulativeSampled) / (totalPopulation - 1)
  64. )
  65. const stdError =
  66. Math.sqrt((proportion * (1 - proportion)) / cumulativeSampled) * fpc
  67. // 95% confidence interval is approximately ±1.96 standard errors
  68. const marginOfError = 1.96 * stdError
  69. return {
  70. proportion,
  71. percentage: (proportion * 100).toFixed(2),
  72. marginOfError,
  73. errorPercentage: (marginOfError * 100).toFixed(2),
  74. lowerBound: ((proportion - marginOfError) * 100).toFixed(2),
  75. upperBound: ((proportion + marginOfError) * 100).toFixed(2),
  76. sampleSize: cumulativeSampled,
  77. populationSize: totalPopulation,
  78. }
  79. }
  80. async function main() {
  81. console.log('Date:', new Date().toISOString())
  82. const totalCount = await projectsCollection.estimatedDocumentCount({
  83. readPreference: READ_PREFERENCE_SECONDARY,
  84. })
  85. console.log(
  86. `Total projects in collection (estimated): ${totalCount.toLocaleString()}`
  87. )
  88. console.log(`Target margin of error: ${TARGET_ERROR_PERCENTAGE}%`)
  89. let cumulativeSampled = 0
  90. let cumulativeBackedUp = 0
  91. let currentError = Infinity
  92. let iteration = 0
  93. console.log('Iteration | Total Sampled | % Backed Up | Margin of Error')
  94. console.log('----------|---------------|-------------|----------------')
  95. while (currentError > TARGET_ERROR_PERCENTAGE) {
  96. if (gracefulShutdownInitiated) {
  97. console.log('Graceful shutdown initiated. Exiting sampling loop.')
  98. break
  99. }
  100. iteration++
  101. const { totalSampled, backedUp } = await takeSample(
  102. SAMPLE_SIZE_PER_ITERATION
  103. )
  104. cumulativeSampled += totalSampled
  105. cumulativeBackedUp += backedUp
  106. const stats = calculateStatistics(
  107. cumulativeSampled,
  108. cumulativeBackedUp,
  109. totalCount
  110. )
  111. currentError = parseFloat(stats.errorPercentage)
  112. console.log(
  113. `${iteration.toString().padStart(9)} | ` +
  114. `${cumulativeSampled.toString().padStart(13)} | ` +
  115. `${stats.percentage.padStart(10)}% | ` +
  116. `\u00B1${stats.errorPercentage}%`
  117. )
  118. // Small delay between iterations
  119. await new Promise(resolve => setTimeout(resolve, 100))
  120. }
  121. const finalStats = calculateStatistics(
  122. cumulativeSampled,
  123. cumulativeBackedUp,
  124. totalCount
  125. )
  126. console.log(
  127. `Projects sampled: ${cumulativeSampled.toLocaleString()} out of ${totalCount.toLocaleString()}`
  128. )
  129. console.log(
  130. `Estimated percentage with lastBackedUpVersion: ${finalStats.percentage}%`
  131. )
  132. console.log(
  133. `95% Confidence Interval: ${finalStats.lowerBound}% - ${finalStats.upperBound}%`
  134. )
  135. console.log(`Final Margin of Error: \u00B1${finalStats.errorPercentage}%`)
  136. }
  137. main()
  138. .then(() => console.log('Done.'))
  139. .catch(err => {
  140. console.error('Error:', err)
  141. process.exitCode = 1
  142. })
  143. .finally(() => {
  144. client.close().catch(err => console.error('Error closing MongoDB:', err))
  145. })