处理入站事件,不把到达顺序当作事实顺序
本文目录
当入站事件重复或乱序到达时,“使用最新事件”不是一项完整指令。“最新”可能指最后收到、时间戳最大、资源版本最新,或当前权威状态。应给编程 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。增量协议需要不同的缺口处理策略。不能因为接收器需要作出排序决定,就推断所有提供方都提供这种版本契约。
重放事件流前,先选择应用规则
对于已知且未暂停的卡片:
- 与保留的事件 ID 记录比较。已处理过的相同事件不产生新效果。同一 ID 对应不同语义内容时,视为冲突:保留首份记录,暂存传入的变体,并暂停其影响到的已知卡片。
- 要求类型受支持、对象已知且版本为有效整数。缺少依据的输入暂存等待对账;不自动创建未知对象。
- 较高版本替换卡片的完整状态。较低版本记为过时,不改变卡片。同版本且状态相同不产生新效果;同版本但状态不同则视为冲突。
- 卡片暂停期间,其他本来有效的事件也要暂存,直到有明确的对账决定。
语义比较包括上面展示的事件封套,不包括仅与投递有关的传输元数据。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 能说清识别了哪个事件、改了哪个对象,以及依据什么证据推进该对象状态时,才成为可执行的任务。