处理同步有效负载的演变

(请注意,本文档已采用 应用程序服务 ADR 的格式编写,但相关团队最终决定此文档的最佳归宿是 mozilla-central。)

  • 状态:已接受

  • 决策者:同步团队、凭据管理团队

  • 日期:2023-03-15

技术故事

  • https://github.com/mozilla/application-services/pull/5434

  • https://docs.google.com/document/d/1ToLOERA5HKzEzRVZNv6Ohv_2wZaujW69pVb1Kef2jNY

上下文和问题陈述

同步存在于所有平台(桌面、Android、iOS)、所有渠道(Nightly、Beta、Release、ESR)上,并在所有 Firefox 功能中得到广泛使用。每当出现可能涉及模式更改的功能更改或请求时,都没有很多好的选择来确保同步不会对任何特定客户端造成破坏。由于同步数据是从所有渠道同步的,我们需要确保每个客户端都能处理新数据,并且所有渠道都能支持新模式。诸如 由于桌面 Nightly 上的模式更改导致信用卡在 Android 和桌面发布渠道上失败 之类的问题是我们可能遇到的此类案例的示例。本文档描述了我们关于如何随着时间的推移支持有效负载演变的决策。

请注意,即使本文档存在于 application-services 存储库中,也应将其视为适用于所有同步实现,无论是在此存储库中、在 mozilla-central 中,还是在任何其他地方。

定义

  • “新”Firefox 安装是指 Firefox 的一个版本,该版本对同步有效负载进行了更改,而“近期”版本尚不了解这些更改。最常见的示例是包含尚未发布到发布渠道的新功能的 Firefox Nightly 版本。

  • “近期”Firefox 安装是指比“新”版本旧的版本,它不理解或不支持“新”版本中的新功能,但我们仍然希望在不造成破坏且用户不会察觉数据丢失的情况下支持它。这通常被认为是指当前的 ESR 版本或更高版本,但要考虑到发布新 ESR 时更新缓慢。

  • “旧”版本是指我们认为“近期”版本之前的任何版本。

决策驱动因素

  • 必须能够更改同步携带的数据以满足未来的产品需求。

  • 必须考虑桌面和移动平台。

  • 当“新”Firefox 安装同步时,我们不得破坏“近期”Firefox 安装,反之亦然。

  • 通过“近期”Firefox 安装对“新”Firefox 安装的数据进行往返传输不得丢弃任何新数据,反之亦然。

  • 如果绝对必要,可能会考虑对“旧”Firefox 安装在“新”或“近期”Firefox 同步时造成一定程度的破坏是可以接受的。

  • 但是,当“旧”版本同步时,“新”或“近期”Firefox 出现故障是不可接受的

  • 由于此类演变应该很少见,因此我们不想预先制定关于仅仅因为将来可能出现问题而锁定“旧”版本的策略。也就是说,我们希望避免一项策略,该策略规定超过(例如)2 年的版本在同步时“以防万一”就会出现故障。

  • 此问题的任何解决方案都必须在相对较短的时间内实现,因为我们知道未来会提出需要此功能的产品请求。

考虑的选项

  • 向后兼容的模式策略,包括 (a) 让引擎“往返传输”它们不了解的数据,以及 (b) 从不更改现有数据的语义。

  • 一项阻止“近期”客户端同步或编辑数据或其他限制的策略。

  • 正式的模式驱动流程。

  • 将同步有效负载视为冻结状态,永不更改。

  • 为新数据使用单独的集合

决策结果

选择的选项:向后兼容的模式策略,因为它非常灵活,并且是唯一满足决策驱动因素的选项。

选项的优缺点

向后兼容的模式策略

此选项的摘要是:

  • 每个同步引擎都必须安排持久化有效负载中它不理解的任何字段。下次该引擎需要将该记录上传到存储服务器时,它必须安排将所有此类“未知”字段添加回有效负载中。

  • 不同的引擎必须识别可能发生此情况的不同位置。例如,passwords 引擎将识别有效负载的“根”、addressescreditcards 将识别有效负载中的 entry 子对象,而历史记录引擎可能会识别有效负载的visits 数组。

  • 字段不能更改类型,也不能在很长一段时间内删除。这意味着“新”客户端必须同时支持新字段这些“新”客户端认为已弃用的字段,因为“近期”版本仍在使用它们。

优点和缺点

  • 优点,因为它满足了要求。

  • 优点,因为最初确定的一组工作相对容易实施(该工作具体是指支持“未知”字段的往返传输,希望在提出实际模式更改之前,此往返传输功能将在所有“近期”版本上可用)。

  • 缺点,因为无法弃用或更改现有字段意味着某些演变任务变得复杂。例如,考虑一个假设的更改,我们希望将“街道/城市/州”字段更改为自由格式的“地址”字段。新的 Firefox 版本在写入服务器时需要填充新字段旧字段,并处理仅当它看到“近期”或“旧”版本的 Firefox 编写的传入记录时才会更新旧字段的事实。但是,这种情况应该很少见。

  • 缺点,因为无法证明建议的更改满足要求——该策略是非正式的,并且在提出更改时需要良好的判断力。

一项阻止“近期”客户端同步或编辑数据的策略

适合此类别的提案可能是通过(例如)向模式添加版本号来实现的,如果客户端没有完全理解模式,它将阻止同步记录,或者同步记录但不允许编辑它,或者类似的操作。

此提案被拒绝,因为

  • 如果我们完全忽略传入数据,用户肯定会察觉到数据丢失。

  • 如果我们仍然希望旧版本“部分”查看记录(例如,但不允许编辑),我们仍然需要选择的选项的大部分内容——具体来说,我们仍然不能弃用字段等。

  • 尝试向用户解释为什么他们无法编辑记录的 UI/UX 被认为无法以令人满意的方式完成。

  • 这实际上会以任何方式惩罚选择使用 Firefox Nightly 版本的用户。仅仅允许 Nightly 同步实际上会破坏 Release/Mobile Firefox 版本。

正式的模式驱动流程。

理想情况下,我们可以正式描述模式,但我们在这里无法提出任何适用于支持旧客户端的约束条件的方案——我们根本无法更新旧版已发布的 Firefox 以便它们知道如何处理新模式。我们也无法提出一个模式动态下载的解决方案,该解决方案还允许描述新字段的语义(而不是仅仅是有效性)。

将同步有效负载视为冻结状态,永不更改。

有效负载冻结的流程被拒绝,因为

  • 此处最简单的做法将无法满足 Firefox 未来需求。

  • 一个复杂的系统,我们开始创建新的有效负载和新的集合(即冻结“旧”模式,然后创建仅供较新客户端理解的“新”模式),无法以仍然满足要求的方式构思,尤其是在旧客户端的数据丢失方面。例如,在 Nightly 版本上添加信用卡,但在发布版 Firefox 上完全不可用是不可接受的。

为新数据使用单独的集合

我们可以将新数据存储在单独的集合中。例如,定义一个 bookmarks2 集合,其中每个记录与 bookmarks 中的一个记录具有相同的 guid,以及任何新字段。较新的客户端同时使用这两个集合进行同步。

优点和缺点

  • 优点,因为它允许较新的客户端同步新数据,而不会影响近期或较旧的客户端

  • 缺点,因为在没有服务器更改的情况下,同步写入将失去原子性。我们目前可以以原子方式写入单个集合,但没有办法写入多个集合。

  • 缺点,因为每次我们想要添加字段时,此集合的数量都会增加。

  • 缺点,因为它可能会向获取加密服务器记录访问权限的攻击者泄露额外信息。例如,如果我们为单个字段添加了一个新集合,那么攻击者可以根据加密记录的大小猜测该字段是否已设置。

  • 缺点,因为它难以使用此方法处理嵌套数据,例如向历史记录记录访问添加字段。

  • 缺点,因为它与所选解决方案具有相同的依赖数据问题。