resync_subscriptions.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. const { Subscription } = require('../../app/src/models/Subscription')
  2. const RecurlyWrapper = require('../../app/src/Features/Subscription/RecurlyWrapper')
  3. const SubscriptionUpdater = require('../../app/src/Features/Subscription/SubscriptionUpdater')
  4. const minimist = require('minimist')
  5. const { setTimeout } = require('node:timers/promises')
  6. // make sure all `allMismatchReasons` are displayed in the output
  7. const util = require('util')
  8. const pLimit = require('p-limit')
  9. const { waitForDb } = require('../../app/src/infrastructure/mongodb')
  10. util.inspect.defaultOptions.maxArrayLength = null
  11. const ScriptLogger = {
  12. checkedSubscriptionsCount: 0,
  13. mismatchSubscriptionsCount: 0,
  14. allMismatchReasons: {},
  15. recordMismatch: (subscription, recurlySubscription) => {
  16. const mismatchReasons = {}
  17. if (subscription.planCode !== recurlySubscription.plan.plan_code) {
  18. mismatchReasons.recurlyPlan = recurlySubscription.plan.plan_code
  19. mismatchReasons.olPlan = subscription.planCode
  20. }
  21. if (recurlySubscription.state === 'expired') {
  22. mismatchReasons.state = 'expired'
  23. }
  24. if (!Object.keys(mismatchReasons).length) {
  25. return
  26. }
  27. ScriptLogger.mismatchSubscriptionsCount += 1
  28. const mismatchReasonsString = JSON.stringify(mismatchReasons)
  29. if (ScriptLogger.allMismatchReasons[mismatchReasonsString]) {
  30. ScriptLogger.allMismatchReasons[mismatchReasonsString].push({
  31. id: subscription._id,
  32. name: subscription.planCode,
  33. })
  34. } else {
  35. ScriptLogger.allMismatchReasons[mismatchReasonsString] = [
  36. {
  37. id: subscription._id,
  38. name: subscription.planCode,
  39. },
  40. ]
  41. }
  42. },
  43. printProgress: () => {
  44. console.warn(
  45. `Subscriptions checked: ${ScriptLogger.checkedSubscriptionsCount}. Mismatches: ${ScriptLogger.mismatchSubscriptionsCount}`
  46. )
  47. },
  48. printSummary: () => {
  49. console.log('All Mismatch Reasons:', ScriptLogger.allMismatchReasons)
  50. console.log(
  51. 'Mismatch Subscriptions Count',
  52. ScriptLogger.mismatchSubscriptionsCount
  53. )
  54. },
  55. }
  56. const handleSyncSubscriptionError = async (subscription, error) => {
  57. console.warn(`Errors with subscription id=${subscription._id}:`, error)
  58. if (typeof error === 'string' && error.match(/429$/)) {
  59. await setTimeout(1000 * 60 * 5)
  60. return
  61. }
  62. if (typeof error === 'string' && error.match(/5\d\d$/)) {
  63. await setTimeout(1000 * 60)
  64. await syncSubscription(subscription)
  65. return
  66. }
  67. await setTimeout(80)
  68. }
  69. const syncSubscription = async subscription => {
  70. let recurlySubscription
  71. try {
  72. recurlySubscription = await RecurlyWrapper.promises.getSubscription(
  73. subscription.recurlySubscription_id
  74. )
  75. } catch (error) {
  76. await handleSyncSubscriptionError(subscription, error)
  77. return
  78. }
  79. ScriptLogger.recordMismatch(subscription, recurlySubscription)
  80. if (!COMMIT) {
  81. return
  82. }
  83. try {
  84. await SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  85. recurlySubscription,
  86. subscription,
  87. {}
  88. )
  89. } catch (error) {
  90. await handleSyncSubscriptionError(subscription, error)
  91. return
  92. }
  93. await setTimeout(80)
  94. }
  95. const syncSubscriptions = async subscriptions => {
  96. const limit = pLimit(ASYNC_LIMIT)
  97. return await Promise.all(
  98. subscriptions.map(subscription =>
  99. limit(() => syncSubscription(subscription))
  100. )
  101. )
  102. }
  103. const loopForSubscriptions = async skipInitial => {
  104. let skip = skipInitial
  105. // iterate while there are more subscriptions to fetch
  106. while (true) {
  107. const subscriptions = await Subscription.find({
  108. recurlySubscription_id: { $exists: true, $ne: '' },
  109. })
  110. .sort('_id')
  111. .skip(skip)
  112. .limit(FETCH_LIMIT)
  113. .exec()
  114. if (subscriptions.length === 0) {
  115. console.warn('DONE')
  116. return
  117. }
  118. await syncSubscriptions(subscriptions)
  119. ScriptLogger.checkedSubscriptionsCount += subscriptions.length
  120. retryCounter = 0
  121. ScriptLogger.printProgress()
  122. ScriptLogger.printSummary()
  123. skip += FETCH_LIMIT
  124. }
  125. }
  126. let retryCounter = 0
  127. const run = async () => {
  128. await waitForDb()
  129. while (true) {
  130. try {
  131. await loopForSubscriptions(
  132. MONGO_SKIP + ScriptLogger.checkedSubscriptionsCount
  133. )
  134. break
  135. } catch (error) {
  136. if (retryCounter < 3) {
  137. console.error(error)
  138. retryCounter += 1
  139. console.warn(`RETRYING IN 60 SECONDS. (${retryCounter}/3)`)
  140. await setTimeout(60000)
  141. } else {
  142. console.error('Failed after 3 retries')
  143. throw error
  144. }
  145. }
  146. }
  147. }
  148. let FETCH_LIMIT, ASYNC_LIMIT, COMMIT, MONGO_SKIP
  149. const setup = () => {
  150. const argv = minimist(process.argv.slice(2))
  151. FETCH_LIMIT = argv.fetch ? argv.fetch : 100
  152. ASYNC_LIMIT = argv.async ? argv.async : 10
  153. MONGO_SKIP = argv.skip ? argv.skip : 0
  154. COMMIT = argv.commit !== undefined
  155. if (!COMMIT) {
  156. console.warn('Doing dry run without --commit')
  157. }
  158. if (MONGO_SKIP) {
  159. console.warn(`Skipping first ${MONGO_SKIP} records`)
  160. }
  161. }
  162. setup()
  163. run().then(() => {
  164. process.exit()
  165. })