resync_subscriptions.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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 'node: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. try {
  81. await SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  82. recurlySubscription,
  83. subscription,
  84. {}
  85. )
  86. } catch (error) {
  87. await handleSyncSubscriptionError(subscription, error)
  88. }
  89. }
  90. await setTimeout(80)
  91. }
  92. const syncSubscriptions = async subscriptions => {
  93. const limit = pLimit(ASYNC_LIMIT)
  94. return await Promise.all(
  95. subscriptions.map(subscription =>
  96. limit(() => syncSubscription(subscription))
  97. )
  98. )
  99. }
  100. const loopForSubscriptions = async skipInitial => {
  101. let skip = skipInitial
  102. // iterate while there are more subscriptions to fetch
  103. while (true) {
  104. const subscriptions = await Subscription.find({
  105. recurlySubscription_id: { $exists: true, $ne: '' },
  106. })
  107. .sort('_id')
  108. .skip(skip)
  109. .limit(FETCH_LIMIT)
  110. .exec()
  111. if (subscriptions.length === 0) {
  112. console.warn('DONE')
  113. return
  114. }
  115. await syncSubscriptions(subscriptions)
  116. ScriptLogger.checkedSubscriptionsCount += subscriptions.length
  117. retryCounter = 0
  118. ScriptLogger.printProgress()
  119. ScriptLogger.printSummary()
  120. skip += FETCH_LIMIT
  121. }
  122. }
  123. let retryCounter = 0
  124. const run = async () => {
  125. while (true) {
  126. try {
  127. await loopForSubscriptions(
  128. MONGO_SKIP + ScriptLogger.checkedSubscriptionsCount
  129. )
  130. break
  131. } catch (error) {
  132. if (retryCounter < 3) {
  133. console.error(error)
  134. retryCounter += 1
  135. console.warn(`RETRYING IN 60 SECONDS. (${retryCounter}/3)`)
  136. await setTimeout(60000)
  137. } else {
  138. console.error('Failed after 3 retries')
  139. throw error
  140. }
  141. }
  142. }
  143. }
  144. let FETCH_LIMIT, ASYNC_LIMIT, COMMIT, MONGO_SKIP
  145. const setup = () => {
  146. const argv = minimist(process.argv.slice(2))
  147. FETCH_LIMIT = argv.fetch ? argv.fetch : 100
  148. ASYNC_LIMIT = argv.async ? argv.async : 10
  149. MONGO_SKIP = argv.skip ? argv.skip : 0
  150. COMMIT = argv.commit !== undefined
  151. if (!COMMIT) {
  152. console.warn('Doing dry run without --commit')
  153. }
  154. if (MONGO_SKIP) {
  155. console.warn(`Skipping first ${MONGO_SKIP} records`)
  156. }
  157. }
  158. if (process.env.NODE_ENV !== 'development') {
  159. console.warn(
  160. 'This script can cause issues with manually amended subscriptions and can also exhaust our rate-limit with Recurly so is not intended to be run in production. Please use it in development environments only.'
  161. )
  162. process.exit(1)
  163. }
  164. setup()
  165. await run()
  166. process.exit()