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