Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### 新增
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

<PropertyGroup>
<PackageId>StreamFrame.Protocols.Xml</PackageId>
<Version>2.3.0</Version>
<Version>2.3.1</Version>
<Description>StreamFrame 的 XML 报文示例驱动:XmlDocumentCodec(XmlSerializer / XmlWriter 编解码)。</Description>
<Authors>Marci</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/StreamFrame/StreamFrame.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<PropertyGroup>
<PackageId>StreamFrame</PackageId>
<Version>2.3.0</Version>
<Version>2.3.1</Version>
<Description>通用 socket 通讯框架:帧定界(IFramer)与帧内编解码(ICodec&lt;TMessage&gt;)插件化,支持长度前缀 / STX-ETX 定界与 IPv4/IPv6 双栈,可选流式零拷贝发送。</Description>
<Authors>Marci</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
7 changes: 5 additions & 2 deletions test/StreamFrame.Tests/IncompleteFrameTimeoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
Loading