框架深潜

Odoo Microsoft Calendar 同步为什么难在 token 失效之外:delta token、重复事件与 post-commit 安全边界

Outlook 日历同步最麻烦的从来不只是 OAuth 授权过期,而是增量 delta token 失效后如何回退全量同步、循环事件为什么必须尽量在 Outlook 端创建,以及 Odoo 为什么要把外部推送动作放到 post-commit,避免本地事务和远端日历状态互相打架。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的 microsoft_calendar 模块,看上去像一个“把会议同步到 Outlook”的集成功能;但从源码看,它真正处理的是一个很棘手的双向同步问题:

  1. 本地事务提交节奏与远端 Graph API 调用节奏并不相同;
  2. 增量同步依赖 delta token,而 token 会失效;
  3. 循环事件在 Outlook 侧有很多限制,不能随便在 Odoo 里改;
  4. 同一个会议同时存在 Microsoft event id、iCalUId、seriesMasterId 等多层身份。

所以,这个模块最值得看的不是“怎么接 Microsoft”,而是双向日历同步如何尽量避免状态打架

need_sync_m 是整套同步系统的心跳字段

在多个模型里你都能看到 need_sync_m

  • calendar.event
  • calendar.recurrence
  • microsoft.calendar.sync

它不是一个普通“是否已同步”的展示位,而是待同步意图

源码里的模式大致是:

  • 本地事件被创建、修改、删除时,若属于微软同步字段,且当前同步处于活动状态,就把 need_sync_m=True
  • 当某些变更是从微软端反向拉回 Odoo 时,会带 dont_notify=True 并把 need_sync_m=False,避免回写风暴;
  • 循环事件的特殊展开与映射也会反复调整这个标记。

这说明 Odoo 并不是“每改一次就立刻强推 Graph API”,而是在维护一个可重试、可去抖的本地待同步集合

这非常重要,因为日历同步天然会遇到:

  • 多人编辑;
  • 批量写入;
  • 事务回滚;
  • 网络失败;
  • 远端对象已不存在。

如果没有 need_sync_m 这一层,本地变更和远端同步会很快缠成一团。

为什么 post-commit 是这类集成的生命线?

microsoft_sync.py 里有一个关键点:使用了 self.env.cr.postcommit.add

这件事很关键,甚至可以说是整套设计的安全底座。

为什么不能在事务里直接强推 Outlook?

设想一个场景:

  • 用户保存会议;
  • Odoo 在数据库事务尚未提交时,就先调用 Microsoft Graph 创建或更新远端事件;
  • 结果本地事务后面又因为别的字段校验失败而回滚了。

这时你就会得到一个非常尴尬的结果:

  • Outlook 里已经有了新状态;
  • Odoo 本地却没有这次变更。

这就是典型的分布式一致性问题。

Odoo 的做法是:

  • 本地先完成事务;
  • 提交成功后,再触发对微软端的同步动作。

这并不能让双向同步变成强一致,但至少守住了一个核心原则:

不要让一个最终会回滚的本地状态先污染外部系统。

对所有 SaaS 集成来说,这都是非常重要的设计经验。

delta token 为什么既高效又脆弱?

res_users.pyutils/microsoft_calendar.py 里可以看到,Odoo 会保存 microsoft_calendar_sync_token,并通过 Graph 的 delta 机制拉增量事件。

这很高效,因为它避免了每次全量扫用户日历。

但源码也明确处理了 InvalidSyncToken

  • 如果旧 token 失效,就退回 get_events(token=token) 做无旧 token 的同步,再获取新的 next sync token。

这告诉我们什么?

  • delta token 不是永久凭证;
  • 它是一个“仅在当前服务端历史窗口内有效”的增量游标;
  • 一旦服务端历史被裁剪、上下文变化、或者 token 过旧,客户端就必须接受一次更重的重新对齐。

这也是为什么同步系统里,能够优雅回退到全量重建,比“增量模式平时很快”更重要。

为什么循环事件不让你在 Odoo 里随便建、随便改?

如果当前同步活跃,源码会明确阻止:

  • 在 Odoo 中直接创建 recurrent events;
  • 在 Odoo 中直接更新 recurrence;
  • 某些已同步循环事件的删除/归档操作。

报错信息也很坦率:

  • 由于 Outlook Calendar 的限制,循环事件应直接在 Outlook 里创建或更新。

很多用户第一次看到这类限制会觉得“不够智能”,但实际上这是在尊重对端系统的真实约束

源码里提到的典型限制包括:

  • Outlook 对 recurrence exception 的移动有边界,不能把一次 occurrence 挪到前一场之前或后一场之后;
  • Outlook 对循环事件的修改容易触发垃圾通知/垃圾同步;
  • Odoo 若强行本地改,很容易把整组 recurrence 弄乱。

这也是为什么模块会建议:

  • 单次例外可以谨慎处理;
  • 真正的 recurrence 结构变更,尽量回到 Outlook 端操作。

这不是功能欠缺,而是把高风险操作放回更权威的一侧。

一个会议为什么要有三个 ID?

在微软日历同步里,常见三个标识:

  • microsoft_id
  • ms_universal_event_id(iCalUId)
  • microsoft_recurrence_master_id(seriesMasterId)

很多人会问,一个会议为什么不能只用一个 ID?

因为它们解决的问题不同:

microsoft_id

更像 Graph API 当前对象的具体实例标识。

iCalUId

是跨日历、跨副本更稳定的“通用事件身份”。源码甚至会在某些场景通过 iCalUId 去反查真实 event id。

seriesMasterId

是循环事件体系里的主系列身份,用来把单个 occurrence 重新挂回母体结构。

如果没有这三层身份,以下问题会很难处理:

  • 同一会议被不同日历副本感知;
  • 循环事件拆分为主事件与实例;
  • 某次同步拿到的是 occurrence,而不是 master;
  • 本地和远端需要重新匹配未映射事件。

所以这不是 ID 设计过度,而是日历同步本身比普通业务单据复杂得多。

为什么“第一次同步日期”也要被单独记录?

源码里有 microsoft_calendar.sync.first_synchronization_date 相关逻辑。

它的作用是:

  • 当系统进入同步后,用一个时间边界限制后续要纳入的本地事件范围;
  • 避免把过久远的历史会议全量推送到 Outlook,造成噪音和性能压力。

这一步很有产品感。

因为日历不是发票。旧会议大量回灌到外部系统,通常没有意义,反而会制造:

  • 同步耗时变长;
  • 历史提醒被重建;
  • 用户误以为旧事项被重新安排。

所以首同步日期本质上是在定义:从什么时候开始,把 Odoo 当成外部日历的有效历史来源。

同步暂停与停止不是一回事

源码里 _get_microsoft_sync_status() 会区分:

  • sync_active
  • sync_paused
  • sync_stopped
  • 以及未配置 / 未授权场景

这区分非常有必要。

paused

更像系统级开关:

  • 暂时别同步;
  • 但连接与状态还在;
  • 恢复后可以继续。

stopped

更像用户或系统显式断开:

  • 不再认为当前连接是活跃同步关系;
  • 某些本地记录会停止继续向外推。

如果把这两者混成一个布尔值,就很难做好:

  • 运维临时停同步;
  • 用户主动断开;
  • 恢复策略;
  • 批量重启同步。

实战里最该注意的 5 个坑

1. 把事务内外边界想简单了

日历同步最怕本地还没 commit,外部已经更新成功。post-commit 不是锦上添花,而是底线。

2. 以为 delta token 永远有效

一旦 token 失效,必须允许系统重新全量对齐,而不是硬顶着旧 token 重试。

3. 低估循环事件复杂度

循环事件不是“多条普通会议”,它在 Outlook 里有非常强的结构约束。

4. 不理解多层 ID 的必要性

只靠一个 event id,很难正确追踪 recurrence、重映射与跨副本匹配。

5. 把 need_sync_m 当成展示字段

它其实是本地同步队列的一部分,是整个双向集成稳定性的关键。

最后一句

Microsoft Calendar 模块最成熟的地方,不在于“能同步会议”,而在于它承认了一件事:

日历双向同步本质上不是接口调用问题,而是一致性问题。

所以它才会:

  • need_sync_m 缓冲本地意图;
  • 用 post-commit 保护事务边界;
  • 用 delta token 提升效率,同时接受失效回退;
  • 对循环事件保持克制;
  • 用多层 ID 管理远端对象身份。

这套设计不花哨,但非常靠谱。真正长期稳定的集成,往往就是靠这些看似保守的边界守出来的。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。