Просмотр исходного кода

Merge pull request #9319 from overleaf/mj-deferred-recurly-email

[web] Use bull queues for deferred cancellation email

GitOrigin-RevId: a104f9940badcffc15f1f237a1cefd5dd912f4e0
Mathias Jakobsen 4 лет назад
Родитель
Сommit
f5b2cdc3cb

+ 10 - 0
services/web/app/src/Features/Email/EmailHandler.js

@@ -2,11 +2,13 @@ const { callbackify } = require('util')
 const Settings = require('@overleaf/settings')
 const EmailBuilder = require('./EmailBuilder')
 const EmailSender = require('./EmailSender')
+const Queues = require('../../infrastructure/Queues')
 
 const EMAIL_SETTINGS = Settings.email || {}
 
 module.exports = {
   sendEmail: callbackify(sendEmail),
+  sendDeferredEmail,
   promises: {
     sendEmail,
   },
@@ -22,3 +24,11 @@ async function sendEmail(emailType, opts) {
   opts.subject = email.subject
   await EmailSender.promises.sendEmail(opts)
 }
+
+function sendDeferredEmail(emailType, opts, delay) {
+  Queues.createScheduledJob(
+    'deferred-emails',
+    { data: { emailType, opts } },
+    delay
+  )
+}

+ 3 - 14
services/web/app/src/Features/Subscription/SubscriptionHandler.js

@@ -199,20 +199,9 @@ function cancelSubscription(user, callback) {
               first_name: user.first_name,
             }
             const ONE_HOUR_IN_MS = 1000 * 60 * 60
-            setTimeout(
-              () =>
-                EmailHandler.sendEmail(
-                  'canceledSubscription',
-                  emailOpts,
-                  err => {
-                    if (err) {
-                      logger.warn(
-                        { err },
-                        'failed to send confirmation email for subscription cancellation'
-                      )
-                    }
-                  }
-                ),
+            EmailHandler.sendDeferredEmail(
+              'canceledSubscription',
+              emailOpts,
               ONE_HOUR_IN_MS
             )
             callback()

+ 16 - 0
services/web/app/src/infrastructure/QueueWorkers.js

@@ -7,6 +7,9 @@ const {
   addOptionalCleanupHandlerBeforeStoppingTraffic,
   addRequiredCleanupHandlerBeforeDrainingConnections,
 } = require('./GracefulShutdown')
+const EmailHandler = require('../Features/Email/EmailHandler')
+const logger = require('@overleaf/logger')
+const OError = require('@overleaf/o-error')
 
 function start() {
   if (!Features.hasFeature('saas')) {
@@ -47,6 +50,19 @@ function start() {
     await FeaturesUpdater.promises.refreshFeatures(userId, reason)
   })
   registerCleanup(refreshFeaturesQueue)
+
+  const deferredEmailsQueue = Queues.getQueue('deferred-emails')
+  deferredEmailsQueue.process(async job => {
+    const { emailType, opts } = job.data
+    try {
+      await EmailHandler.promises.sendEmail(emailType, opts)
+    } catch (e) {
+      const error = OError.tag(e, 'failed to send deferred email')
+      logger.warn(error)
+      throw error
+    }
+  })
+  registerCleanup(deferredEmailsQueue)
 }
 
 function registerCleanup(queue) {

+ 27 - 0
services/web/test/unit/src/Email/EmailHandlerTests.js

@@ -20,11 +20,15 @@ describe('EmailHandler', function () {
         sendEmail: sinon.stub().resolves(),
       },
     }
+    this.Queues = {
+      createScheduledJob: sinon.stub(),
+    }
     this.EmailHandler = SandboxedModule.require(MODULE_PATH, {
       requires: {
         './EmailBuilder': this.EmailBuilder,
         './EmailSender': this.EmailSender,
         '@overleaf/settings': this.Settings,
+        '../../infrastructure/Queues': this.Queues,
       },
     })
   })
@@ -92,4 +96,27 @@ describe('EmailHandler', function () {
       })
     })
   })
+
+  describe('send deferred email', function () {
+    beforeEach(function () {
+      this.opts = {
+        to: 'bob@bob.com',
+        first_name: 'hello bob',
+      }
+      this.emailType = 'canceledSubscription'
+      this.ONE_HOUR_IN_MS = 1000 * 60 * 60
+      this.EmailHandler.sendDeferredEmail(
+        this.emailType,
+        this.opts,
+        this.ONE_HOUR_IN_MS
+      )
+    })
+    it('should add a email job to the queue', function () {
+      expect(this.Queues.createScheduledJob).to.have.been.calledWith(
+        'deferred-emails',
+        { data: { emailType: this.emailType, opts: this.opts } },
+        this.ONE_HOUR_IN_MS
+      )
+    })
+  })
 })

+ 13 - 1
services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js

@@ -110,7 +110,10 @@ describe('SubscriptionHandler', function () {
 
     this.LimitationsManager = { userHasV2Subscription: sinon.stub() }
 
-    this.EmailHandler = { sendEmail: sinon.stub() }
+    this.EmailHandler = {
+      sendEmail: sinon.stub(),
+      sendDeferredEmail: sinon.stub(),
+    }
 
     this.AnalyticsManager = { recordEventForUser: sinon.stub() }
 
@@ -405,6 +408,15 @@ describe('SubscriptionHandler', function () {
           .calledWith(this.subscription.recurlySubscription_id)
           .should.equal(true)
       })
+
+      it('should send the email after 1 hour', function () {
+        const ONE_HOUR_IN_MS = 1000 * 60 * 60
+        expect(this.EmailHandler.sendDeferredEmail).to.have.been.calledWith(
+          'canceledSubscription',
+          { to: this.user.email, first_name: this.user.first_name },
+          ONE_HOUR_IN_MS
+        )
+      })
     })
   })