NodeJS Lambda + API网关超时

1tuwyuhd  于 2023-06-22  发布在  Node.js
关注(0)|答案(1)|浏览(166)

我有以下lambda:(NodeJS18)。目的是使用puppeteer打开浏览器并生成报告,将其保存为PDF,上传到S3,获得预签名的URL作为响应。它在某种程度上工作正常,因为如果我使用console.log(presignedUrl),我可以看到实际的url,唯一的问题是我的lambda超时。
这似乎不是一个资源问题(我认为),因为从1024 MB的RAM分配它似乎只使用~ 700 MB。而且,在2-3次超时后,每一次连续的调用都是可以的,并且它可以工作。如果我再等几分钟再试一次,我会得到几次超时,然后它又开始工作。
我在一些帖子中读到,这可能是由于lambda试图重新启动,或者我没有正确关闭一些连接,但到目前为止似乎没有任何工作。
高度赞赏任何答案!

const PDFDocument = require("pdf-lib").PDFDocument;
const AWS = require("aws-sdk");
const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3");
const chromium = require("@sparticuz/chromium");
const puppeteer = require("puppeteer-core");
const FRONTEND_URL = process.env.FRONTEND_URL;
const BUCKET_NAME = process.env.SLEEP_STUDIES_BUCKET;

const lambdaError = (error) => {
  return {
    errorType: "InternalServerError",
    httpStatus: error.statusCode,
    code: error.statusCode,
    statusCode: error.statusCode,
    message: error.message,
    error,
  };
};

async function processReport(studyId, scoreSetId, sectionsList, onSpot = false) {
  const sections = [
    "main",
    ...sectionsList.split(","),
  ];
  console.info("open browser");
  const browser = await puppeteer.launch({
    executablePath: await chromium.executablePath(),
    headless: "new",
    ignoreHTTPSErrors: true,
    defaultViewport: chromium.defaultViewport,
    args: [...chromium.args, "--hide-scrollbars", "--disable-web-security"],
  });
  try {
    const isSecondPageVisible = [
      "oxygenSaturationTRT",
      "arousalTRT",
      "snoreInTst",
      "cardiacTST",
      "movementEvents",
      "positionStatistics",
    ].some(key => sections.includes(key));
    let trendsPageNumber;
    if (isSecondPageVisible && sections.includes("note")) {
      trendsPageNumber = 4;
    } else if (!isSecondPageVisible && !sections.includes("note")) {
      trendsPageNumber = 2;
    } else {
      trendsPageNumber = 3;
    }

    let totalPages = 1;
    if (isSecondPageVisible) {
      totalPages++;
    }

    if (sections.includes("note")) {
      totalPages++;
    }

    if (sections.includes("trends")) {
      totalPages++;
    }

    let range = `1-${totalPages}`;
    if (sections.includes("trends")) {
      range = `1-${totalPages - 1}`;
    }

    console.info("generate report");
    let report = await generatePdf(
      browser,
      studyId,
      scoreSetId,
      sections.filter(section => section !== "trends"),
      range,
      onSpot,
      false,
      totalPages,
    );
    if (sections.includes("trends")) {
      const landscapePage = await generatePdf(
        browser,
        studyId,
        scoreSetId,
        "trends",
        "1",
        onSpot,
        true,
        totalPages,
        trendsPageNumber,
      );
      console.info("merge reports");
      report = await mergePdfs(report, landscapePage);
    }
    console.info("save report");
    const fileUrl = await savePdf(studyId, scoreSetId, report, onSpot);
    console.info("close browser");
    browser.close();
    console.info("browser closed, returning response for filename", fileUrl);
    return {
      message: "Report generated and uploaded to S3!",
      fileUrl,
    };
  } catch (e) {
    console.info(e);
    browser.close();
    console.info("browser closed, returning error");
    e.statusCode = e.statusCode ?? 500;
    throw e;
  }
}

async function generatePdf(
  browser,
  studyId,
  scoreSetId,
  sections,
  range,
  watermark,
  landscape,
  totalPages = 4,
  trendsPageNumber = 4,
) {
  try {
    console.info("open new page for ", sections.toString());
    const page = await browser.newPage();
    let url = `${FRONTEND_URL}/report/${studyId}/${scoreSetId}/print?sections=${sections}&totalPages=${totalPages}&trendsPageNumber=${trendsPageNumber}`;
    if (watermark) {
      url += `&watermark=${watermark}`;
    }
    console.info("navigate to page", url);
    await page.goto(url, { waitUntil: "networkidle0" });
    await page.addStyleTag({
      content: `
      @page {
        size: A4 ${landscape ? "landscape" : "portrait"} !important;
        margin: 0 !important;
      }
      .navbar {
        display: none;
      }
      `,
    });
    await page.emulateMediaType("screen");
    console.info("generate report for pages ", range);
    return await page.pdf({
      margin: "none",
      footerTemplate: "",
      printBackground: true,
      format: "A4",
      landscape,
      pageRanges: range,
    });
  } catch (e) {
    console.error(e);
    throw e;
  }
}

async function mergePdfs(portraitPages, landscapePage) {
  try {
    const pdfsToMerge = [portraitPages, landscapePage];
    const mergedPdf = await PDFDocument.create();
    for (const pdfBytes of pdfsToMerge) {
      const pdf = await PDFDocument.load(pdfBytes);
      const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
      copiedPages.forEach((page) => {
        mergedPdf.addPage(page);
      });
    }

    return await mergedPdf.save();
  } catch (e) {
    console.info(e);
    throw e;
  }
}

async function savePdf(studyId, scoreSetId, buffer, onSpot = false) {
  const path = `${studyId}/reports/${scoreSetId}/${onSpot ? "draft-" : ""}report-${new Date().getTime() / 1000}.pdf`;
  const client = new S3Client({});
  const command = new PutObjectCommand({
    Bucket: BUCKET_NAME,
    Key: path,
    Body: buffer,
  });
  await client.send(command);
  console.log("file uploaded on s3", path);

  if (!onSpot) {
    return path;
  }

  const s3 = new AWS.S3();
  const params = {
    Bucket: BUCKET_NAME,
    Key: path,
  };
  return await s3.getSignedUrlPromise("getObject", params);
}

exports.handler = function (event, context, callback) {
  if (!FRONTEND_URL) {
    const error = new Error();
    error.message = "Frontend URL not found!";
    error.statusCode = 500;
    throw error;
  }

  if (!BUCKET_NAME) {
    const error = new Error();
    error.message = "Required S3 Environment Variables aren't found!";
    error.statusCode = 500;
    throw error;
  }

  // @Todo: extract authorization token and pass it as query param in FE
  const { sections, onSpot, sleep_study_id, score_set_id } = event;
  context.callbackWaitsForEmptyEventLoop = false;
  processReport(sleep_study_id, score_set_id, sections ?? "", onSpot)
    .then((response) => {
      callback(null, response);
    })
    .catch((e) => {
      console.log(e);
      e.statusCode = e.statusCode ?? 500;
      callback(lambdaError(e));
    });
};
x8diyxa7

x8diyxa71#

对于任何可能进来问自己同样问题的人:
在将lambda与API Gateway链接的场景中,lambda的超时时间并不重要,重要的是API Gateway的29s(最大)超时时间。因此,您的lambda(包括冷启动)应该在最长29秒内向API Gateway返回响应,否则API Gateway将简单地发送超时响应。
我们解决这个问题的方法是创建另一个lambda,异步调用这个lambda,并在所需的时间内向API网关返回一个答案。

相关问题