From e45175fe450f782c21fd0ac8a682c9b40c7e9110 Mon Sep 17 00:00:00 2001 From: Marci Date: Fri, 28 Aug 2026 23:05:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?build:=20=E7=89=88=E6=9C=AC=E5=8D=87?= =?UTF-8?q?=E8=87=B3=202.3.1=EF=BC=88=E8=AF=84=E5=AE=A1=20P1=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=A1=A5=E4=B8=81=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 ++++++++----- .../StreamFrame.Protocols.Xml.csproj | 2 +- src/StreamFrame/StreamFrame.csproj | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f9281b..06a9d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,19 @@ ## [Unreleased] +### 变更 +- 暂无 + +## [2.3.1] - 2026-08-28 + ### 修复 - **迟到的过期故障重连污染活会话**(v2.3.0 评审 P1-1):垂死旧会话的迟到故障(epoch 已被替换,如 net48 线程池慢导致第二个 `ScheduleReconnect` 延迟到达)此前会在 epoch 校验之前发布 `Retry` 并把 `CurrentSessionId` 归零——完全存活的会话被谎报为重连中、`WaitForConnectedAsync` 悬挂、活会话编号被误判失效。现改为过期故障在任何对外发布之前整体丢弃(gate 内权威复查保留)。回归测试以反射调度过期故障验证。 -- **残留的旧会话绑定条目可能在新会话 socket 上错发并报告成功**(评审 P1-2,"绝不跨会话重放"保证的漏洞):`Connected→Connected` 直连拆除(双 `StartAsync` 竞速/Connected 回调内 `Reconnect()` 重入)路径下,旧会话排队条目既不被清扫也无编号校验,会被新 worker 发到新 socket 并以成功收尾。现发送 worker 认领时校验条目 `SessionId`,不属当前会话的绑定条目一律以 `SessionExpiredException` 失败、绝不发送。压测改为按帧内容对账(跨会话错发探测器)。 +- **残留的旧会话绑定条目可能在新会话 socket 上错发并报告成功**(v2.3.0 评审 P1-2,"绝不跨会话重放"保证的漏洞):`Connected→Connected` 直连拆除(双 `StartAsync` 竞速/Connected 回调内 `Reconnect()` 重入)路径下,旧会话排队条目既不被清扫也无编号校验,会被新 worker 发到新 socket 并以成功收尾。现发送 worker 认领时校验条目 `SessionId`,不属当前会话的绑定条目一律以 `SessionExpiredException` 失败、绝不发送。压测改为按帧内容对账(跨会话错发探测器)。 - 发送失败类型收敛(评审 P2-5/P2-6):停机时清扫先于发送通道完成(队满等待中的条目统一以 `SessionExpiredException` 收尾而非偶发 `ChannelClosedException`);worker 写出失败时 internal 的 `SessionFaultException` 与 `ObjectDisposedException` 统一映射为 `SessionExpiredException`,不再向调用方泄漏非文档类型。 - `Reconnect()` 在 `Start()` 之前调用由 NRE 改为明确的 `InvalidOperationException`(存量问题顺手修复)。 - 未完成帧超时关闭(默认)时不再每读循环拷贝半帧快照(评审 P2-1,2.2.x 路径的小开销)。 - `SendInSessionAsync` 注册竞态窗口的失败以条目携带的同一异常收尾,不再留下未观察的任务异常(评审 P2-4)。 -### 变更 -- 暂无 - ## [2.3.0] - 2026-08-28 ### 新增 @@ -154,7 +156,8 @@ - 发布自动化:`release.yml`(tag 自动建 GitHub Release)、`publish-nuget.yml`(OIDC Trusted Publishing 推送 nuget.org)。 - 单元测试 25 个 + 三场景端到端 demo。 -[Unreleased]: https://github.com/CSJ608/StreamFrame/compare/v2.3.0...HEAD +[Unreleased]: https://github.com/CSJ608/StreamFrame/compare/v2.3.1...HEAD +[2.3.1]: https://github.com/CSJ608/StreamFrame/compare/v2.3.0...v2.3.1 [2.3.0]: https://github.com/CSJ608/StreamFrame/compare/v2.2.1...v2.3.0 [2.2.1]: https://github.com/CSJ608/StreamFrame/compare/v2.2.0...v2.2.1 [2.2.0]: https://github.com/CSJ608/StreamFrame/compare/v2.1.0...v2.2.0 diff --git a/src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj b/src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj index 4eb316e..f22db5b 100644 --- a/src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj +++ b/src/StreamFrame.Protocols.Xml/StreamFrame.Protocols.Xml.csproj @@ -32,7 +32,7 @@ StreamFrame.Protocols.Xml - 2.3.0 + 2.3.1 StreamFrame 的 XML 报文示例驱动:XmlDocumentCodec(XmlSerializer / XmlWriter 编解码)。 Marci MIT diff --git a/src/StreamFrame/StreamFrame.csproj b/src/StreamFrame/StreamFrame.csproj index 733b42c..f070d03 100644 --- a/src/StreamFrame/StreamFrame.csproj +++ b/src/StreamFrame/StreamFrame.csproj @@ -13,7 +13,7 @@ StreamFrame - 2.3.0 + 2.3.1 通用 socket 通讯框架:帧定界(IFramer)与帧内编解码(ICodec<TMessage>)插件化,支持长度前缀 / STX-ETX 定界与 IPv4/IPv6 双栈,可选流式零拷贝发送。 Marci MIT From c4a906e3822cfbccaca76f35b799e55e353e56fb Mon Sep 17 00:00:00 2001 From: Marci Date: Fri, 28 Aug 2026 23:17:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20CompleteFrameThenLongIdle=20?= =?UTF-8?q?=E6=94=BE=E5=AE=BD=E6=97=B6=E5=BA=8F=E4=BD=99=E9=87=8F=EF=BC=88?= =?UTF-8?q?500ms=20=E8=B6=85=E6=97=B6/1200ms=20=E7=A9=BA=E9=97=B2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 满负载并行下小帧仍可能被 TCP 拆段,解码循环续读唤醒被拖过 200ms 会让 帧内间隔合法触发超时(本地满套件偶发一次、单测 5/5 绿)。帧内续传重置 语义由 Trickle 用例的 1000ms 大余量覆盖,本用例只验证'帧切尽后不计时'。 --- test/StreamFrame.Tests/IncompleteFrameTimeoutTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/StreamFrame.Tests/IncompleteFrameTimeoutTests.cs b/test/StreamFrame.Tests/IncompleteFrameTimeoutTests.cs index b0e58b9..0c5a5e0 100644 --- a/test/StreamFrame.Tests/IncompleteFrameTimeoutTests.cs +++ b/test/StreamFrame.Tests/IncompleteFrameTimeoutTests.cs @@ -153,7 +153,10 @@ public async Task IdleConnection_NeverReceivedBytes_DoesNotTrigger() public async Task CompleteFrameThenLongIdle_DoesNotTrigger() { var port = GetFreePort(); - await using var server = CreateServer(port, new StreamConnectionOptions { IncompleteFrameTimeoutMs = 200 }); + // 余量说明:超时取 500ms 而非 200ms——并行测试负载下,单次 Write 的小帧仍可能被 + // TCP 拆段,解码循环的续读唤醒被拖过 200ms 会让"帧内间隔"合法触发超时(曾偶发)。 + // 帧内续传的重置语义由 TrickleWithinDeadline(1000ms 超时 + 250ms 间隔)覆盖 + await using var server = CreateServer(port, new StreamConnectionOptions { IncompleteFrameTimeoutMs = 500 }); var errors = AttachErrorRecorder(server); var received = StartMessageDrain(server); server.Start(CancellationToken.None); @@ -166,7 +169,7 @@ public async Task CompleteFrameThenLongIdle_DoesNotTrigger() // 一帧完整送达后长时间静默:帧已切尽、缓冲为空,不应计时 await client.GetStream().WriteAsync(Frame("done")); await WaitForAsync(() => { lock (received) return received.Count == 1; }); - await Task.Delay(600); + await Task.Delay(1200); Assert.Equal(ConnectionState.Connected, server.State); }