ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WuKongIM v2→v3 迁移中的插件运行时验收与配置决策

WuKongIM v2→v3 迁移中的插件运行时验收与配置决策
  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载

导读

本文以 WuKongIM 仓库中docs/superpowers/reports/2026-09-07-v2-migration-plugin-runtime.md验收记录为骨架,完整还原 v2→v3 迁移过程中对wk.plugin.ai-example插件的运行时验收过程、隔离测试设计,以及"统一采用源节点 1001 配置"这一决策背后的技术推导与实现细节。读完本文,你将理解:插件离线 Receive 执行器节点的选择如何影响回复内容、迁移计划中plugin_configs覆盖策略的语义与校验规则,以及为什么这份验收证据仍不足以打开cutover_ready门禁。

背景:为什么插件运行时验收独立于数据迁移

v2→v3 迁移不只是把表数据搬到新存储。对绑定用户的离线 AI 插件而言,迁移必须同时证明:原插件绑定(PluginUser)在原生 v3 集群中仍能被正确查询、插件仍能被正常调度并产生回复、回复可持久化读取、且在完整重启后历史不丢失。2026-09-07 的这份运行时验收报告(工作树codex/v2-v3-migration),正是围绕这条链路做的一次"隔离组件级"验证。

报告记录了一个关键事实:在验收前,节点本地配置 1001→1、1002→2、1003→3 已被批准(保留了节点本地配置名),但运行时验收暴露了该配置没有解决的问题——回复内容中的名字不统一。这直接推动了后续"统一 source-1001 配置"的决策。

验收设计与输入隔离

验收测试TestMigrationPluginBindingsInNativeThreeNodeCluster(位于 internal/app/migration_plugin_cluster_integration_test.go,integration构建标签下)被刻意设计为只含插件绑定范围的组件夹具,而不是全量源迁移证明:

  • 源码固定钉在a888f89533d0e7d1b2030e06504ca97f1ad891d4;父捕获 digest 为3780b1757dbba3b6e46bf2c750bcaec1c09d7b65d974f586d365e2f893d5a896。
  • 私有组件夹具恰好包含 9 行捕获的 PluginUser 行(每个原节点 1 条主行 + 2 条索引行);所有 3 条主字段 digest 都匹配62ddf9a60db1d99a6c79844ef59b426e94a226295ff32e7cab3d9678d45d4967。集成测试中同样对每行主行字段做 SHA-256 核对(测试代码第 84-88 行)。
  • 原可执行文件 SHA-256 为671b3436d1a8d765371077009b1dfd6dec4528a1ce9cdc0dbebe2cfddc5b3224。测试把这串字节原样复制为wk.plugin.ai-example.wkp:v3 默认扫描器从文件名推导插件标识,旧的-linux-amd64平台后缀不属于注册的插件编号(测试代码第 143-147 行的注释与此一致)。
  • 运行环境是测试主机上的临时 Docker 容器:无外网、只读根、只读输入挂载、无 Linux capabilities、2 CPU 配额、1536 MiB 内存、512 MiB 可执行临时文件系统;三个应用节点仅在回环接口上通信。程序、配置与绑定文件均为私有,报告不含任何真实 UID 或配置值。

组件的工作方式:使用原源快照检查拓扑与 Slot 进度,校验原索引,选择正式副本,转换 1 条逻辑绑定,安装 3 个原生副本,并在启动目标集群前独立校验它们。报告特别强调:组件捕获 digest 刻意区别于全源捕获,不是全源 PREPARED 或 cutover 证书;生产准备流程中的插件注册/配置门禁一个都没有被跳过。

两种配置候选的验收结果

每个配置候选都完整跑完三阶段:直接离线 hook、普通消息发送触发自动 Receive、整集群关闭重启后再验证。

  1. 每轮 6 个直接离线 hook 用例:在三个节点上分别对个人/群组消息调用离线 hook,走真实集群绑定读取与真实提交回复。
  2. 每轮 6 个普通消息发送用例:个人/群组发送进入任一节点都会自动触发离线 Receive,插件回复经正常消息 usecase 返回并可持久化读取。
  3. 完整关停并重启集群:重启后重复同样的绑定/配置检查,并在全新频道上追加 12 个用例。

即每个配置候选共 24 条已提交的插件回复,其中 12 条为提交后的自动回复。测试不声称恢复了早期频道的完整历史——该历史恢复阻塞项被单独记录,仍处于打开状态;网关/SDK 投递、在线客户端状态、故障切换、任意插件方法与生产流量都不在本有界测试范围内。

候选一:保留节点本地配置

原"每节点各自配置"候选在 27.70 秒通过。其 12 条自动回复使用了全部三种配置名:目标配置 1 出现 3 次、配置 2 出现 6 次、配置 3 出现 3 次。即使绑定的是同一个 UID,不同频道也由不同执行器节点处理——这正是下一节要解释的机制差异。

候选二:统一 source-1001 配置

统一候选在 27.39 秒通过,全部 12 条自动回复都匹配源 1001 配置的名字。该候选只改变每个目标的"有效 config"值;每个目标原有的期望状态身份、启用标志与时间戳全部保留,已批准的原分配与原源数据保持不变。

配套单测与两个环境问题

聚焦的应用插件/离线观察者单元测试同样通过。初跑时发现两个环境问题:Docker 默认临时挂载导致暂存文件无法执行、零值应用配置遗漏了 Delivery 设置。通过显式临时执行权限与正常的 product delivery 配置解决,未改动任何服务端运行时或存储代码。

为什么配置决策是实质性的:执行器节点决定回复内容

报告基于钉住的原始源(pinned original sources)给出了这条调用链:

  • internal/manager/manager_tag.go:calcUsersInNode把用户分配到其 UID Slot 的 Leader;
  • internal/channel/handler/event_distribute.go:distributeByTag转发到这些节点,并为接收者排队离线事件;
  • internal/pusher/handler/event_pushoffline.go:pushOffline在该节点上通过processAIPush调用绑定插件。

说明:以上三条路径是文档记录的 v2 原源(pinned original sources)路径,当前仓库为 v3 代码,搜索不到同名文件;v3 中对应的投递与离线机制实现于 internal/runtime/delivery/runtime.go(durable 计划才产生离线效果,见 internal/runtime/delivery/FLOW.md)与 internal/usecase/plugin/receive.go(ReceiveOffline调用绑定插件)。

捕获的绑定用户 UID 哈希到源 Slot 36,其 Leader 是节点 1001。因此在该捕获的、稳定的拓扑下,源 1001 就是预期执行器。文档明确这是"代码与快照推断",不是重放已停机的生产服务器,也不声称每条历史回复都使用同一节点。

PluginUser 存储本身属于 Slot 0,这一独立的存储权威不应与 Receive 执行亲缘性混为一谈。

v3 的投递运行时从执行投递计划的进程发出离线效果(internal/runtime/delivery/runtime.go 中 durable 计划收集离线 UID 并通过OfflineRecipientsObserver通知)。观察到的"混合名字回复"证实了这一差异对该插件的影响——该插件把config.name嵌入回复负载(集成测试第 296-301 行按我是%s,收到您的消息:hello匹配回复者名字)。此前批准的 1001→1、1002→2、1003→3 保留了节点本地配置,但没有解决这个新暴露的回复内容变化问题。

用户随后批准:该插件在全部三个目标上统一使用源 1001 的有效配置。该决策不改变任何 v3 存储或调度逻辑。

统一 source-1001 配置策略的实现与校验

配置策略细节记录在 2026-09-07-v2-migration-plugin-config-policy.md。实现位于 internal/usecase/migration/plugin_settings.go。

计划表示

保留既有plugin_nodes分配 1001→1、1002→2、1003→3,新增:

"plugin_configs": [ {"plugin_no": "wk.plugin.ai-example", "source_node": 1001} ]

对应的 Go 类型是 plugin_settings.go 第 24-29 行 的PluginConfigMapping:只覆盖有效配置,绝不触碰接收节点的注册、启用标志与时间戳。

语义与校验规则(validatePluginNodes)

  • 只有该插件的有效 Config 改变;每个目标保留其原始映射节点的启用状态与精确期望状态时间戳;未列出的插件保留其映射源配置。
  • 选中源与插件必须存在,且插件必须在每个映射源节点上注册;重复、未知、不安全或不完整的选择直接失败;插件设置仍需完整的"源节点→目标节点"双射(plugin_settings.go 第 60-97 行)。
  • prepare 与归档重建都从原始行推导分配;私有记录保留原字段、源行 key/digest 与所选配置的源 key/digest;报告不含任何配置值。
  • 该策略改变计划 digest:旧工作区/归档不能作为新计划的工件复用。
  • import 在发布代次指纹前,调用未改动的原生插件 Store 写入data_dir/plugin-state;精确重试保留文件,变更文件被拒绝(包括无 READY 检查点的中断代次)。
  • 离线验证独立地再次解码原始 Plugin 行并应用计划,不读取转换后的设置:检查 JSON 时避免 float64 舍入,核对启用标志、时间戳、缺失/多余文件与计数;有界目录枚举拒绝符号链接、损坏文件与意外条目;证据在磁盘工作区中有序排列,保证 digest 可复现。
  • CLI 同时接通原生 import 与源派生验证;plugin_settings验证报告携带自身源绑定 digest,其成功从不设置cutover_ready。

PreparePluginSettings(plugin_settings.go 第 104-242 行)从捕获行重建每条记录:读取各源节点 Plugin 主行 → 解码业务事实 → 构造DesiredState→ 以"capture+plan+source node+plugin no"为 key 落盘 → 按映射为目标节点指派 → 对配置覆盖的插件从指定源行取出原始 Config 替换 → 校验覆盖插件必须存在于每个映射源节点 → 计算可复现的 settings digest。

回归验证:13 类故障场景

internal/usecase/migration/plugin_profile_test.go 的TestReceiveProfileRejectsUnverifiedContractChanges用一张表覆盖 13 类故障注入:valid、binary、profile、missing-program、unmapped-program、competition、unapproved-config、source-version、method、priority、version、status、config-key、config-type、legacy-config。除valid外全部必须失败——包括改可执行文件 SHA-256、声明未验证的 profile、删程序、改方法/优先级/版本/状态、给配置加键、把配置类型改成null、用旧版大写Name键等。unapproved-config场景(未提供配置映射)也会失败,证明运行时验收不能绕过配置门禁。

配置策略回归命令(来自策略报告,实测包耗时 usecase 6.449 s、v2 适配器 110.194 s、v3 适配器 11.194 s):

GOWORK=off go test ./internal/usecase/migration ./internal/infra/migrationv2 \ ./internal/infra/migrationv3 ./internal/app/migration \ ./internal/access/migratecli ./cmd/wkmigrate -count=1

真实捕获配置核对

策略报告的plugin-33探针复用了恰好 3 条捕获的原 Plugin 主行,核对全部行 SHA-256 后,用批准的策略生成设置、经原生 Store 往返、再独立于原始行验证。三个有效配置的 SHA-256 均为7cae637b161fc5057487e38b8bc39277b683cb1c3c6b8c366a2a21a97c47616d——与集成测试中source-1001候选把三种配置哈希统一为第一个(7cae…)的做法完全一致(集成测试第 134-141 行)。此外,全部 275 个此前跟踪的原生文件仍与基线ab1ced72450658ba85399b03293ac37c20278721逐字节一致,证明该变更没有引入任何 v3 存储、消息恢复、插件调度或运行时适配改动。

集成测试如何执行两种候选与三轮重启

集成测试通过环境变量选择候选:WKMIGRATE_TEST_PLUGIN_BINARY(受审计程序路径)、WKMIGRATE_TEST_PLUGIN_BINDING_FIXTURE(捕获绑定夹具)、WKMIGRATE_TEST_PLUGIN_STATE_ROOT(分配配置目录)、WKMIGRATE_TEST_PLUGIN_CONFIG_POLICY(仅允许空值或source-1001)。测试先校验程序字节 SHA-256、父捕获 digest 与 9 行夹具,然后执行:源目录构建 → 索引校验 → 源选择 → 目标记录转换(断言恰好 1 条plugin_binding)→ 原生 v3 安装 → 目标验证(断言 3 条绑定且CutoverReady == false)。

每轮循环(共 3 轮,第 3 轮只做历史检查)都会:停止全部节点 → 以相同配置重建并启动 3 个App→ 等待插件注册(pluginhost.Runtime中状态为StatusRunning)与集群绑定查询 →在任何新发送之前通过全部 3 个节点逐条核对此前记录的重启前历史(reflect.DeepEqual比较完整消息字段)→ 再执行新的直接 hook 与普通发送用例。测试明确要求a.onlineDelivery != nil,因为自动 Receive 依赖 product delivery 运行时(测试代码第 206 行);这正是初跑时零值配置遗漏 Delivery 导致失败的根因。

回复断言migrationPluginCommittedReply(第 283-303 行)检查:回复在 10 秒内提交、发送者 UID 等于绑定 UID、负载内容匹配我是<名字>,收到您的消息:hello。source-1001模式下日志记录config_source=1001,否则记录实际的config_node——每条自动回复的执行器节点因此可审计。

2026-09-08 后续:历史恢复验收补齐

针对"重启后只验证新频道新回复"的局限,2026-09-08-v2-migration-plugin-recovery.md 补齐了单节点与三节点集群各两次完整重启的验收,使用原程序字节、原捕获绑定组件、批准的源 1001 统一配置,未改变任何 v3 存储或恢复逻辑:

拓扑自动回复直接 hook 回复重启前历史核对耗时
三节点集群1212第一次重启 12 个频道,第二次 24 个;每个经全部 3 个入口核对33.68 秒
单节点集群44第一次重启 4 个频道,第二次 8 个22.09 秒

两种拓扑合计 32 条插件回复、120 次重启后完整历史读取,全部通过;整个容器实际运行 56.454 秒。测试不用追加消息预热频道,不把"最终能读到"当作第一份成功结果的完整性。第一次启动受镜像默认 UID 与私有输入文件归属影响,失败日志保留;纠正仅该临时输入目录的归属后,在原隔离限制下重跑通过,未改动产品代码。私有证据位于tmp/server-rehearsal-20260908/plugin-recovery-41/。

门禁边界:这份证据认证了什么,又没认证什么

原运行时验收证据位于tmp/server-rehearsal-20260907/plugin-32/(绑定夹具、测试二进制与源码、两种候选输入、完整运行日志、源码摘录/digest、校验和、最终主机状态)。

明确未认证(cutover_ready仍为 false):

  • 没有导入、启动、停止或重配置任何源备份或线上目标;测试容器已移除。
  • 全源插件 prepare、最终可执行文件与配置安装、剩余源业务兼容性缺口、历史恢复、SDK 缓存过渡、100 GiB 性能验收均未认证。
  • 配置策略报告同样声明:活动插件的程序归档、正式安装及兼容映射尚未接入完整 prepare/import;网关与实际业务 SDK、在线客户端、故障切主与生产流量未被覆盖;消息格式缺口(含 RedDot 与保留的 stream-main 字段)、历史会话可见性、独立复现的原生历史恢复问题、SDK 缓存切换与 100 GiB/四小时验收仍是独立未决项。
  • 配置夹具是组件/描述符级测试,不证明真实活动源插件已通过完整 prepare 门禁;plugin_settings验证报告的成功从不设置cutover_ready。

这份验收链条的意义在于:它把"绑定可查询、插件可运行、回复可持久化、重启后历史完整"这四个运行时事实,与"配置策略只影响 config、不碰存储与调度"这一变更边界分别钉死在证据上,从而为后续把插件 prepare 接入生产迁移工作流提供了可复现的组件级基线——但正式插件兼容检查仍保持关闭。

  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载
上一篇:如何搭建家庭游戏串流服务器:Sunshine完整指南
下一篇:Sunshine游戏串流终极指南:免费搭建家庭游戏共享平台

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进