news 2026/11/1 11:35:38

WuKongIM 禁发与 Quorum 拒绝的无投递验证:真实三节点 E2E 投递边界证明

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WuKongIM 禁发与 Quorum 拒绝的无投递验证:真实三节点 E2E 投递边界证明
  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

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

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

本文基于 WuKongIM 仓库中的真实 E2E 测试报告 2026-09-24-send-ban-delivery.md 展开,结合 send_ban_test.go 的源码实现,完整解读"被拒绝的发送请求绝不能产生任何投递"这一核心承诺:从用户禁发 / 频道禁发、到 Slot 失去 quorum 时的失败关闭,以及解禁 / 恢复后的精确历史对照。读完本文,你将掌握该 E2E 用例的拓扑构建、断言矩阵、运行命令与底层权限检查实现,可直接在自己的 WuKongIM 三节点集群上复现同样的验证。

一、为什么需要"无投递验证"

消息系统的权限拒绝通常只验证"请求被拒绝"这一面(返回错误码),但更隐蔽的风险在于:被拒绝的请求是否仍然泄漏到了接收端或历史存储。一旦出现"被禁发的消息仍然被对方收到"或"被 503 拒绝的消息仍然写入历史"的情况,就构成数据完整性与合规性的严重缺陷。

本报告对应的测试 TestRejectedSendHasNoDelivery 正是针对这一风险设计的黑盒 E2E 证明:它要求所有被禁发 / 被 quorum 拒绝的请求必须同时满足——

  • 响应中不带任何消息 ID 与消息序号(MessageID == 0 && MessageSeq == 0);
  • 接收端在有界空读窗口内收不到任何 RECV 包;
  • 解禁后旧拒绝消息不得排在成功消息前面;
  • 完整历史中只包含成功的请求身份,一条被拒请求都不许出现。

该测试的状态为:真实三节点黑盒 E2E 通过,耗时 44.86 秒,且没有修改任何生产代码。这意味着它运行在未打补丁的生产二进制之上,证据只来自公开 API、公开指标与真实进程行为。

二、测试环境与拓扑:公开 Placement 定位 Slot Owner

测试不是随机挑选节点,而是先通过公开拓扑信息确定发送者 UID 所属 Hash Slot 的真实 Owner,再把发送、接收连接都建立在 Owner 上。关键环境参数如下(见 send_ban_test.go):

参数值说明
WK_CLUSTER_HASH_SLOT_COUNT256逻辑 Hash Slot 总数
WK_CLUSTER_INITIAL_SLOT_COUNT12初始物理 Slot 数
副本数3消息数据三副本
WK_MESSAGE_PERMISSION_CACHE_TTL1h辅助权限缓存 TTL
WK_GATEWAY_TOKEN_AUTH_ONfalse测试期间关闭 token 认证
拓扑确认Manager HTTPMustSlots通过公开 placement 定位 UID Slot Leader

源码中通过crc32.ChecksumIEEE([]byte(sender)) % 256计算发送者 UID 的 Hash Slot,再遍历公开 Slot 列表找到该 Hash Slot 的Runtime.LeaderID,从而确定 Owner 节点:

hashSlot := uint16(crc32.ChecksumIEEE([]byte(sender)) % 256) var ownerID uint64 for _, slot := range cluster.ManagerClient(t, 1).MustSlots(t) { if slot.HashSlots != nil { for _, h := range slot.HashSlots.Items { if h == hashSlot { ownerID = slot.Runtime.LeaderID } } } }

发送者与接收者的 WKProto 连接都在禁发建立之前完成,并全程保持在 Owner 节点上——这排除了"连接迁移导致的偶发缺投"这一干扰变量,让后续的禁发拒绝与 quorum 失败都是纯粹的权限 / 一致性行为。

三、禁发场景验证矩阵:48 次明确拒绝

测试将"用户禁发"与"频道禁发"两种作用域分别展开,形成一个完整的矩阵(源码见 send_ban_test.go):

  • 两种作用域:用户禁发(SetUserSendBan)、频道禁发(SetChannelSendBan);
  • 两种频道类型:私聊(channel_type=1,person)、群聊(channel_type=2,group);
  • 两种协议入口:HTTP 三个接入节点 + 既有 WKProto 连接;
  • 三种消息形态:普通消息、NoPersist(不持久化)、SyncOnce(只同步 / 消费一次)。

组合起来即2 作用域 × 2 类型 × 3 形态 × 4 入口 = 48 次明确禁发拒绝。每一次拒绝断言都必须是:

require.Equal(t, want, ack.ReasonCode, label) if want != frame.ReasonSuccess { require.Zero(t, ack.MessageID) require.Zero(t, ack.MessageSeq) }

即拒绝码为frame.ReasonSendBan(定义见 common.go:ReasonSendBan // 用户或目标频道禁止发送),且响应中消息 ID 与序号必须为零。

禁发期间的"有界空读窗口"

每个作用域禁发执行完毕后,测试在接收端执行一次有界空读:ReadRecv等待 2 秒,必须返回context.DeadlineExceeded且没有收到任何包:

quiet := func(label string) { packet, err := recv.ReadRecv() require.ErrorIs(t, err, context.DeadlineExceeded, label) require.Nil(t, packet) }

四个空读窗口分别覆盖:用户禁发后(user-no-delivery)、频道禁发后(channel-no-delivery)、quorum 丢失后(quorum-no-delivery)、最终收尾(final-no-delivery)。这正是"无投递"的直接黑盒证据:不只是响应里没有 ID/序号,接收端的真实连接上也确实收不到任何 RECV 包。

解禁后的精确对照

每次解禁后,测试通过两种协议再次发送成功对照,并精确断言接收端收到的正是当前这次成功请求,禁止旧拒绝消息排在前面:

packet, err := recv.ReadRecv() require.NoError(t, err, label) require.Equal(t, label, string(packet.Payload), "a rejected packet must not leak ahead of the positive control") require.Equal(t, sender, packet.FromUID)

这一断言非常关键:它证明被拒绝的消息既没有"插队"出现在成功消息之前,也没有以任何形式残留在接收队列里。

四、Quorum 拒绝验证:失败关闭与恢复

禁发之外,测试还覆盖了更严苛的一致性场景——UID Slot 失去 quorum 时的失败关闭:

  1. 保留接收连接所在进程存活,停止另两个节点进程,让 UID Slot 失去 quorum(源码见 send_ban_test.go);
  2. 通过 Owner 节点发起一条 HTTP SEND,必须返回HTTP 503,且响应中无消息 ID / 序号;
  3. 接收端再次执行空读,确认仍为空;
  4. 重启节点,等待公开 HTTP readiness(WaitHTTPReady)与实际 Slot Leader 稳定(WaitSlotLeadersStable)之后,原接收连接收到成功对照;
  5. 最后再执行一次空读收尾。

quorum 拒绝的直接断言如下:

require.ErrorAs(t, err, &statusErr) require.Equal(t, http.StatusServiceUnavailable, statusErr.StatusCode) require.Zero(t, got.MessageID) require.Zero(t, got.MessageSeq)

这条 quorum SEND 的 503 / 零 ID / 序号由用例直接断言,而随后的空读与恢复后的精确历史又进一步排除了它的任何副作用——即使它"看似失败",也必须确凿地没有任何投递与持久化痕迹。

五、产物统计:70 条观察的完整证据链

整个测试共产生70 条观察记录,全部写入WK_E2E_SEND_BAN_REPORT指定的 JSON 产物(本用例写为<路径>.delivery.json,见 send_ban_test.go):

观察类别数量说明
明确禁发拒绝48上述 2×2×3×4 矩阵,均零 ID/序号
已发送且已接收的成功对照16warm / 用户解禁 / 频道解禁 / quorum 恢复各 4 条
有界空读窗口4四个 2 秒无 RECV 窗口
完整历史2私聊与群历史各严格包含 8 条成功请求

其中"完整历史各 8 条成功请求"的由来:4 次controls(warm、user-recovered、channel-recovered、quorum-recovered)每次对每个 kind 产生 HTTP + WKProto 两条成功,因此 kind=1(私聊)与 kind=2(群)各有 8 条。测试通过messagesync拉取完整历史并断言ElementsMatch,只允许这 8 个成功身份出现。

测试报告同时明确了证据边界:该测试只对观测窗口负责,不声称"有限等待能证明任意未来时刻永不产生异常投递"——这是对黑盒测试能力边界的诚实声明,也是后续任意时间点回归验证的前提。

六、运行方式与复现命令

测试属于 e2e build tag,需要真实进程。完整命令如下(与报告一致):

WK_E2E_SEND_BAN_REPORT=/tmp/send-ban-delivery.json GOWORK=off go test -tags=e2e ./test/e2e/message/send_ban -run '^TestRejectedSendHasNoDelivery$' -count=1 -timeout=3m -p=1 -v

关键参数说明:

  • WK_E2E_SEND_BAN_REPORT:JSON 产物输出路径,不设置时默认写入系统临时目录下的wukongim-send-ban-report.json;
  • GOWORK=off:关闭 Go workspace 模式,保证只编译仓库自身模块;
  • -tags=e2e:启用 e2e 构建标签(源码文件头部即//go:build e2e);
  • -run '^TestRejectedSendHasNoDelivery$':精确选中无投递验证用例;
  • -count=1:禁止测试缓存;
  • -p=1:串行执行,避免多进程干扰。

报告记录的运行环境为 Go 1.25.11 / Darwin arm64;测试源码基于提交2f49a2869加未提交测试,而生产二进制输入仍对应1ba4a6352——即被测的是仓库既有生产代码。前一持续压力运行已在编译执行前终止,确保观测窗口干净。更完整的包级说明见 send_ban 目录 AGENTS.md,其中要求:保持黑盒断言、覆盖缓存准入、跨接入点变更、私聊双向、群隔离、CAS 与拒绝消息缺失,且"被禁发的拒绝绝不能重试直到成功"。

相关配置项取值含义

上述环境变量即 WuKongIM 的运行时配置项,在 suite 中通过WithNodeConfigOverrides注入:

  • WK_CLUSTER_HASH_SLOT_COUNT=256:Hash Slot 总数,决定 UID / 频道在集群中的分布粒度;
  • WK_CLUSTER_INITIAL_SLOT_COUNT=12:初始物理 Slot 数,每个物理 Slot 承载一组 Hash Slot;
  • WK_MESSAGE_PERMISSION_CACHE_TTL=1h:辅助权限缓存 TTL,测试中固定为 1 小时,避免缓存过期引入的时序抖动;
  • WK_GATEWAY_TOKEN_AUTH_ON=false:关闭 token 认证,把验证焦点完全放在权限禁发本身。

七、源码级原理:权限检查链路与事实计划

"无投递"之所以成立,底层依赖发送路径上严格的权限前置检查。核心实现在 internal/usecase/message/permission.go:

  1. 入口:checkSendPermission对所有 SEND 请求先执行用户级检查checkSenderSendPermission——通过GetUserSendPolicy读取 UID 的禁发策略,非零即observeSendBan("user", 1)并返回ReasonSendBan(见 permission.go);
  2. 频道级检查:checkTerminalChannelPermission/checkGroupSendPermission读取源频道的元数据,SendBan != 0时返回ReasonSendBan(见 permission.go);
  3. 拒绝即终止:只要返回ReasonSendBan,发送流程在进入消息 ID 分配、序号生成、持久化与 fanout 之前就已终止,因此拒绝响应天然是零 ID / 零序号,接收端也绝不会得到投递包。

在批量发送路径上,权限读取被组织为固定的事实计划(fact plan):checkSendPermissionsBatch把用户策略、源频道与辅助集合事实收集进一个 plan 中统一读取(见 permission_batch.go),并公开wukongim_message_permission_counts_total系列指标(facts_before/facts/messages/users/channels/node_envelopes/slot_groups)。这正是配套测试(如 TestSendBanGatewayDistributions)用来证明"拒绝请求不会走接收者遍历"的公开证据:10 万人大群的拒绝场景中,facts增量固定、fanout 增量为零。

管理面写入接口同样公开可查:用户禁发走POST /user/send_ban,频道禁发走POST /channel/send_ban,请求体为{"uid": ..., "send_ban": value}与{"channel_id": ..., "channel_type": ..., "send_ban": value},响应带版本号send_ban_version(封装见 test/e2e/suite/send_ban.go)。send_ban取值为1(禁发)与0(解禁),管理用例还覆盖了 CAS 冲突(带错误expected_version返回 HTTP 409)与非法参数(HTTP 400)等边界。

八、结论与边界声明

WuKongIM 的TestRejectedSendHasNoDelivery用一次真实三节点黑盒 E2E(44.86 秒、零生产代码改动)完成了对"禁发与 quorum 拒绝无投递"的完整证明:48 次禁发拒绝全部零 ID/序号、四个 2 秒空读窗口无任何 RECV、解禁后成功消息精确排位、quorum 丢失时 HTTP 503 失败关闭、恢复后私聊与群历史各严格等于 8 条成功请求。

需要强调的是,该测试的证据边界同样明确:有限等待窗口的观察只证明观测期间无异常投递,并不声称任意未来时刻永不产生异常投递。如需在任何新版本、新拓扑或新配置上维持这一承诺,应原样重跑该用例——命令与断言矩阵已全部沉淀在仓库中,可随时复现。

  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

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

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

相关推荐

上一篇:dnSpyEx实战指南:掌握.NET逆向调试与程序集编辑的完整解决方案
下一篇:3步搞定人声分离:RVC WebUI免费AI工具的终极实战指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 13:20:58

火山引擎图像生成MaaS服务层工程优化实践

1. 这不是“选哪家”的问题&#xff0c;而是“怎么用对”的问题最近在多个技术交流群和行业论坛里&#xff0c;频繁看到类似提问&#xff1a;“图像生成模型推荐哪家&#xff1f;”——语气里带着点急切&#xff0c;甚至有点焦虑。好像只要选对了平台&#xff0c;就能立刻跑通业…

作者头像 李华
网站建设 2026/10/26 7:26:02

基于Spark和Django的老年人健康数据分析系统设计与实现

1. 项目概述与选题价值这个选题完美结合了当前最热门的三大技术方向&#xff1a;大数据处理&#xff08;Spark&#xff09;、健康医疗数据分析&#xff08;老龄化研究&#xff09;和Web应用开发&#xff08;Django&#xff09;。作为指导过上百个大数据毕业设计的导师&#xff…

作者头像 李华
网站建设 2026/10/25 17:48:02

Windows下安装Python 3.14完整指南:从下载到配置虚拟环境

前阵子把Windows开发机从头到尾整理了一遍&#xff0c;顺手把手上的Python升级到了3.14。版本号听起来挺新&#xff0c;但安装过程本身不算复杂&#xff0c;真正花时间的是一些边角问题&#xff1a;下载链接选哪个、PATH勾选后为什么还是调不动、新旧版本怎么共存。这篇不只是记…

作者头像 李华
网站建设 2026/10/25 23:24:10

Python plistlib模块:高效处理苹果配置文件的终极指南

1. 初识 plistlib&#xff1a;Python 处理苹果配置文件的瑞士军刀第一次在 Mac 上看到.plist文件时&#xff0c;我完全不明白这种带有 XML 结构的文件有什么用。直到需要批量修改 200 多台 Mac 设备的统一配置时&#xff0c;才发现 plistlib 这个宝藏模块。它就像 Python 与苹果…

作者头像 李华
网站建设 2026/10/31 15:14:22

文件包含漏洞实战:从低到高级绕过与木马植入全解析

文件包含漏洞这几年在各类安全测试里出现的频率一直不低&#xff0c;很多入门的朋友问我&#xff1a;明明代码里已经把用户输入带进了include&#xff0c;为什么有的站能直接 getshell&#xff0c;有的站折腾半天只能读到个源码&#xff1f;其实答案往往就藏在“漏洞利用难度分…

作者头像 李华
网站建设 2026/10/26 4:46:54

零基础学Python:从刻意练习到动手写代码的完整路线

1. 项目概述&#xff1a;为什么“笨办法”反而是学Python的捷径这几年总有人拿着《笨办法学Python》的标题来问我&#xff1a;现在网课、视频、AI辅助工具都这么多了&#xff0c;干嘛还要一本本啃习题、一行行敲代码&#xff1f;我的回答一直很直接&#xff1a;正因为工具太多、…

作者头像 李华