WuKongIM MQTT 精确所有者会话终止机制解析:隔离证明、原子 Will 解析与安全清理
发布时间:2026/10/15 2:49:56 锦皓数字建站

即时通讯后端【免费下载链接】WuKongIMMore than just IM 不只是即时通讯(IM)项目地址https://gitcode.com/gh_mirrors/wu/WuKongIM点击查看免费下载导读本文基于 WuKongIM 仓库中的docs/specs/mqtt-session-ending.md规格深入解析 MQTT Session 的Exact-owner精确所有者结束机制它如何在“先证明执行静止、再原子结束”的前提下安全地终结一个会话并使遗嘱Will就绪同时绝不错杀后继连接、绝不丢失投递债务。读者将掌握App.End的调用契约与结束原因枚举、quiescence 隔离证明的底层实现、时钟纪律与失败语义以及子项回收child reclamation与 End 之间的边界划分。一、Exact-owner Session ending 在生命周期中的定位在 WuKongIM 的 MQTT Session 生命周期模型中internal/usecase/mqttsession/FLOW.md将完整的生命周期编排为CONNECT 获取与接管隔离 → 租约续期Renew→ 断开记录Disconnect→精确所有者 End→ 子项回收SessionReclamation→ 订阅建立/移除、投递与 ACK 等。其中End承担的是“由应用方主动终结会话”这一职责与客户端断开DISCONNECT 报文、租约自然过期lease expiry在语义上严格区分断开记录Disconnect由连接侧发起保留 Will/过期时钟与正常意图过期Expired由执行截止驱动属于异常的被动结局End 是可信调用方应用/维护者基于明确业务决策发起的主动终结。从 FLOW.md 主流程 5 可以看到Endrequires exact isolation even for ended rows, then atomically ends Session/Will while retaining delivery/source debt.这句话浓缩了本规格的全部核心即使持久化行已经是 EndedEnd 依然要求精确隔离End 原子结束会话与 Will但保留投递与源债务。二、App.End 的输入契约可信决策而非客户端报文End 的入口定义在 internal/usecase/mqttsession/end.go// EndCommand carries a trusted application decision, never a client-supplied // reason. The caller establishes definitive denial/loss and releases every owner // operation before End; unavailable authority is not a revocation decision. type EndCommand struct { Owner contract.Owner Reason meta.MQTTSessionEndReason }两个字段都有严格的语义约束字段约束说明Owner必须是当前可信的所有者身份携带 namespace/ClientID、SessionGeneration、OwnerGeneration、NodeID、BootID、ConnectionID 完整元组任何一项不匹配都会导致隔离失败Reason只能是四种显式应用原因之一Revoked撤销、SourceLost源丢失、Quota配额、Explicit管理性/清理触发结束原因枚举六种持久化原因结束原因的完整集合定义在 pkg/db/meta/mqtt_session.goconst ( MQTTSessionExpired MQTTSessionEndReason 1 // 过期由断开/到期自动产生 MQTTSessionQuota MQTTSessionEndReason 2 // 配额终结 MQTTSessionRevoked MQTTSessionEndReason 3 // 撤销如权限吊销 MQTTSessionCleanStart MQTTSessionEndReason 4 // Clean Start 语义产生的新生命周期 MQTTSessionExplicit MQTTSessionEndReason 5 // 显式管理性终结 MQTTSessionSourceLost MQTTSessionEndReason 6 // 数据源丢失 )关键事实源码可证End只接受Revoked / SourceLost / Quota / Explicit四种原因Expired和CleanStart不通过 End 入口产生CleanStart作为结束原因是由 CONNECT 生命周期转换产生的见validMQTTLifecycleSession的MQTTLifecycleConnect分支第一个持久化的结束原因被保留源码中if row.State meta.MQTTSessionEnded { return nil }end.go意味着对已 Ended 行再次调用 End 是幂等的、直接返回绝不覆盖原有原因结束原因不是 MQTT 线上的状态码而是持久化的应用层原因MQTTSessionEndReason is a durable application reason, not an MQTT wire code。为什么“不是客户端报文操作”规格原文强调 End “is not a client packet operation or proof that permission was denied”。结合 types.go 中的错误定义可以看出一组关键区分ErrWillDenied errors.New(mqttsession: Will publication not authorized)ErrWillDenied专门用于表达当前发布策略明确拒绝而 End 的使用场景是调用方已经确认了终结决策撤销、源丢失、配额或管理动作不可用的授权unavailable authority绝不能当作撤销决策来执行。也就是说“权限不可用”与“权限被明确拒绝”是两种不同的失败前者不能驱动 End后者才会。三、End 的执行协议读取 → 隔离证明 → 重读App.End的实现end.go遵循一条严格的“两阶段读取 隔离 再读取”协议1. now() 获取单调时钟作为 observation 2. readEndingSession(ctx, owner) # 第一次读取完整点读当前所有者 3. 时钟检查observed row.UpdatedAtMS否则 ErrClock 4. 记住 uid : row.UID 5. Isolation.Quiesce(ctx, owner) # 隔离证明物理关闭 已准入效果排空 6. readEndingSession(ctx, owner) # 第二次读取同所有者重读 7. 校验 row.UID uid否则 ErrEvidence 8. 若为“已过期活动租约”先记录原始断开再重读校验 revision1 9. 若已是 Ended 状态直接返回保留首次原因与分离的 Will 工作 10. 构造 next 行StateEnded、TerminationReason原因、租约清空 以 MQTTLifecycleEnd 事件提交 11. 校验提交回执 r.WillGeneration 0否则 ErrEvidence这段协议的每一步都对应规格中的一条硬性要求协议步骤规格要求违规后果首次读取读取当前授权而不是陈旧缓存陈旧身份会让后续隔离错误地作用到新所有者隔离证明证明精确执行/传输静止quiescence未排空就提交会误判隔离二次重读同一所有者、同一 UID所有者变更 →ErrEvidence不跟随也不终止后继者已 Ended 行仍需隔离证明配额清理必须在会计事务结束后仍能停止旧所有者提交回执WillGeneration 必须为 0说明提交不是原子地完成了 Will 解析属于异常readEndingSession只接受“完整当前点读”readEndingSession 对读取结果做了严格限定必须r.Done true不允许任何额外的 After 游标、Accounting、SourceOwners、Sessions、Subscriptions、DeliveryCursors、Inflight、Bindings、Wills 混杂Session 必须存在否则ErrFenced对行做ValidateMQTTSession校验Revision 不能是MaxUint64耗尽修订Ended 行不允许携带活 WillGenerations.State MQTTSessionEnded s.WillGeneration ! 0视为证据异常sessionOwner(s)必须精确等于调用者传入的 Owner否则ErrFenced。这个函数体现的正是规格中“missing/foreign registries do not replace isolation”缺失/外来注册表不能替代隔离的要求即使行不存在或归属他人也不会被当作“已经隔离”的证明只会明确失败。四、隔离证明的底层实现Quiesce 与不确定效果End 的第二阶段依赖Isolation接口types.go// Isolation proves exact execution/transport isolation, never merely absence, // a stored state, clock expiry, RPC failure or a different process boot. type Isolation interface { Quiesce(context.Context, contract.Owner) error }在 internal/runtime/mqttsession/owner.go 中Owners.Quiesce定义了“什么才算真正的隔离证明”先 Fence永久关闭准入、取消已准入作用域但这本身不提供任何隔离收据Fence ... supplies no isolation receipt物理传输关闭调用CloseTransport回调必须“封存所有未来 socket 写入并物理关闭连接”后才算完成已准入操作排空等待所有 admitted Operation 的Done不确定效果屏障任何操作曾调用MarkUncertain()依赖可能仍在异步执行、无法证明已结束则 Quiesce 最终返回ErrOwnerUnknown——即internal/runtime/mqttsession/owner.go中ErrOwnerUnknown errors.New(mqttsession: owner isolation unproved)。func (m *Owners) awaitQuiescence(ctx context.Context, e *ownerEntry, done -chan struct{}) error { if err : awaitOwner(ctx, done); err ! nil { return err } m.mu.Lock() defer m.mu.Unlock() if e.uncertain { return ErrOwnerUnknown } return nil }这一实现直接支撑了规格中的两条核心断言Ended/offline 状态、租约过期、缺失/外来注册表都不能替代隔离Quiesce 只看本地注册表的物理关闭与排空结果从不读取持久化行状态作判断一个已结束的所有者仍然需要 quiescence这正是配额终结清理quota-ending cleanup能在“会计事务已经把 Session 标记为 Ended”之后依然能安全停止旧所有者的原因——持久化状态永远不等于执行已停止。此外internal/runtime/mqttsession/retirement.go 将当前注册表与已证明的旧 boot 退休记录组合成Isolation对“外来 boot”的隔离只委托给显式持久化证明绝不降级为“查不到就当作成功”。五、原子结束一个生命周期命令同时终结 Session 与 WillEnd 的提交使用meta.MQTTLifecycleEnd事件pkg/db/meta/mqtt_lifecycle.go中MQTTLifecycleEnd MQTTLifecycleEvent 5通过 ApplyMQTTLifecycle 在一个 Slot 批次内完成两件事替换 Session 行validMQTTLifecycleSession的MQTTLifecycleEnd分支要求目标行为 Ended 状态、租约与离线过期时间清零、终止原因与提案一致且旧行不能已经是 Endedif old.State MQTTSessionEnded { return false }解析活的 WillresolveMQTTLifecycleWill的MQTTLifecycleEnd分支把 Armed/Waiting 的 Will 置为Readycase MQTTLifecycleEnd: mqttLifecycleWillReady(r, at)mqttLifecycleWillReadymqtt_lifecycle.go会把DisconnectedAtMS和DueAtMS都固定到结束时刻若已处于 Waiting 则取min使 Will 立即进入可发布状态。Will 阶段与“End 不发布 Will”的边界Will 的阶段定义在 pkg/db/meta/mqtt_will.goArmed(1) → Waiting(2) → Ready(3) → Executing(4) → Published(5) ↘ Cancelled(6) / Rejected(7)End 只负责把 Will 从 Armed/Waiting 推进到Ready就绪绝不直接发布不进入 Executing。发布由独立的 Will 执行器WillExecutor在后续调度中完成——这正是规格“End does not publish a Will”的落点。同样重要的是分离detached的 Will 工作被保留。已经 detach 的 Ready/执行中的 Will 行不通过 Session 的WillGeneration引用见loadMQTTReferencedWill的注释“Detached Ready/execution rows do not use this reference”因此 End 不会触碰、取消或复活它们。六、时钟纪律观察先于隔离租约过期先记录原始断开规格中专门强调“Capture the observation before isolation. A concurrent admitted write may advance the timestamp; drain completion cannot restart that observation.”先于隔离捕获观察并发准入写入可能推进时间戳排空完成不能重启该观察。对应源码end.goobserved在任何隔离动作之前由a.now()取得隔离期间并发准入写入可能把UpdatedAtMS推高所以 End 不会把隔离完成时刻当作观察——它保持第一次读取时的observed若row.State Active observed row.LeaseUntilMS活动租约在观察时刻已过期End 先调用disconnectRow以row.LeaseUntilMS作为断开时刻记录原始异常断开然后重读并校验Revision 期望1 State ! Active再继续 End。这是规格所说的“有界两命令案例”bounded two-command case它保证了原始 Will 的断开时钟DisconnectedAtMS和到期时钟DueAtMS不会被重启为“观察完成时刻”或“隔离完成时刻”。lifecycle.go 的 disconnectRow 展示了断开行的构造UpdatedAtMS at使用租约截止而非当前时间、StateOffline、OfflineExpiresAtMS at expiry*1000若 expiry 为 0 则直接进入 Ended 并记录MQTTSessionExpired。这与规格“An expired active owner records the original abnormal disconnect first”完全一致。时钟校验贯穿全程if now.Before(observed) || now.UnixMilli() observed.UnixMilli() || now.UnixMilli() row.UpdatedAtMS { return ErrClock }任何回归regressed、非单调non-monotonic或墙钟整点值t t.Round(0)见 lifecycle.go 的 now()都会导致ErrClock——本地单调时间是隔离与 Will 时钟正确性的前提但本地时间从来不是远程隔离证明。七、失败清单每一种“貌似成功”都必须失败规格文档给出了实现前的完整故障清单Failure inventory before implementation。结合源码这些故障都被映射为显式错误故障场景源码语义位置End 在效果排空前提交 / 无物理关闭证明Quiesce失败 →ErrOwnerClose/ErrOwnerUnknownowner.go已 Ended/offline 行被误认为隔离Ended 行仍走 Quiesce读取异常 →ErrFenced/ErrEvidenceend.go陈旧身份 / 隔离期间接管 / 并发提案结束新所有者二次重读 UID 不匹配 →ErrEvidenceowner 不匹配 →ErrFencedend.go重试覆盖先前原因已 Ended 行直接返回原因保留end.go两次解决 Will提交回执WillGeneration ! 0→ErrEvidenceend.go无效原因 / 所有者 / 上下文Owner.Validate()失败或原因不在四种之内 →ErrInvalidend.go畸形 / 外来 / 部分授权readEndingSession 严格校验 →ErrEvidence/ErrFencedend.go耗尽修订MaxUint64校验拒绝 →ErrEvidenceend.go回归 / 非单调时间→ErrClocklifecycle.go取消依赖 / 回调 panicctx.Err()检查panic 收敛为ErrLifecycleCallback不泄漏内部细节end.go丢失 / 冲突 / 畸形提交commit校验回执 →ErrConflict/ErrEvidencelifecycle.go其中值得注意的两点“丢失中间回复”不允许无证据重写规格要求“a lost intermediate reply triggers another write without fresh evidence”必须失败。End 不是重试循环——一次调用要么成功提交要么返回错误交由调用方基于权威状态决定是否重试规格明确“No retry loop, table, schema or RPC change is introduced”不引入重试循环、新表、新 schema 或新 RPC。回调 panic 的收敛defer func() { if recover() ! nil { err ErrLifecycleCallback } }()将依赖 panic 收敛为不带敏感细节的通用错误与 runtime 层的诊断约定“Panic/error diagnostics contain no callback text”一致。测试矩阵每种故障都必须失败且不留副作用end_test.go 的TestEndSessionFailureNeverAuthorizesAnotherEffect覆盖了 20 种故障注入invalid_reason、canceled、missing、foreign、partial、exhausted、clock、wall_clock、unproved、successor、cancel_read、cancel_isolation、panic_read、panic_isolation、lost_reply、bad_receipt、live_will_receipt、conflict、panic_commit、cancel_commit并断言每次调用都返回错误错误信息不包含 “secret” 之类的内部细节即使提交已发生committed行状态正确进入 Ended 且 revision 严格 1随后重复 End 幂等接管successor场景下新所有者的行保持 Active 且 OwnerGeneration 1——后继者绝不被 End 终止。八、重连语义Presentfalse、新生命周期、绑定保留End 之后的重新连接行为由 CONNECT 生命周期转换定义。规格要求Connecting again gives Session Presentfalse and a new lifetime, while retaining the ClientID-to-UID binding.Session PresentfalseEnded 行的世代Generation已封存新 CONNECT 属于全新生命周期validMQTTLifecycleSession的 Connect 分支要求新行的Generation old.Generation1因此返回给客户端的SessionPresentfalseClientID→UID 绑定保留MQTTSession行的 UID 字段“stays bound across expiry and Clean Start”validMQTTSessionTransition强制old.UID ! next.UID为非法转换mqtt_session.go普通生命周期写入无法重绑 ClientID 到新 UID。测试TestEndSessionWaitsForOwnerThenResolvesWillAndPreventsResumeend_test.go中使用不同 UID 的 CONNECT 得到ErrBinding正是这一约束的验证旧 Owner 的 End 对新连接无效测试证明对新 Owner 调用 End 返回ErrFenced旧 Owner 身份已被世代围栏fence封死。投递债务的保留规格明确“Delivery counters, allocators, children and source bindings remain until their separately verified cleanup.”投递计数器、分配器、子项与源绑定保留到各自独立验证的清理完成。TestEndSessionRetainsUnfinishedDeliveryResponsibility 直接验证End 之后PendingMessages、PendingBytes、OutboundInflight、NextPacketID、NextDeliveryOrder全部保持不变Inflight 交换行和投递游标原封不动。这是因为 End 的生命周期提交只修改 Session 行的状态字段而投递计数与分配器由独立的窗口/投递命令维护validMQTTSessionTransition明确“Delivery commands own counters and allocators within a session lifetime”End 转换不允许这些字段变动。九、与子项回收的边界reclamation 事务保留隔离边界规格把子项清理明确指向 mqtt-session-reclamation.md 描述的独立事务The separate child reclamation transaction now preserves that boundary; automatic discovery and isolation orchestration remain required.两者职责分离边界清晰职责EndSessionReclamation结束会话 / 解析 Will✅ 原子完成❌ 不触碰证明所有者隔离✅ 严格要求即使行已 Ended❌ 复用 End 的隔离删除旧订阅意图、投递游标、inflight 行、记账收据❌ 保留✅ 分页删除保留 UID 绑定、世代围栏、分配器、结束原因、Will 引用✅✅删除源/UID 绑定、分离的 Will、共享消息内容❌ 绝不做❌ 绝不做在 reclamation.go 的Reconcile中可以看到关键组合逻辑若当前 Ended 代即目标边界则调用现有精确所有者 EndEnder.End(ctx, EndCommand{..., Reason: meta.MQTTSessionExplicit})原因使用Explicit作为可信清理触发器但绝不覆盖存储中的原始结束原因随后重读校验同一 Owner、同一 UID、Ended 状态再提交至多一页的旧子项清理若边界属于已被替换的生命周期replaced lifetime则“never quiesces or follows the live successor”——旧生命周期清理绝不打扰正在执行的新后继者。这与 End 规格中的两条原则互为印证隔离只由 End及其隔离端口证明元数据清理结果从来不是隔离或共享内容 GC 的证明。十、冻结上下文与实现边界规格文档附带了“冻结上下文”Frozen context源提交5e6e1819c5cd9cc434069b43ae2f1d0aa1a66678以及AGENTS.md、internal/usecase/mqttsession/FLOW.md、internal/app/FLOW.md、internal/contracts/mqttsession/FLOW.md、internal/runtime/mqttsession/FLOW.md、pkg/db/FLOW.md、pkg/db/meta/FLOW.md的 SHA-256 哈希。这意味着本文描述的行为、接口与测试都是对冻结版本的实际解读。同时规格明确指出了尚未包含在工作范围内的部分产品进程级接受product process acceptance真实的 Paho/WKProto 进程级场景、发送时刻撤销send-time revocation是“separate required work”独立必需工作传输关闭回调transport close callbacks在“real three-node app/Slot/RPC integration”中被控制而不是在 usecase 层End 不授权源 GCsource GC源墓碑的退役依赖独立的“source-side fence”先拒绝原始 revision-zero Preparing 请求见 mqtt-session-reclamation.md。总结End 的六个不可妥协综合规格与源码WuKongIM 的 Exact-owner Session ending 机制可以凝练为六条不可妥协的原则可信输入End 只接受已认证的当前所有者与四种显式应用原因永不是客户端报文或“权限不可用”的误判隔离先行Ended/offline 状态、租约过期、缺失注册表都不是隔离证明即使行已 Ended也必须有物理关闭 效果排空的 quiescence 证明重读校验隔离后必须重读同一所有者变更即失败绝不跟随或终止后继者原子性一个生命周期命令同时结束 Session 并使活 Will 就绪但 End 本身不发布 Will、不授权源 GC、不碰分离工作时钟纪律观察先于隔离过期活动租约先记录原始断开绝不用隔离完成时刻重启 Will 时钟失败即失败任何畸形、模糊、丢失、冲突或 panic 都被收敛为显式错误绝不报告成功也绝不让取消后的效果继续发生——所有失败都保留在 end_test.go 的 20 种故障注入矩阵中可复现、可验证。对于需要在 WuKongIM 中实现“配额终结清理、权限撤销、源丢失兜底、管理性下线”等能力的开发者App.End是唯一的正确入口而子项回收事务mqtt-session-reclamation.md则是它之后唯一被允许触碰旧会话子项的路径。理解两者的边界是安全运营 MQTT 会话生命周期的前提。赞分享即时通讯后端【免费下载链接】WuKongIMMore than just IM 不只是即时通讯(IM)项目地址https://gitcode.com/gh_mirrors/wu/WuKongIM点击查看免费下载相关推荐WuKongIM MQTT Session 获取与续期机制深度解析精确所有者隔离与本地执行门的设计实践WuKongIM MQTT Session 获取与续期机制深度解析精确所有者隔离与本地执行门的设计实践 本文基于仓库文档 docs/specs/mqtt se即时通讯后端WuKongIM 网关物理关闭证明Gateway Physical Close Proof机制解析为 MQTT 精确所有者静默提供可验证的传输关闭边界WuKongIM 网关物理关闭证明Gateway Physical Close Proof机制解析为 MQTT 精确所有者静默提供可验证的传输关闭边界 导即时通讯后端WuKongIM MQTT 优雅退役机制Graceful Owner Retirement证明、持久化与隔离契约深度解析WuKongIM MQTT 优雅退役机制Graceful Owner Retirement证明、持久化与隔离契约深度解析 导读 WuKongIM 在 MQ即时通讯后端上一篇如何在10分钟内完成专业级漫画翻译BalloonTranslator完整指南下一篇告别虚拟机Whisky让macOS原生运行Windows程序的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。