异步函数不在一个 AWS Lambda 函数中执行(等待),而是在另一个

时间:2021-03-02 08:00:26

标签: typescript express aws-lambda serverless-framework nodemailer

考虑以下MailService

export class MailService {
    private transporter?: nodemailer.Transporter;

    async sendMail(opts: {
        from: string,
        to: string,
        subject: string,
        template?: string,
        context?: any,
        bcc?: string,
        html?: string
    }) {
        console.log(`About to send an email to ${opts.to}...`);

        this.getTransporter();
        console.log("Generating email content...");
        let htmlToSend = "Default message."
        if (opts.template) {
            const htmlData = await fs.readFile(path.resolve(__dirname, process.env.EMAIL_TEMPLATES_DIR + opts.template), { encoding: 'utf-8' });
            const htmlBuffer = Buffer.from(htmlData);
            const template = handlebars.compile(htmlBuffer.toString());
            htmlToSend = template(opts.context);
        }
        if (opts.html) {
            htmlToSend = opts.html
        }

        const mailOptions = {
            from: opts.from,
            to: opts.to,
            subject: opts.subject,
            html: htmlToSend
        };
        console.log(htmlToSend);
        console.log("Sending...");   

        // Here, lambda just terminates silently
        const sentMessageInfo = await this.transporter.sendMail(mailOptions);
        // This log is never displayed:
        console.log("Done.", sentMessageInfo);
        return sentMessageInfo;
    }

    /* Creates a mail transporter if needed */
    private getTransporter() {...}
}

然后在我的 express 端点中,我使用这样的服务:

app.post("/subscriptions/new", async (req, res) => {
    // Check if this Customer already exists before creating it
    let customer = await stripeService.getExistingCustomer(req.body.email);

    const returningCustomer = customer !== null;

    if (returningCustomer) {
        console.log("Customer(s) with same email address already exist(s).", customer);
    }
    else {
        customer = await stripeService.createCustomer({
            name: req.body.name,
            email: req.body.email,
            phone: req.body.phone,
            source: req.body.stripeSource,
            preferred_locales: ['fr-FR'],
            metadata: {
                ...
            }
        });
        console.log("New Customer created", customer);
    }

    // Serve a success page
    const filePath = path.resolve(process.env.STATIC_DIR + "/success.html");
    res.sendFile(filePath);

    // Send notifications to My Company and the Customer using nodemailer
    const mailService = new MailService();

    if (customer && returningCustomer) {
        console.warn("This is a returning customer. Sending the returning customer email.", returningCustomer);
        await mailService.sendMail({
            from: "'The Guy' <no-reply@mydomain.com>",
            to: customer.email as string,
            bcc: "order@mydomain.com",
            subject: "Merci pour votre confiance",
            template: "returning-customer.handlebars",
            context: {
                customer,
                subscription: req.body,
                orderItems: req.body.orderItems.split('\n'),
                futureDeliveries: Scheduler.getFutureDeliveries({
                    frequencyStr: req.body.frequency,
                    targetWeekDay: Scheduler.getWeekDayFromDropLocation(req.body.dropLocation)
                })
            }
        });
        console.log("Returning customer email sent.");
    }
    else {
        if (customer) {
            await mailService.sendMail({
                from: "'The Guy' <no-reply@mydomain.com>",
                to: customer.email as string,
                bcc: "order@mydomain.com",
                subject: "Merci pour votre confiance !",
                template: "new-customer.handlebars",
                context: {
                    customer,
                    subscription: req.body,
                    orderItems: req.body.orderItems.split('\n'),
                    futureDeliveries: Scheduler.getFutureDeliveries({
                        frequencyStr: req.body.frequency,
                        targetWeekDay: Scheduler.getWeekDayFromDropLocation(req.body.dropLocation)
                    })
                }
            });
            console.log("The confirmation email has been sent to the new customer", customer.email);
        }
    }
})

本地一切正常。

这是我在 serverless.yml 中的函数定义:

functions:
  my-api:
    handler: handler.handler
    events:
      - http:
          path: /
          method: GET
          cors: true
      - http:
          path: /{proxy+}
          method: ANY
          cors: true
  cronjobs:
    handler: handler.sendDeliveryReminders
    events:
      # every day at 03:00 PM
      - schedule: cron(0 15 * * ? *)

最后是我的处理程序:

const serverlessExpress = require('@vendia/serverless-express');
const app = require('./src/server.js');

// This doesn't await for every async call to complete before terminating the lambda invocation
exports.handler = serverlessExpress({
  app: app.app
});

// This is awaiting everything as expected
exports.sendDeliveryReminders = require('./src/cronjobs.js').sendDeliveryReminders;

发送电子邮件在 API 端点 (handler.handler) 和 cronjobs (handler.sendDeliveryReminders) 中完成。 但是,只有后者有效。

当尝试从 API 端点发送电子邮件(使用与 cronjobs 中工作的参数完全相同的参数)时,lambda 函数在 sendMail 调用处停止(编辑:甚至之前的一些指令!它可能会在遇到 await fs.readFile 中的 MailService.sendMail 时立即失败:

// Here, lambda just terminates silently
const sentMessageInfo = await this.transporter.sendMail(mailOptions);
// This log is never displayed:
console.log("Done.", sentMessageInfo);

就好像 await this.transporter.sendMail(mailOptions) 从未真正被等待过,即使它是在 async 函数中调用的,并且应该在内部等待一些东西(我认为 Nodemailer 的实现是正确的).

这是来自 CloudWatch 的日志:

2021-03-02T11:08:10.409+01:00   START RequestId: 0c455857-ab09-4547-b861-038e91f666b7 Version: $LATEST

2021-03-02T11:08:10.724+01:00   2021-03-02T10:08:10.724Z 0c455857-ab09-4547-b861-038e91f666b7 WARN This is a returning customer. Sending the returning customer email. true

2021-03-02T11:08:10.727+01:00   2021-03-02T10:08:10.726Z 0c455857-ab09-4547-b861-038e91f666b7 INFO About to send an email to me@mydomain.com...

2021-03-02T11:08:10.727+01:00   2021-03-02T10:08:10.727Z 0c455857-ab09-4547-b861-038e91f666b7 INFO Creating the mail transporter...

2021-03-02T11:08:10.728+01:00   2021-03-02T10:08:10.728Z 0c455857-ab09-4547-b861-038e91f666b7 INFO Generating email content...

2021-03-02T11:08:10.735+01:00   2021-03-02T10:08:10.734Z 0c455857-ab09-4547-b861-038e91f666b7 INFO SERVERLESS_ENTERPRISE {"c":true,"b":"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","origin":"sls-agent"}

2021-03-02T11:08:10.735+01:00   END RequestId: 0c455857-ab09-4547-b861-038e91f666b7

2021-03-02T11:08:10.735+01:00   REPORT RequestId: 0c455857-ab09-4547-b861-038e91f666b7 Duration: 321.58 ms Billed Duration: 322 ms Memory Size: 1024 MB Max Memory Used: 120 MB

2021-03-02T11:08:10.965+01:00   START RequestId: 9d5a4391-6568-4d80-976b-d88a5f946d41 Version: $LATEST

2021-03-02T11:08:10.973+01:00   2021-03-02T10:08:10.973Z 9d5a4391-6568-4d80-976b-d88a5f946d41 INFO SERVERLESS_ENTERPRISE {"c":true,"b":"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","origin":"sls-agent"}

2021-03-02T11:08:10.974+01:00   END RequestId: 9d5a4391-6568-4d80-976b-d88a5f946d41

2021-03-02T11:08:10.974+01:00   REPORT RequestId: 9d5a4391-6568-4d80-976b-d88a5f946d41 Duration: 5.36 ms Billed Duration: 6 ms Memory Size: 1024 MB Max Memory Used: 120 MB

2021-03-02T11:09:29.926+01:00   START RequestId: 6c5cadaf-b14f-4669-834d-46fbeabdc561 Version: $LATEST

2021-03-02T11:09:29.970+01:00   2021-03-02T10:09:29.970Z 6c5cadaf-b14f-4669-834d-46fbeabdc561 INFO <body> <h1>Welcome back John Doe</h1> </body>

2021-03-02T11:09:29.970+01:00   2021-03-02T10:09:29.970Z 6c5cadaf-b14f-4669-834d-46fbeabdc561 INFO Sending...

2021-03-02T11:09:30.012+01:00   2021-03-02T10:09:30.012Z 6c5cadaf-b14f-4669-834d-46fbeabdc561 INFO SERVERLESS_ENTERPRISE {"c":true,"b":"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","origin":"sls-agent"}

2021-03-02T11:09:30.015+01:00   END RequestId: 6c5cadaf-b14f-4669-834d-46fbeabdc561

2021-03-02T11:09:30.015+01:00   REPORT RequestId: 6c5cadaf-b14f-4669-834d-46fbeabdc561 Duration: 86.16 ms Billed Duration: 87 ms Memory Size: 1024 MB Max Memory Used: 120 MB

知道为什么这会在某些 lambda 函数中起作用而不是在其他函数中起作用吗? 另一个谜(至少对我而言)是为什么处理程序的执行跨越多个 Lambda 调用(参见日志)...

仅供参考,我正在使用 "nodemailer": "^6.4.18""@types/nodemailer": "^6.4.0"

2 个答案:

答案 0 :(得分:1)

我认为正在发生的事情是:

  1. 您的处理程序到达 res.sendFile(filePath)(这是异步的,并在成功时发送响应)。
  2. 节点开始发送文件
  3. 由于以上是异步的,node 还会继续运行您的其他指令。
  4. 最终发送了文件,因此响应消失了。这是 lambda 关闭的地方。它可能发生在 res.sendFile() 之后的任何时间点,从立即到发送电子邮件之后。

直接的解决方案是将 res.sendFile 移到处理程序的底部。

如果您想尽快提供下一页并且您无法等到电子邮件发送完毕,您可以将一个事件推送到一个队列(例如,到 SQS),然后触发一个 lambda从 SQS 发送电子邮件。不过,这会让您进入分布式领域,因此并不总是那么简单。

答案 1 :(得分:0)

您的 sendMail 没有返回 Promise,那么 await 关键字将不会按您的预期工作。该函数将在 sendMail 进程完成之前完成。

在本地方面,我猜您使用 serverless-offline 插件来测试您的功能。然后,该函数不会“完成”,它只是响应 -> Lambda 环境的不同行为。

如果 this.transporter 是 Nodemailler 的 Transporter 的一个实例,那么 .sendMail 函数是一个回调函数。你必须把它转换成一个返回 Promise 的新函数,或者像这样把它包装成一个新的 Promise:

const sentMessageInfo = await new Promise((resolve, reject) => {
  this.transporter.sendMail(mailOptions, (error, info) => {
    if (error) {
      return reject(error);
    }
    return resolve(info);
  })
});