Phorge 现代化改造实战(十):迁移 Webhook 投递服务,先解决重复投递

💡 原文中文,约11200字,阅读约需27分钟。
📝

内容提要

本文介绍Phorge现代化改造中首个主动读取数据库的Gorge Webhook服务迁移。核心难点是任务所有权与重复投递问题,通过复用dateModified字段实现CAS乐观锁抢占,避免修改Phorge schema。同时处理了全局静默配置、数据库初始化依赖、日志级别不当等隐患,强调共享队列消费者需先设计所有权和可观察性,否则可能静默产生重复请求。

🔎

延伸解读

为什么不能直接改状态或加列

文章指出,直接修改 status 或新增租约列看似简单,实则危险。status 的合法值由 Phorge 定义,PHP worker 和 UI 依赖它;新增列会被 Lisk ORM 视为 surplus,在后续 bin/storage adjust 时被删除,导致重复投递在数周后突然出现。复用 dateModified 做 CAS 是更安全的选择,因为它不改变 schema,且能保证并发下的原子性。

灰度切换的陷阱

无状态服务可以先上线新实例再切流量,但共享队列消费者不能这样。PHP worker 与 Gorge 同时运行会从同一张表读取同一行,导致接收方收到两份相同请求。因此切换必须通过配置 URI 一次性完成,且要防止 URI 未写入时容器已运行的状态,否则会双写队列。

日志级别与可观察性

默认日志级别下,证明 claim 成功的 Debug 日志被隐藏,而熔断时的 Warn 日志每秒可能产生 8 条,一天近 69 万条。这提示日志级别应根据线上排查需要分配,而非代码重要性。建议对熔断跳过做聚合或状态变化日志,并考虑可配置日志级别。

数据库初始化依赖

gorge-webhook 的 /readyz 只检查数据库连接,但 DSN 中的库名若不存在,连接仍会失败。这导致依赖关系闭环:Phorge 等 Gorge ready,Gorge 等数据库存在,数据库等 Phorge 执行 storage upgrade。修复方法是让 Phorge 只要求 Gorge 已启动,而非 ready,允许首次启动出现短暂 unhealthy 窗口。

Q&A

Phorge 现代化改造中,Webhook 投递服务迁移的核心难点是什么?

核心难点是任务所有权与重复投递问题。由于 Gorge 直接轮询 Phorge 的队列表,如果处理不当,同一个 Webhook 请求可能被重复投递。解决方案是复用 dateModified 字段实现 CAS 乐观锁抢占,避免修改 Phorge 的 schema。

Gorge 的 Webhook 服务如何避免重复投递同一个请求?

Gorge 通过 CAS(Compare-And-Swap)乐观锁来避免重复投递。它使用 UPDATE 语句,条件包括 status='queued' 和 dateModified 等于之前 SELECT 到的值,并将 dateModified 更新为 GREATEST(dateModified+1, UNIX_TIMESTAMP())。只有当 RowsAffected()==1 时才表示抢占成功,否则当前进程不发送请求。

为什么不能直接给 Phorge 的 herald_webhookrequest 表增加一个租约列(如 lease_until)来解决重复投递?

因为 Phorge 使用 Lisk ORM 管理 schema,如果增加一个未在模型配置中声明的列,该列会被 bin/storage adjust 视为 surplus column 并在后续维护中删除,导致租约机制失效,重复投递问题会再次出现。

在迁移 Webhook 投递服务时,如何处理 Phorge 的全局静默配置(phabricator.silent)?

Gorge 无法直接读取 Phorge 的全局静默配置,因此通过让 PHP worker 在全局静默时先将请求标记为 failed 或 silent,Gorge 只查询 status='queued' 的记录,从而避免发送。委托守卫(isDeliveryDelegated)会检查 phabricator.silent,如果开启则返回 false,让 PHP worker 继续处理。

为什么 Gorge 的 Webhook 服务不能像其他服务一样先上线新实例观察再切换流量?

因为 Webhook 服务是共享队列的消费者,如果 PHP worker 和 Gorge 同时运行,它们会从同一张表读取同一行并都发送请求,导致接收方收到重复的 POST。因此必须通过配置开关(如 gorge.webhook.uri)确保只有一方在投递,不能简单灰度。

在迁移 Webhook 服务时,如何保证与 Phorge 原实现的请求兼容性?

请求体必须保持与 Phorge 原实现完全一致的格式,包括两空格缩进、字段顺序和末尾换行,因为 HMAC 签名是基于原始字节计算的。任何格式变化都会导致签名不匹配,接收方会拒绝请求。

Gorge 的 Webhook 服务在数据库初始化时如何处理依赖关系?

Gorge 的 /readyz 只检查数据库连接,但 DSN 中指定的数据库(如 phabricator_herald)可能尚未创建,导致连接失败。修复方法是调整编排依赖:Phorge 只要求 gorge-webhook 已启动(service_started),而不是 ready,这样 Phorge 先创建数据库和表,Gorge 的 /readyz 随后变为 200。

为什么 Gorge 的 Webhook 服务日志中,默认看不到证明重复投递被阻止的日志?

因为证明重复投递被阻止的日志(WEBHOOK_CLAIM_LOST)是 Debug 级别,而仓库没有配置 slog 级别,Go 默认只输出 Info 及以上,所以该日志在默认部署中不可见。而熔断相关的 Warn 日志却可能刷屏,造成日志级别分配不当的问题。

🏷️

标签

➡️

继续阅读