异步处理用户结合Firestore批量更新时触发WriteBatch已提交错误的解决方案咨询
大家好,我最近在优化Cloud Functions里的通知推送逻辑时遇到了一个棘手的问题,想请社区的大佬们帮忙分析下。
先说说背景:我有个React Native项目,后端用Firestore存用户和活动数据,用Cloud Functions给用户推送个性化活动通知。最开始的代码是同步循环处理每个用户,但用户和活动量上来后,就算把超时设到540秒、内存开512MB还是会超时。所以我改成了异步处理用户,同时打算用Firestore的WriteBatch批量更新用户的通知记录,还有FCM的sendEach批量发通知,这样能提升效率避免超时。
但修改后的代码在Firebase模拟器里跑的时候,触发了这个错误:
⚠ functions: Error: Cannot modify a WriteBatch that has been committed.
错误出现在第二次调用batch.commit()的时候。我先把相关代码贴出来,麻烦大家帮我看看问题出在哪,怎么改才能正确实现异步处理+批量更新的逻辑。
原同步处理代码(能运行但会超时)
async function sendNotifications() { console.log("Sending notifications for recommended events..."); // Fetch all events once const eventsRef = admin.firestore().collection("Events"); const eventsSnapshot = await eventsRef .where('Start', '>=', new Date()) .get(); if (eventsSnapshot.empty) { console.log("No upcoming events found."); return; } const allEvents = eventsSnapshot.docs.map(doc => ({ ...doc.data(), docId: doc.id })); // Fetch all users const usersRef = admin.firestore().collection("Users"); const usersSnapshot = await usersRef.get(); let reset = false; for (const userDoc of usersSnapshot.docs) { try { const userData = userDoc.data(); const { fcmToken, preferences, language = "en", sentNotifications = [] } = userData; if (!fcmToken) continue; // Skip users without FCM token const userPreferredTags = preferences ? preferences : []; let eventToSend = findEventForUser(allEvents, userPreferredTags, sentNotifications); // Fallback logic: No matching events, or user has no preferences if (!eventToSend) { eventToSend = findBangerEvent(allEvents, sentNotifications); } if (!eventToSend && sentNotifications.length > 0) { console.log(`No new events to suggest, resetting`); eventToSend = sentNotifications[sentNotifications.length - 1]; reset = true; } if (!eventToSend) { console.log(`No events to send for user ${userDoc.id}. Skipping.`); continue; } const notificationPayload = createNotificationPayload( eventToSend, fcmToken, language ); await admin.messaging().send(notificationPayload); console.log(`Successfully sent message to user ${userDoc.id}, ${notificationPayload.notification.title}`); const updatedNotifications = updateSentNotifications(eventToSend, reset ? [] : sentNotifications); await userDoc.ref.update({ sentNotifications: updatedNotifications }); } catch (error) { console.error(`Error processing user ${userDoc.id}:`, error); } } console.log("Notifications sent successfully."); }
修改后的异步+批量处理代码(触发WriteBatch错误)
async function sendNotifications() { console.log("Sending notifications for recommended events..."); // Fetch all events once const eventsRef = admin.firestore().collection("Events"); const eventsSnapshot = await eventsRef .where('Start', '>=', new Date()) .get(); if (eventsSnapshot.empty) { console.log("No upcoming events found."); return; } const allEvents = eventsSnapshot.docs.map(doc => ({ ...doc.data(), docId: doc.id })); // Fetch all users const usersRef = admin.firestore().collection("Users"); const usersSnapshot = await usersRef.get(); const usersToProcess = usersSnapshot.docs.filter(userDoc => { const userData = userDoc.data(); return true; // Include all users with an FCM token (set to true in emulator) }); console.log(`Processing ${usersToProcess.length} users...`); const notifications = []; let batch = admin.firestore().batch(); let batchUserCount = 0; // Track the number of users in the current batch const userPromises = usersToProcess.map(async (userDoc) => { const userData = userDoc.data(); const { fcmToken, preferences, language = "en", sentNotifications = [] } = userData; const userPreferredTags = preferences || []; let eventToSend = findEventForUser(allEvents, userPreferredTags, sentNotifications); // Fallback logic: No matching events if (!eventToSend) { eventToSend = findBangerEvent(allEvents, sentNotifications) || sentNotifications[sentNotifications.length - 1]; } if (!eventToSend) { console.log(`No events to send for user ${userDoc.id}. Skipping.`); return; } const notificationPayload = createNotificationPayload(eventToSend, fcmToken ? fcmToken : "ezeazea", language); notifications.push(notificationPayload); const updatedNotifications = updateSentNotifications(eventToSend, sentNotifications); const dataSize = JSON.stringify({ sentNotifications: updatedNotifications }).length; console.log(`Estimated size of update: ${dataSize} bytes`); batch.update(userDoc.ref, { sentNotifications: updatedNotifications }); batchUserCount++; // If the batch has 100 operations, commit the batch and start a new one if (batchUserCount === 100) { console.log("Committing Firestore batch..."); await batch.commit(); // Commit the batch batch = admin.firestore().batch(); // Create a new batch batchUserCount = 0; // Reset the batch user count } }); await Promise.all(userPromises); // Commit remaining updates if any users were left in the batch if (batchUserCount > 0) { console.log("Committing remaining Firestore batch..."); await batch.commit(); } // Send notifications in bulk (in batches of 100) console.log("Sending notifications in bulk..."); while (notifications.length) { const batchNotifications = notifications.splice(0, 100); // Firebase max batch size for FCM try { await admin.messaging().sendEach(batchNotifications); } catch (error) { console.error("Error sending notifications:", error); // Handle the error as necessary } } console.log("Notifications sent successfully."); }
触发的错误信息
⚠ functions: Error: Cannot modify a WriteBatch that has been committed.
我自己猜测是因为多个异步的用户处理函数同时在操作同一个全局的batch对象,当其中一个函数提交了batch之后,其他还在运行的函数不知道batch已经被提交了,还在往里面加update操作,导致报错。但我不确定这个猜测对不对,也不知道该怎么改才能正确实现异步处理+批量更新的逻辑,希望大家能给我一些具体的实现建议,谢谢啦!
备注:内容来源于stack exchange,提问作者TiropZ

