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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

### 修复

- 升级官方 StreamFrame 2.5.0,采用自适应发送编码缓冲,并获得 Passive
接受循环代次门控,避免显式关闭与自动重连竞速泄漏监听器。
- 增加真实 TCP Passive 重连竞态测试,连续验证监听恢复、Session ID 单调
递增和原会话精确关闭。
- 升级官方 StreamFrame 2.4.0,采用 Connected 发布点的会话 epoch 收口,
并获得 Passive 接受重试出锁与监听端口立即重绑加固。
- 升级官方 StreamFrame 2.3.1,修复迟到旧会话故障污染活会话状态、旧会话
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<PackageVersion Include="PolySharp" Version="1.16.0" />
<PackageVersion Include="Secs4Net" Version="3.1.0" />
<PackageVersion Include="StreamFrame" Version="2.4.0" />
<PackageVersion Include="StreamFrame" Version="2.5.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>
Expand Down
7 changes: 4 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ HSMS 会话切换后,旧会话的控制请求、数据事务和等待中的应
<code>IHsmsTransport</code> 使用单一事件流按 Session ID 报告会话打开、
帧到达和会话关闭。<code>StreamFrameHsmsTransport</code> 已在内部实现:

- 直接采用 StreamFrame 2.4.0 的单调 TCP Session ID,并在 Connected
- 直接采用 StreamFrame 2.5.0 的单调 TCP Session ID,并在 Connected
发布点同步分配会话 epoch;
- 通过会话感知接收流保留帧的原始 Session ID,迟到消费不会被标成新会话;
- 会话绑定发送在 StreamFrame 原生 FIFO 中校验 Session ID,旧会话消息
Expand All @@ -67,8 +67,9 @@ HSMS 会话切换后,旧会话的控制请求、数据事务和等待中的应
这些能力仍隔离在 <code>IHsmsTransport</code> 后,不进入公共 API。此前
跟踪的 StreamFrame #38/#39 已在 2.3.0 完成迁移,2.3.1 进一步封闭迟到
旧故障污染活会话和排队消息跨 Socket 错发的竞态,2.4.0 又收口发布窗口
旧 epoch 故障并加固 Passive 监听恢复;SecsFrame 只保留协议关闭原因和
异常边界转换。详细失效模式与验证证据见
旧 epoch 故障并加固 Passive 监听恢复,2.5.0 进一步以接受循环代次门控
阻止重连竞速泄漏旧监听器,并加入不改变编码契约的自适应发送缓冲;
SecsFrame 只保留协议关闭原因和异常边界转换。详细失效模式与验证证据见
[STREAMFRAME-ADAPTER.md](STREAMFRAME-ADAPTER.md)。

### 单线程会话状态机
Expand Down
5 changes: 3 additions & 2 deletions docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
StreamFrame #38(未完成帧超时)与 #39(会话感知发送确认及消息上下文)
已经随官方 StreamFrame 2.3.0 发布,2.3.1 随后补充迟到旧会话故障隔离、
排队发送 Session ID 复核和稳定失效异常修复。SecsFrame 当前固定官方
2.4.0 NuGet 包,进一步收口 Connected 发布点的会话 epoch,加固 Passive
接受重试与监听端口重绑。内部
2.5.0 NuGet 包;2.4.0 收口 Connected 发布点的会话 epoch,并加固 Passive
接受重试与监听端口重绑;2.5.0 再以接受循环代次门控阻止显式关闭/自动重连
竞速泄漏旧监听器,并采用不改变编码契约的自适应发送缓冲。内部
<code>StreamFrameHsmsTransport</code> 使用原生会话 API;此前基于
原始字节回调、发送信封和自建 Session ID 的临时实现已经删除。

Expand Down
7 changes: 4 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

- [x] 完整 SECS-II Item 类型、嵌套 List 和严格二进制编解码。
- [x] 动态 SecsMessage、可选单根 Item 与 HSMS Data Message Payload 编解码。
- [x] 迁移到 StreamFrame 2.4.0 原生会话绑定、写出确认、会话 epoch/监听
- [x] 迁移到 StreamFrame 2.5.0 原生会话绑定、写出确认、会话 epoch/监听
竞态修复和未完成帧超时。
- [x] Active/Passive 共用状态机的 Select/Select Response、Separate、
T6/T7、System Bytes 关联和会话失效处理。
Expand All @@ -27,8 +27,9 @@
- [x] Host、Equipment 双角色端点及两种 Active/Passive 拓扑的双向回环。

阶段 1 的工程能力已经形成完整基础链路,StreamFrame #38/#39 的正式包
迁移与 2.3.1 会话隔离修复已经完成;当前固定 2.4.0,并继续验证发布窗口
epoch 收口与 Passive 监听加固。后续切片继续扩展 GEM 通用行为。任何
迁移与 2.3.1 会话隔离修复已经完成;2.4.0 又收口发布窗口 epoch 并加固
Passive 监听。当前固定 2.5.0,并增加接受循环代次门控与自适应发送缓冲的
本地回归证据。后续切片继续扩展 GEM 通用行为。任何
标准默认值
和完整合规结论都依赖团队合法获得的 E37/E37.1 及相关 GEM 标准副本与
一致性测试。
Expand Down
18 changes: 15 additions & 3 deletions docs/STREAMFRAME-ADAPTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## 上游基线

SecsFrame 固定依赖官方 [StreamFrame 2.4.0](https://www.nuget.org/packages/StreamFrame/2.4.0)。
SecsFrame 固定依赖官方 [StreamFrame 2.5.0](https://www.nuget.org/packages/StreamFrame/2.5.0)。
2.3.0 已经解决此前跟踪的两项能力缺口:

- [#38](https://github.com/CSJ608/StreamFrame/issues/38) / [PR #41](https://github.com/CSJ608/StreamFrame/pull/41):
Expand All @@ -25,6 +25,16 @@ ID 一样在 Connected 发布点分配,闭合状态已经对外可见但会话
提升为公共 API。详见
[2.4.0 发布说明](https://github.com/CSJ608/StreamFrame/releases/tag/v2.4.0)。

2.5.0 继续保留上述公共接口和目标框架。发送 worker 会按连接记忆上一帧
编码高水位并自适应选择初始缓冲区,减少尺寸相近的大报文在暖机期跨池扩容;
该缓冲策略不进入 SecsFrame 公共 API,也不改变帧或消息编码。Passive
接受循环新增代次门控,被显式 <code>Reconnect</code> 取代的旧循环不能再
创建、关闭监听器或发布 Connected,修复显式关闭与对端断开触发的自动重连
竞速可能泄漏无人消费监听 Socket 的问题。详见
[2.5.0 发布说明](https://github.com/CSJ608/StreamFrame/releases/tag/v2.5.0)、
[PR #51](https://github.com/CSJ608/StreamFrame/pull/51) 和
[PR #52](https://github.com/CSJ608/StreamFrame/pull/52)。

SecsFrame 不再维护基于原始字节回调的 T8 监视器、发送确认计数器、会话
信封 codec 或自建 TCP Session ID。公共 API 和内部
<code>IHsmsTransport</code> 契约保持不变,上层 HSMS 状态机不直接依赖
Expand Down Expand Up @@ -80,9 +90,11 @@ StreamFrame 只在已有未完成帧时运行该超时:空闲连接和完整
适配器单元测试覆盖原生 Session ID、迟到消息、发送完成、并发入队、会话
失效、显式关闭原因、关闭/释放竞态和 T8 诊断映射。真实 TCP 回环测试覆盖
分片帧收发、实际 T8 到期,以及会话替换时排队发送的精确失效与新 Socket
内容隔离;完整套件继续覆盖 T3/T6 从发送完成点启动以及旧会话不重放。
内容隔离;Passive 重连竞态测试反复并发对端关闭与本地显式关闭,验证监听
恢复、Session ID 单调递增和原会话精确关闭。完整套件继续覆盖 T3/T6 从
发送完成点启动以及旧会话不重放。

这些测试证明当前工程契约与 StreamFrame 2.4.0 的互操作行为,不构成
这些测试证明当前工程契约与 StreamFrame 2.5.0 的互操作行为,不构成
SEMI 合规声明。T5/T8 的标准默认值、精确启停边界和异常恢复仍须依据团队
合法获得的 SEMI E37/E37.1 版本及一致性测试核对。仓库不得提交标准正文、
表格或 Schema。
78 changes: 78 additions & 0 deletions test/SecsFrame.Tests/StreamFrameHsmsTransportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,47 @@ await SendAndReadAsync(
await events.DisposeAsync().ConfigureAwait(true);
}

[Fact]
public async Task Real_passive_reconnect_race_keeps_listener_available()
{
var port = GetFreePort();
await using var transport = StreamFrameHsmsTransport.Create(
IPAddress.Loopback,
port,
isActive: false,
new HsmsTransportOptions(T5, T8),
new StreamConnectionOptions
{
AcceptRetryDelayMs = 10,
ConnectRetryDelayMs = 10,
});
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var events = transport.GetEventsAsync(cancellation.Token).GetAsyncEnumerator();
transport.Start(cancellation.Token);
var previousSessionId = new HsmsTransportSessionId(0);

for (var attempt = 0; attempt < 8; attempt++)
{
using var client = await ConnectWithRetryAsync(
port,
cancellation.Token).ConfigureAwait(true);
var opened = await NextAsync(events).ConfigureAwait(true);
Assert.Equal(HsmsTransportEventKind.SessionOpened, opened.Kind);
Assert.True(opened.SessionId.Value > previousSessionId.Value);

var explicitClose = Task.Run(
() => transport.TryCloseSession(opened.SessionId));
client.Dispose();
_ = await explicitClose.ConfigureAwait(true);
var closed = await NextAsync(events).ConfigureAwait(true);
Assert.Equal(HsmsTransportEventKind.SessionClosed, closed.Kind);
Assert.Equal(opened.SessionId, closed.SessionId);
previousSessionId = opened.SessionId;
}

await events.DisposeAsync().ConfigureAwait(true);
}

private static StreamFrameHsmsTransport CreateTransport(
FakeStreamConnection connection)
=> new(connection);
Expand Down Expand Up @@ -506,6 +547,43 @@ private static int GetFreePort()
return port;
}

private static async Task<System.Net.Sockets.TcpClient> ConnectWithRetryAsync(
int port,
CancellationToken cancellationToken)
{
for (var attempt = 0; attempt < 100; attempt++)
{
var client = new System.Net.Sockets.TcpClient();
try
{
var connect = client.ConnectAsync(IPAddress.Loopback, port);
var completed = await Task.WhenAny(
connect,
Task.Delay(
TimeSpan.FromSeconds(2),
cancellationToken)).ConfigureAwait(false);
if (!ReferenceEquals(connect, completed))
{
client.Dispose();
throw new TimeoutException(
"The passive listener accepted no connection.");
}

await connect.ConfigureAwait(false);
return client;
}
catch (System.Net.Sockets.SocketException)
{
client.Dispose();
await Task.Delay(
TimeSpan.FromMilliseconds(10),
cancellationToken).ConfigureAwait(false);
}
}

throw new TimeoutException("The passive listener did not become available.");
}

private static async Task<byte[]> ReadExactlyAsync(
Stream stream,
int count,
Expand Down
Loading