Google Calendar插件多事件用户同步失效问题排查
问题描述
我开发了一款自定义Google Calendar插件,用户完成认证后可创建包含名称、日期、时间及所选附加组件的事件,选择附加组件后,事件会同步到Google Calendar并存入数据库。
该功能在部分测试用户(如educasticxxx@gmail.com)中运行正常,但部分用户出现同步异常:
- 用户日历事件数量少的时候,同步符合预期;
- 用户日历事件较多时,同步失败或无响应。
已采取的措施
使用带sync token的calendar.events.list接口进行增量同步。
同步代码
function syncEvents(e) { var calendarId = e.calendarId; var properties = PropertiesService.getUserProperties(); var syncToken = properties.getProperty('syncToken'); var options; if (syncToken) { options = { syncToken: syncToken }; } else { var now = new Date(); var yesterday = new Date(); yesterday.setDate(now.getDate() - 1); options = { timeMin: now.toISOString(), updatedMin: yesterday.toISOString(), maxResults: 50, orderBy: 'updated' } } var events; var pageToken; do { try { options.pageToken = pageToken; events = Calendar.Events.list(calendarId, options); // 疑似问题所在行 } catch (err) { if (err.message === "Sync token is no longer valid, a full sync is required.") { properties.deleteProperty('syncToken'); syncEvents(e); return; } else { throw new Error(err.message); } } // 处理事件 if (events.items && events.items.length > 0) { for (var i = 0; i < events.items.length; i++) { var calEvent = events.items[i]; if (eventHasConference(calEvent)) { updateConference(calEvent, calEvent.conferenceData.conferenceId); } else if(calEvent.status === 'cancelled') { deleteConference(calEvent.id); } } } pageToken = events.nextPageToken; } while (pageToken); if (events.nextSyncToken) { properties.setProperty('syncToken', events.nextSyncToken); } }
问题分析与解决方案
1. 分页执行超时问题
当用户日历事件量大时,循环分页请求Calendar.Events.list容易触发Google Apps Script的6分钟执行超时限制,尤其是全量同步阶段(无sync token时),maxResults=50会导致分页次数过多,超出时间上限。
优化方案:
- 缩小全量同步时间范围:将
updatedMin从"昨天"调整为"最近7天",减少首次同步的事件总量; - 提升单页请求数量:将
maxResults设为API允许的最大值100,减少分页循环次数; - 分页间隔休眠:在每页请求后加入
Utilities.sleep(100),避免短时间高频请求触发API速率限制; - 拆分同步任务:用
ScriptApp.newTrigger()创建时间驱动触发器,分批次处理事件,避免单次执行超时。
2. Sync Token递归重试风险
当前代码在sync token失效时直接递归调用syncEvents(e),若全量同步仍因事件量过大失败,会引发重复递归,甚至触发脚本调用栈限制。
优化方案:
- 替换递归为带次数限制的循环重试(比如最多3次);
- 捕获API速率限制错误(如
Quota exceeded),加入指数退避重试逻辑(每次重试间隔翻倍); - 同步失败时记录日志,便于排查具体用户的异常情况。
3. 事件处理性能瓶颈
单条处理事件的数据库操作(updateConference/deleteConference)会导致IO次数过多,事件量越大耗时越长。
优化方案:
- 批量处理事件:收集所有待更新/删除的事件ID,统一执行数据库操作,减少IO开销;
- 精简请求字段:在
Calendar.Events.list的options中添加fields参数,只请求所需字段(如items(id,status,conferenceData),nextPageToken,nextSyncToken),减少数据传输量,提升请求速度。
优化后代码示例
function syncEvents(e) { var calendarId = e.calendarId; var properties = PropertiesService.getUserProperties(); var syncToken = properties.getProperty('syncToken'); const MAX_RETRY = 3; let retryCount = 0; // 只请求所需字段,减少数据传输 var options = { fields: 'items(id,status,conferenceData),nextPageToken,nextSyncToken' }; if (syncToken) { options.syncToken = syncToken; } else { var now = new Date(); // 缩小全量同步时间范围到最近7天 var sevenDaysAgo = new Date(); sevenDaysAgo.setDate(now.getDate() - 7); options.timeMin = now.toISOString(); options.updatedMin = sevenDaysAgo.toISOString(); options.maxResults = 100; options.orderBy = 'updated'; } var events; var pageToken; const updateQueue = []; const deleteQueue = []; do { try { options.pageToken = pageToken; events = Calendar.Events.list(calendarId, options); } catch (err) { if (err.message.includes("Sync token is no longer valid")) { properties.deleteProperty('syncToken'); if (retryCount < MAX_RETRY) { retryCount++; Utilities.sleep(1000 * retryCount); return syncEvents(e); } else { throw new Error("同步token失效后重试次数已达上限"); } } else if (err.message.includes("Quota exceeded") || err.message.includes("Rate limit")) { Utilities.sleep(1000 * Math.pow(2, retryCount)); retryCount++; if (retryCount >= MAX_RETRY) throw err; continue; } else { throw err; } } // 批量收集待处理事件 if (events.items && events.items.length > 0) { events.items.forEach(calEvent => { if (eventHasConference(calEvent)) { updateQueue.push({event: calEvent, id: calEvent.conferenceData.conferenceId}); } else if (calEvent.status === 'cancelled') { deleteQueue.push(calEvent.id); } }); } pageToken = events.nextPageToken; if (pageToken) Utilities.sleep(100); } while (pageToken); // 批量执行数据库操作 if (updateQueue.length > 0) batchUpdateConferences(updateQueue); if (deleteQueue.length > 0) batchDeleteConferences(deleteQueue); if (events.nextSyncToken) { properties.setProperty('syncToken', events.nextSyncToken); } } // 批量更新数据库逻辑示例 function batchUpdateConferences(queue) { // 实现批量更新逻辑 } // 批量删除数据库逻辑示例 function batchDeleteConferences(queue) { // 实现批量删除逻辑 }
内容的提问来源于stack exchange,提问作者Educastic educast
相关产品推荐
相关产品推荐

