每日12点GMT触发的Pub/Sub云函数无法正常发送邮件
问题描述
部署了一个Pub/Sub Cloud Function,设定每日GMT时间0点自动执行,逻辑为:从Firestore读取访客列表,转换为Excel文件后,通过SendGrid API发送给Firestore中存储的收件人邮箱。
出现异常:
- 当设置为10/30分钟短间隔触发,或手动触发时,函数正常运行,收件人能收到邮件
- 每日0点定时触发时,云函数日志显示执行状态为OK,但收件人收不到邮件
云函数与Firebase区域为us-central-1,时区配置为Europe/London(GMT),相关代码如下:
const { initializeApp, cert } = require('firebase-admin/app'); const { getFirestore, doc, deleteDoc, delete:deleteDocument } = require('firebase-admin/firestore'); const functions = require('firebase-functions'); const serviceAccount = require('./serviceAcc.json'); const sgMail = require("@sendgrid/mail"); const excelJS = require('exceljs'); initializeApp({ credential: cert(serviceAccount) }); const db = getFirestore(); let receiver = ""; let newVisitors = []; let todaysDate; async function deleteCollection(db, collectionPath, batchSize) { const collectionRef = db.collection(collectionPath); const query = collectionRef.orderBy('__name__').limit(batchSize); return new Promise((resolve, reject) => { deleteQueryBatch(db, query, resolve).catch(reject); }); } async function deleteQueryBatch(db, query, resolve) { const snapshot = await query.get(); const batchSize = snapshot.size; if (batchSize === 0) { // When there are no documents left, we are done if (newVisitors.length !== 0) { let workbook = new excelJS.Workbook(); const worksheet = workbook.addWorksheet('Sheet1'); worksheet.columns = [ { header: "Name", key: "name", width:"30"}, { header: "Type", key: "type", width:"20"}, { header: "EnteringTime", key: "enteringTime", width:"40"}, { header: "LeavingTime", key: "leavingTime", width:"40"}, ] newVisitors.map((value,id) => { worksheet.addRow({ name: value.name, type: value.type, enteringTime: value.enteringTime, leavingTime: value.leavingTime, }); }) workbook.xlsx.writeBuffer().then((buffer) => { const msg = { to: receiver, from: "venuemanager@acostadancecentre.com", subject: "VISITORS LIST FOR TODAY", text: `PFA visitor list for ${todaysDate}`, html: `<h1>PFA visitor list for ${todaysDate}</h1>`, attachments: [ { content: buffer.toString('base64'), filename: 'visitors.xlsx', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment', }, ], }; sgMail.send(msg) .then((response) => { console.log("Email sent successfully"); newVisitors = []; }) .catch((error) => { console.log(`Error while sending email: ${error.message}`) newVisitors = []; }); }) .catch((error) => console.log(`Error while making buffer of excel: ${error.message}`)) } resolve(); return; } // Delete documents in a batch const batch = db.batch(); snapshot.docs.forEach((doc) => { const abc = doc.data(); todaysDate = abc.enteringTime.toDate().toDateString(); const enteringTime = abc.enteringTime.toDate().toDateString() + " at " + abc.enteringTime.toDate(). toLocaleTimeString(); let leavingTime; if (abc.isLeft) { leavingTime = abc.leavingTime.toDate().toDateString() + " at " + abc.leavingTime.toDate().toLocaleTimeString(); } else { leavingTime = "NIL"; // or set it to a default value } const newVisitor = { name: abc.name, type: abc.type, enteringTime, // isLeft: abc.isLeft, leavingTime, }; newVisitors.push(newVisitor); batch.delete(doc.ref); }); await batch.commit(); // Recurse on the next process tick, to avoid // exploding the stack. process.nextTick(() => { deleteQueryBatch(db, query, resolve); }); } exports.emailTask = functions.pubsub.schedule("0 0 * * *").timeZone("Europe/London").onRun(async (context) => { sgMail.setApiKey('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'); // Replace with your SendGrid API key db.collection('RecipientEmail').get().then((snapshot) => { snapshot.forEach((doc) => { receiver = doc.data(); console.log(receiver); }); deleteCollection(db, "visitors", 10).then((res) => console.log(`successs`)) .catch((error) => console.log(`faillllll: ${error.message()}`)); }) .catch((error) => console.log(`Error while fetching receipientEmail: ${error.message}`)); });
问题根源
- 异步操作未被正确等待:
emailTask是async函数,但内部的Firestore查询、deleteCollection调用都用.then()链式处理,没有返回Promise也没有用await,导致Cloud Function在异步逻辑(比如Excel生成、邮件发送)完成前就提前终止。短间隔/手动触发时执行时间短,逻辑可能刚好跑完;但每日触发时数据量可能更大,异步操作还没完成就被函数生命周期中断。 - 全局变量引发的状态混乱:
receiver、newVisitors、todaysDate都是全局变量,Cloud Function的实例复用机制可能导致上一次执行的残留值干扰当前执行,比如收件人信息错误、访客列表数据混乱。 - 邮件发送逻辑未纳入Promise链:
deleteQueryBatch中生成Excel和发送邮件的.then()没有被包含在函数的Promise链里,这部分异步操作不被Cloud Function的生命周期追踪,函数可能在邮件发送前就结束。 - 收件人赋值错误:
receiver = doc.data()将整个文档对象赋值给receiver,但SendGrid的to字段需要的是邮箱字符串,应该读取文档中对应的邮箱字段(比如doc.data().email,需根据实际字段名调整)。
修复方案
将所有异步操作改为await语法,移除全局变量,确保所有异步逻辑被函数生命周期追踪,修正收件人赋值:
const { initializeApp, cert } = require('firebase-admin/app'); const { getFirestore } = require('firebase-admin/firestore'); const functions = require('firebase-functions'); const serviceAccount = require('./serviceAcc.json'); const sgMail = require("@sendgrid/mail"); const excelJS = require('exceljs'); initializeApp({ credential: cert(serviceAccount) }); const db = getFirestore(); // 初始化SendGrid API Key,无需在每次触发时重复设置 sgMail.setApiKey('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'); async function deleteCollectionAndCollectVisitors(db, collectionPath, batchSize) { const visitors = []; let todaysDate = ''; const deleteQueryBatch = async (query) => { const snapshot = await query.get(); const batchSize = snapshot.size; if (batchSize === 0) { return { visitors, todaysDate }; } const batch = db.batch(); snapshot.docs.forEach(doc => { const data = doc.data(); // 取第一个访客的日期作为当日日期 if (!todaysDate) { todaysDate = data.enteringTime.toDate().toDateString(); } const enteringTime = `${data.enteringTime.toDate().toDateString()} at ${data.enteringTime.toDate().toLocaleTimeString()}`; const leavingTime = data.isLeft ? `${data.leavingTime.toDate().toDateString()} at ${data.leavingTime.toDate().toLocaleTimeString()}` : "NIL"; visitors.push({ name: data.name, type: data.type, enteringTime, leavingTime }); batch.delete(doc.ref); }); await batch.commit(); // 递归时等待下一批处理完成 await deleteQueryBatch(query); }; const collectionRef = db.collection(collectionPath); const query = collectionRef.orderBy('__name__').limit(batchSize); await deleteQueryBatch(query); return { visitors, todaysDate }; } exports.emailTask = functions.pubsub.schedule("0 0 * * *").timeZone("Europe/London").onRun(async (context) => { try { // 获取收件人邮箱,这里假设文档中有email字段,根据实际调整 const recipientSnapshot = await db.collection('RecipientEmail').get(); let receiver = ''; recipientSnapshot.forEach(doc => { // 读取邮箱字段,替换成实际字段名 receiver = doc.data().email; console.log('收件人邮箱:', receiver); }); if (!receiver) { console.log('未找到收件人邮箱'); return; } // 删除访客集合并收集数据 const { visitors, todaysDate } = await deleteCollectionAndCollectVisitors(db, "visitors", 10); if (visitors.length === 0) { console.log('今日无访客数据,无需发送邮件'); return; } // 生成Excel const workbook = new excelJS.Workbook(); const worksheet = workbook.addWorksheet('Sheet1'); worksheet.columns = [ { header: "Name", key: "name", width: 30 }, { header: "Type", key: "type", width: 20 }, { header: "EnteringTime", key: "enteringTime", width: 40 }, { header: "LeavingTime", key: "leavingTime", width: 40 }, ]; visitors.forEach(visitor => { worksheet.addRow(visitor); }); const buffer = await workbook.xlsx.writeBuffer(); // 发送邮件 const msg = { to: receiver, from: "venuemanager@acostadancecentre.com", subject: "VISITORS LIST FOR TODAY", text: `PFA visitor list for ${todaysDate}`, html: `<h1>PFA visitor list for ${todaysDate}</h1>`, attachments: [ { content: buffer.toString('base64'), filename: 'visitors.xlsx', type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', disposition: 'attachment', }, ], }; await sgMail.send(msg); console.log("邮件发送成功"); } catch (error) { console.error('执行出错:', error.message); throw error; // 抛出错误让Cloud Function标记为失败,便于排查 } });
关键优化点
- 移除所有全局变量,改用函数内部变量,避免实例复用导致的状态污染
- 所有异步操作统一使用
await,确保函数等待所有逻辑完成后再终止 - 将访客数据收集、删除集合的逻辑封装为独立函数,返回明确的数据结构
- 把邮件发送逻辑纳入主Promise链,确保被函数生命周期追踪
- 添加完整的错误捕获,抛出错误让Cloud Function标记执行状态为失败,便于日志排查
- 提前初始化SendGrid API Key,无需每次触发重复设置
内容的提问来源于stack exchange,提问作者Rayan Siddique
相关产品推荐
相关产品推荐

