resync_subscriptions.mjs 4.8 KB

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