处理入站事件,不把到达顺序当作事实顺序

使用 AI 辅助编写与复核。

本文目录

当入站事件重复或乱序到达时,“使用最新事件”不是一项完整指令。“最新”可能指最后收到、时间戳最大、资源版本最新,或当前权威状态。应给编程 Agent 一份可以逐步走查的记录,包括这些信号不一致时该怎么办。

Stripe 的 webhook 文档提供了一个具体的提供方案例:投递顺序没有保证,时间戳可能相同,重复投递需要处理。其重复事件章节讨论了记录已处理事件 ID 的做法。本指南据此关注这些问题,但构建的是另一份虚构协议。下面的资源版本是我们选定的设计,不是 Stripe 字段或 API 版本保证。

定义事件标识什么

设想一个规划看板接收卡片的完整快照。它的初始状态是:

{
  "card-7": {"version": 10, "state": {"stage": "queued", "label": "Map"}},
  "card-8": {"version": 3, "state": {"stage": "ready", "label": "Chart"}}
}

协议采用以下事件封套:

{
  "event_id": "e-201",
  "object_id": "card-7",
  "object_version": 12,
  "occurred_at": "2024-01-01T08:00:00Z",
  "type": "card.snapshot",
  "state": {"stage": "ready", "label": "Map"}
}
字段 本例中的含义
event_id 一个事件的身份,在重复投递中保持不变
object_id 完整状态所对应的卡片
object_version 来源方为该卡片分配的递增整数;只能在同一卡片内比较
occurred_at 提供参考的 UTC 时间;既不是排序键,也不是去重键
type 这里只支持 card.snapshot
state 完整的 stage 与 label 快照,不是对未指定字段的局部补丁

所有记录、ID、版本与结果都是原创教学数据。接收器假定输入已经通过真实集成的真实性与结构校验边界;它不实现该边界,也不联系 Stripe。B62-15-events.json 提供每次投递与两份对账快照。

因为每条消息都包含完整快照,版本之间可以有缺口。因此,在版本 10 上应用版本 12,不一定需要重建版本 11。增量协议需要不同的缺口处理策略。不能因为接收器需要作出排序决定,就推断所有提供方都提供这种版本契约。

重放事件流前,先选择应用规则

对于已知且未暂停的卡片:

  1. 与保留的事件 ID 记录比较。已处理过的相同事件不产生新效果。同一 ID 对应不同语义内容时,视为冲突:保留首份记录,暂存传入的变体,并暂停其影响到的已知卡片。
  2. 要求类型受支持、对象已知且版本为有效整数。缺少依据的输入暂存等待对账;不自动创建未知对象。
  3. 较高版本替换卡片的完整状态。较低版本记为过时,不改变卡片。同版本且状态相同不产生新效果;同版本但状态不同则视为冲突。
  4. 卡片暂停期间,其他本来有效的事件也要暂存,直到有明确的对账决定。

语义比较包括上面展示的事件封套,不包括仅与投递有关的传输元数据。JSON 键顺序或空白不同,不代表事件变化。相同版本下的状态比较关注快照状态,不关注事件各自不同的 ID 或参考时间。

本例拟议的效果是替换本地卡片状态。该替换与事件决定应一起提交。顺序执行的内存归约模型可以演示这些决定,但不能证明数据库事务、并发接收器或下游通知具备同样的边界。

使用相同 ID,走查每次投递

令 T 表示 2024-01-01T08:00:00Z。表中 ready/Map 是完整状态 {"stage":"ready","label":"Map"} 的缩写。编号行表示到达顺序,不是事件时间戳顺序。

投递 完整的有效载荷信息 决定 之后的卡片状态 / 确认响应
1 e-201,card-7,v12,T,ready/Map 一起应用 v12 并记录 e-201 card-7 为 v12 ready/Map;确认响应丢失
2 完全相同的 e-201 再次到达 已知相同事件;不新增状态变化 card-7 保持 v12 ready/Map;确认已有决定
3 e-200,card-7,v11,T,building/Map 较旧版本;记录为过时 card-7 保持 v12;确认已记录的决定
4 e-202,card-7,v13,T+1s,ready/Map v2 应用 v13 card-7 为 v13 ready/Map v2;确认
5 e-207,card-7,v13,T+2s,ready/Map v2 不同事件,版本/状态相同;无新效果 card-7 保持 v13;确认已记录的决定
6 e-202 再次到达,但 label 为 Forge 同一 ID,载荷变化;暂存变体并暂停 card-7 card-7 保持 v13 ready/Map v2;仅确认已持久记录暂存决定
7 e-203,card-7,v14,T+3s,archived/Map v2 卡片已暂停;暂存 card-7 保持 v13;仅确认已持久记录暂存决定
8 e-300,card-99,v1,T+3s,queued/New 未知对象;暂存,不创建 card-99 不存在;仅确认已持久记录暂存决定
9 e-204,card-8,版本缺失,T+4s,archived/Chart 缺少版本;暂存并暂停 card-8 card-8 保持 v3 ready/Chart;仅确认已持久记录暂存决定
10 e-205,card-8,v4,T+5s,building/Chart 卡片已暂停;暂存 card-8 保持 v3;仅确认已持久记录暂存决定

第 1 行与第 3 行的时间戳完全相同。时间戳相同不能说明哪个快照较新;在这份虚构协议中,由版本区分。第 2 行也说明:确认响应丢失,并不能证明第一次应用失败。

确认策略是另一项明确选择:服务持久记录“已应用”“已忽略”或“已暂存”的决定后,可以确认接收。暂存决定表示服务接下了对账责任,不表示它已经应用该状态。如果不能持久记录决定,就不能声称接下了该责任。模型没有实现 HTTP 响应或持久化收件箱。

仅有事件 ID 列表不能解决第 6 行的问题。悄悄丢弃所有重复 ID,会掩盖载荷变化冲突。接收器也不能用变体覆盖首个已存事件,导致调查所需证据丢失。

对账时,不随意判定谁获胜

存在冲突的 e-202 变体不能解释内容为什么变了。看板选择暂停,不认定先到或后到的那份必然正确。

本练习另外提供一份 T+20s 时的权威快照,确定以下内容:

对象 权威快照 处理决定
card-7 v14,archived/Map v2 根据该权威来源替换状态、解除暂停,再重新判断暂存的 e-203
card-8 v5,ready/Chart 根据该权威来源替换状态、解除暂停,再重新判断暂存的 e-205
card-99 未提供权威身份 继续暂存 e-300,不编造卡片

这些是固定的对账数据,不是从账户获取的响应。真实集成在采用相同策略前,必须确定查询的权威性、适用范围和版本语义。

对账后,e-203 与 card-7 的 v14 快照相同,不需要新效果。e-205 比 card-8 的 v5 旧,记为过时。无版本的 e-204 继续作为未解决输入保留;权威快照取代了依靠该事件决定状态的需要,但不会给事件编造版本。原始 e-202、冲突变体与解决记录继续保存在审计记录中。

因此,最终应用状态恰好是:

{
  "card-7": {"version": 14, "state": {"stage": "archived", "label": "Map v2"}},
  "card-8": {"version": 5, "state": {"stage": "ready", "label": "Chart"}}
}

对账前有两次事件驱动的状态替换,之后有两次权威状态替换。重复、过时和同状态投递没有增加额外替换。这个计数描述的是数据案例中的归约模型,不是对恰好一次(exactly-once)投递或效果的保证。

本例将事件身份与决定保留 24 小时;展示的每次投递都在该窗口内。窗口结束后,仅凭身份记录不存在,无法区分新事件与旧事件重放。需要对账或明确的过期识别策略。这里没有暗示去重记录会无限期有效。

复制接收器状态任务说明

为虚构的 card.snapshot 协议设计本地接收器状态模型。
使用 B62-15-events.json、其中十次投递与两份权威快照。
不要搭建 webhook 服务或联系提供方。

event_id 是事件身份,object_id 是资源身份。
在这份虚构契约中,object_version 是每张卡片各自递增的整数。
state 是完整快照。occurred_at 仅供参考,不是排序键。
不要假定真实提供方公开了相同的版本或权威状态模型。

保留首个事件 ID 记录,并在重复投递时比较语义内容。
相同 ID/相同内容:恢复已有决定,不产生新效果。
相同 ID/内容变化:保留两个变体,暂停受影响的已知卡片,
并暂存后续事件。不要覆盖首份记录。

对于未暂停的已知卡片:高版本替换完整状态;
旧版本记为过时;同版本/同状态不操作;
同版本/不同状态视为需要对账的冲突。
暂存未知对象、缺失版本和不支持类型,不修改应用状态。
持久暂存不等于成功应用。

演示确认响应丢失/重复事件、相同时间戳、v12 之后到达的 v11、
e-202 载荷变化、缺失版本和未知卡片。
只根据给定权威快照对账,然后将暂存的 e-203 归为相同,
将 e-205 归为过时。保留未解决输入与审计轨迹。

返回每一步的卡片版本/状态、事件决定、暂停/暂存、
确认响应含义、最终状态与状态替换计数。
在实施前,说明一起提交状态与接收记录所需的真实存储事务和并发边界。
下游通知、认证、存储保留期执行与提供方 API 不在此模型范围内。
把已执行的模型检查与尚未实现的保证分开。

有用的输出应是一份收敛的状态历史,并明确指出未解决输入。“事件去重”只有在 Agent 能说清识别了哪个事件、改了哪个对象,以及依据什么证据推进该对象状态时,才成为可执行的任务。