### 你想解决什么问题? SecsFrame 正在扩展 HSMS 传输故障的可观测、Trace 导出与重放能力。当前只能对 T8(`IncompleteFrameTimeout`)做受限支持;若要安全覆盖 `FrameErrorKind` 的全部错误类型,还缺少两类由 StreamFrame 才能准确提供的事实。 1. **错误的原始会话归属** `FrameErrorEventArgs` 目前没有携带产生错误的 TCP 会话 ID。下游在回调中读取 `ISessionAwareStreamConnection.CurrentSessionId`,得到的是回调时刻的当前会话,而不是检测该错误的 `FrameDecoder<TMessage>` 所属会话。连接重建、会话替换或旧解码器的延迟事件发生时,下游无法严格证明错误属于哪个会话。 这对 `DiscardedByResync` 尤其重要:它未必伴随立即断开,不能依靠后续连接状态变化反推出来源会话。`FrameDecoder<TMessage>` 内部已有 `_sessionId`,但该信息在构造 `FrameErrorEventArgs` 时被丢弃。 2. **错误字节快照是否完整** `IncompleteFrameTimeout` 和 `IncompleteFrameOverflow` 只把最多 8192 字节的快照放进 `FrameErrorEventArgs.Bytes`。内部当时知道实际待处理字节数(`pendingBytes` / `buffer.Length`),但事件没有公开原始字节数或截断标记。下游因此无法诚实地区分“完整的 8192 字节错误数据”和“更长数据的 8192 字节前缀”。 下游用例见 SecsFrame PR #40: https://github.com/CSJ608/SecsFrame/pull/40 当前路线图说明: https://github.com/CSJ608/SecsFrame/blob/main/docs/ROADMAP.md ### 你的设想(可选) 希望 `FrameError` 能提供以下语义;具体 API 形式可以按 StreamFrame 的兼容性要求决定: ```csharp public sealed class FrameErrorEventArgs : EventArgs { public long SessionId { get; } public long ObservedByteCount { get; } public bool IsTruncated { get; } } ``` 也可以保留现有事件,并在 `ISessionAwareStreamConnection<TMessage>` 上增加会话感知的错误事件。关键不是属性名称,而是这些契约: - 每个由 `StreamConnection` 发出的 `FrameError` 都携带检测错误的 `FrameDecoder` 实例所绑定的正数会话 ID,而不是在事件投递时读取的 `CurrentSessionId`。 - 连接重建后,即使旧会话事件延迟到达,它仍保留旧会话 ID,不能被标记为新会话。 - 上述会话归属适用于 `DecodeFailed`、`DiscardedByResync`、`IncompleteFrameOverflow`、`IncompleteFrameTimeout` 四种 `FrameErrorKind`。 - 原始观测字节数满足 `ObservedByteCount >= Bytes.Length`;`IsTruncated` 等价于 `Bytes.Length < ObservedByteCount`。 - `DecodeFailed` 和 `DiscardedByResync` 当前提供完整字节时,两者相等且 `IsTruncated == false`。 - timeout/overflow 使用内部已知的实际待处理字节数,并在超过 8192 字节快照上限时标记截断。 - 现有 `Bytes` 的复制与生命周期保证、`Exception` 行为,以及连接断开/重试策略保持不变。 - 如可行,请通过新增构造重载、可选接口事件或其他方式尽量保持现有使用方的源码/二进制兼容。 建议验收覆盖: - 四种 `FrameErrorKind` 都能报告来源会话 ID。 - 会话重建/替换测试证明旧会话错误不会被归到新会话。 - timeout 和 overflow 在大于 8192 字节时报告实际字节数并标记截断。 - 不超过 8192 字节时,原始字节数等于 `Bytes.Length` 且不标记截断。 - XML 文档明确来源会话和快照完整性的语义。 这项能力到位后,SecsFrame 可以在默认关闭的前提下,把有界故障观测扩展到其余错误类型,并对跨会话过滤、Trace 导出和重放给出可验证的语义;脱敏、容量和 payload 默认拒绝策略仍由 SecsFrame 自己实现。
Activity
CSJ608 commentedon Aug 29, 2026
评审结论:需求合理,建议接受 ✅
对照现有框架逐条核实后,两项诉求都成立,且实现成本低(两个标量 + 一个 long,全部只在错误路径上)。以下是逐项评估与几个契约细节的修正意见。
一、会话归属(SessionId)——痛点属实,且是既有设计的收口
现状核实(与 issue 描述完全一致):
FrameDecoder已持有_sessionId(构造时传入),但四处构造FrameErrorEventArgs的位置(DecodeFailed / Overflow / Timeout / DiscardedByResync)全部丢弃了它。CurrentSessionId确实无法证明归属,且比 issue 描述的还要糟一点:Retry/Disconnected发布时CurrentSessionId已归零为 0——超时/超限事件的投递与状态发布之间存在竞速窗口,下游读到 0 既非旧会话也非新会话,直接无法归属。DiscardedByResync与 SkipFrame 策略下的DecodeFailed都不断开、解码循环继续消费;用户并发Reconnect()时,旧解码器在拆除等待窗口内(StopSessionCore 最长等 2s)仍可能上报错误,此刻CurrentSessionId已是新会话编号——误归到新会话正是 issue 点名要防的情形,代码路径上真实存在。架构一致性论证(这是本评审支持接受的最强理由):
SessionMessage<TMessage>.SessionId存在的意义就是"旧会话解码任务迟到投递的消息,仍携带旧会话的编号"(ISessionAwareStreamConnection/SessionMessage的 XML 契约)。消息通道已经做了会话归属,错误通道没有——这是同一场景下的既有不一致。本次增补是补齐,不是开新口子。实现是构造性保证,不是协议:事件参数在解码器内部构造,天然绑定该解码器实例的会话;旧会话事件无论多晚到达都带旧编号,不需要锁、快照或投递时校验。四种 Kind 全部在解码器内发出,
_sessionId无遗漏路径。二、快照完整性(ObservedByteCount / IsTruncated)——缺口真实,值在内部现成
ErrorSnapshotBytes = 8192上限存在;timeout 用pendingBytes(实际缓冲全长)、overflow 用buffer.Length,内部值都在手边,只是没放进事件。零热路径成本(仅错误路径携带)。三、契约细节的修正意见(实现前需要对齐的点)
ObservedByteCount语义按 Kind 分化,XML 文档必须给语义表:DecodeFailed / DiscardedByResync 的"完整拷贝"现状要写死进契约:这两类目前是全量拷贝、无 8192 上限。注意
StxEtxFramer的 discarded 理论可达缓冲全长(噪声 + 被中止半帧),并非小数据。按现状如实公开ObservedByteCount == Bytes.Length && IsTruncated == false即可;若未来有人想给 DiscardedByResync 统一加 8192 截断,那是行为变更,需另行讨论。IsTruncated建议在构造函数内自动计算(= bytes.Length < observedByteCount),不做独立传参,从根上杜绝两者不一致。旧构造重载委托新重载(observedByteCount: bytes.Length),不变式自动成立——这同时满足 issue 的源/二进制兼容要求(sealed 类 + 保留旧构造签名)。SessionId 取值域写清:与
CurrentSessionId/SessionMessage.SessionId同一计数器(正数、单调、有间隔、不复用),可跨 API 直接关联比对。由连接发出的事件恒 > 0;旧构造重载默认 0 仅出现在用户自建的事件参数中(文档标注 0 = 未提供)。方案选型:同意 issue 首选——直接增补
FrameErrorEventArgs,不建议"在ISessionAwareStreamConnection上另开会话感知错误事件"的替代方案:同一事实两个事件源,订阅者容易漏订/双收;FrameError 本就是连接级单事件,增补字段已满足会话感知。ns2.0 目标无兼容障碍(纯标量属性)。测试验收补充一条(issue 四条都可行,现有基建能承载):DiscardedByResync 在旧会话发生、随后会话重建,事件仍带旧编号——这是四个 Kind 里唯一"不断开还继续解码"的,跨会话延迟投递窗口最大。另外现有
SnapshotIsCappedAtLimit测试可顺手加断言IsTruncated == true && ObservedByteCount == 9004(4 字节头 + 9000 正文)。四、发布
纯增量、无破坏性变更,按语义化版本走 2.6.0 minor,正常走 PR +
build-test绿 + Unreleased 段。issue 对上游代码的事实陈述(
_sessionId被丢弃、8192 上限、四种 Kind 的边界)经核对全部准确,无需要纠正的前提。可以进入实现。feat: FrameError 事件增补会话归属与快照完整性元数据(#56)
test: 四类 FrameError 的会话归属/截断标记/跨会话旧编号回归测试(#56)