如何编写延迟5分钟发送消息的Firebase Cloud Functions?
问题描述
我正在使用PubSub和Scheduler编写Firebase Cloud Functions,希望在timelimitScheduler函数启动后5分钟再发布消息。但目前timelimitScheduler会立即发送消息,导致cancelReservation函数在10秒内就执行,预约被提前取消。
当前代码
timelimitScheduler 函数
exports.timelimitScheduler = functions.firestore// eslint-disable-line .document('Reservation/{documentId}')// eslint-disable-line .onCreate((snapshot, context) => {// eslint-disable-line const newReservation = snapshot.data();// eslint-disable-line const userId = newReservation.userId;// eslint-disable-line const shopId = newReservation.shopId;// eslint-disable-line const topicName = 'reservation-cancellation';// eslint-disable-line const reservationId = context.params.documentId;// eslint-disable-line const data = JSON.stringify({ reservationId });// eslint-disable-line const pubSubClient = new PubSub();// eslint-disable-line const delaySeconds = 300;// eslint-disable-line const publishTime = new Date(Date.now() + delaySeconds * 1000);// eslint-disable-line console.log('Scheduled publish time:', publishTime);// eslint-disable-line const message = {// eslint-disable-line data: Buffer.from(data),// eslint-disable-line attributes: {// eslint-disable-line scheduledTime: publishTime.getTime().toString(),// eslint-disable-line },// eslint-disable-line };// eslint-disable-line return pubSubClient// eslint-disable-line .topic(topicName)// eslint-disable-line .publishMessage(message)// eslint-disable-line .then((messageId) => {// eslint-disable-line console.log(`Message ${messageId} published at ${publishTime}`);// eslint-disable-line return null;// eslint-disable-line })// eslint-disable-line .catch((error) => {// eslint-disable-line console.error(`Error publishing message: ${error}`);// eslint-disable-line throw error;// eslint-disable-line });// eslint-disable-line });// eslint-disable-line
cancelReservation 函数
exports.cancelReservation = functions.pubsub.topic('reservation-cancellation')// eslint-disable-line .onPublish((message) => {// eslint-disable-line const reservationId = message.json.reservationId;// eslint-disable-line const scheduledTime = parseInt(message.attributes.scheduledTime, 10);// eslint-disable-line const delaySeconds = Math.floor((Date.now() - scheduledTime) / 1000);// eslint-disable-line console.log(`Canceling reservation ${reservationId} (delayed by ${delaySeconds} seconds)`);// eslint-disable-line return admin.firestore().collection('Reservation').doc(reservationId).get()// eslint-disable-line .then((reservationDoc) => {// eslint-disable-line const reservation = reservationDoc.data();// eslint-disable-line if (reservation.state !== '예약접수') {// eslint-disable-line return;// eslint-disable-line }// eslint-disable-line return admin.firestore().collection('Reservation').doc(reservationId).update({// eslint-disable-line state: '예약취소',// eslint-disable-line });// eslint-disable-line })// eslint-disable-line .catch((error) => {// eslint-disable-line console.error(`Error canceling reservation: ${error}`);// eslint-disable-line throw error;// eslint-disable-line });// eslint-disable-line });// eslint-disable-line
问题原因
你的代码仅将计划发布时间存入消息属性,但直接调用了publishMessage方法——PubSub本身不支持延迟发布消息,因此消息会被立即发送,cancelReservation自然会马上触发。
解决方案
方案1:使用Cloud Scheduler创建定时PubSub任务
通过Cloud Scheduler API,在Firestore文档创建时生成一个5分钟后触发的定时任务,该任务会向指定PubSub主题发送消息,实现真正的延迟执行。
首先安装依赖:
npm install @google-cloud/scheduler
修改timelimitScheduler函数:
const { SchedulerClient } = require('@google-cloud/scheduler'); const schedulerClient = new SchedulerClient(); exports.timelimitScheduler = functions.firestore .document('Reservation/{documentId}') .onCreate(async (snapshot, context) => { const reservationId = context.params.documentId; const projectId = process.env.GCLOUD_PROJECT; const location = 'asia-northeast1'; // 替换为你的函数部署区域 const topicName = 'reservation-cancellation'; const jobName = schedulerClient.jobPath(projectId, location, `cancel-reservation-${reservationId}`); // 计算5分钟后的触发时间 const scheduleTime = new Date(Date.now() + 300 * 1000); // 转换为Cloud Scheduler支持的Cron格式 const cronSchedule = `${scheduleTime.getSeconds()} ${scheduleTime.getMinutes()} ${scheduleTime.getHours()} ${scheduleTime.getDate()} ${scheduleTime.getMonth() + 1} ? ${scheduleTime.getFullYear()}`; const job = { name: jobName, pubsubTarget: { topicName: `projects/${projectId}/topics/${topicName}`, data: Buffer.from(JSON.stringify({ reservationId })).toString('base64'), }, schedule: cronSchedule, timeZone: 'Asia/Seoul', // 替换为你的时区 }; try { await schedulerClient.createJob({ parent: schedulerClient.locationPath(projectId, location), job }); console.log(`定时任务已创建,将在${scheduleTime}触发取消预约`); return null; } catch (error) { console.error('创建定时任务失败:', error); throw error; } });
方案2:直接使用延迟执行逻辑(无需PubSub)
如果不需要PubSub中转,可以直接在timelimitScheduler中设置延迟执行取消逻辑,注意这种方式依赖函数实例存活时间,若实例提前被回收则任务失效:
exports.timelimitScheduler = functions.firestore .document('Reservation/{documentId}') .onCreate((snapshot, context) => { const reservationId = context.params.documentId; const delaySeconds = 300; setTimeout(async () => { try { const reservationDoc = await admin.firestore().collection('Reservation').doc(reservationId).get(); const reservation = reservationDoc.data(); if (reservation?.state === '예약접수') { await admin.firestore().collection('Reservation').doc(reservationId).update({ state: '예약취소' }); console.log(`预约${reservationId}已取消`); } } catch (error) { console.error('取消预约失败:', error); } }, delaySeconds * 1000); console.log(`已预约5分钟后取消预约${reservationId}`); return null; });
临时优化方案(不推荐)
如果要保留原有PubSub逻辑,可以在消费端过滤未到时间的消息,但这不是真正的延迟,只是避免提前执行:
exports.cancelReservation = functions.pubsub.topic('reservation-cancellation') .onPublish(async (message) => { const reservationId = message.json.reservationId; const scheduledTime = parseInt(message.attributes.scheduledTime, 10); const now = Date.now(); if (now < scheduledTime) { console.log(`未到计划时间,延迟至${new Date(scheduledTime)}执行`); return null; } // 原有的取消逻辑 const reservationDoc = await admin.firestore().collection('Reservation').doc(reservationId).get(); const reservation = reservationDoc.data(); if (reservation?.state === '예약접수') { await admin.firestore().collection('Reservation').doc(reservationId).update({ state: '예약취소' }); console.log(`预约${reservationId}已取消`); } return null; });
内容的提问来源于stack exchange,提问作者안은노
相关产品推荐
相关产品推荐

