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

## [Unreleased]

### 修复

- 升级官方 StreamFrame 2.3.1,修复迟到旧会话故障污染活会话状态、旧会话
绑定消息跨 Socket 错发,以及会话拆除时发送失败类型不稳定的问题。
- 串行化适配器的当前会话关闭与 transport 释放,避免已入队的迟到
Separate 在销毁竞态中调用已经释放的 StreamFrame 生命周期。
- 增加关闭/释放并发状态测试和真实 TCP 会话替换回归测试,验证排队发送
以会话失效结束且不会在新会话重放。

### 新增

- 初始化 SecsFrame 仓库、工程规范和 CI。
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.3.0" />
<PackageVersion Include="StreamFrame" Version="2.3.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>
Expand Down
9 changes: 6 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,20 @@ HSMS 会话切换后,旧会话的控制请求、数据事务和等待中的应
<code>IHsmsTransport</code> 使用单一事件流按 Session ID 报告会话打开、
帧到达和会话关闭。<code>StreamFrameHsmsTransport</code> 已在内部实现:

- 直接采用 StreamFrame 2.3.0 的单调 TCP Session ID;
- 直接采用 StreamFrame 2.3.1 的单调 TCP Session ID;
- 通过会话感知接收流保留帧的原始 Session ID,迟到消费不会被标成新会话;
- 会话绑定发送在 StreamFrame 原生 FIFO 中校验 Session ID,旧会话消息
不会重放,整帧写入本机 Socket 后才完成;
- 当前会话关闭/重连与 transport 释放串行化,迟到 actor 输入不会访问已
释放的 StreamFrame 生命周期;
- Active 连接把显式 T5 映射为固定 StreamFrame 重试间隔,关闭指数退避;
- 显式 T8 映射为 StreamFrame 原生未完成帧超时,并把对应 FrameError
转换为带 transport Session ID 的 HSMS 关闭原因。

这些能力仍隔离在 <code>IHsmsTransport</code> 后,不进入公共 API。此前
跟踪的 StreamFrame #38/#39 已在 2.3.0 完成迁移;SecsFrame 只保留
协议关闭原因和异常边界转换。详细失效模式与验证证据见
跟踪的 StreamFrame #38/#39 已在 2.3.0 完成迁移,2.3.1 进一步封闭迟到
旧故障污染活会话和排队消息跨 Socket 错发的竞态;SecsFrame 只保留协议
关闭原因和异常边界转换。详细失效模式与验证证据见
[STREAMFRAME-ADAPTER.md](STREAMFRAME-ADAPTER.md)。

### 单线程会话状态机
Expand Down
3 changes: 2 additions & 1 deletion docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
## 上游能力集成

StreamFrame #38(未完成帧超时)与 #39(会话感知发送确认及消息上下文)
已经随官方 StreamFrame 2.3.0 发布。SecsFrame 固定该 NuGet 版本,并由
已经随官方 StreamFrame 2.3.0 发布。SecsFrame 固定官方 2.3.1 NuGet 包,
包含迟到旧会话故障隔离、排队发送 Session ID 复核和稳定失效异常修复,并由
内部 <code>StreamFrameHsmsTransport</code> 使用原生会话 API;此前基于
原始字节回调、发送信封和自建 Session ID 的临时实现已经删除。

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

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

阶段 1 的工程能力已经形成完整基础链路,StreamFrame #38/#39 的正式包
迁移与等价测试已经完成。后续切片继续扩展 GEM 通用行为。任何标准默认值
迁移、2.3.1 会话隔离修复与等价测试已经完成。后续切片继续扩展 GEM 通用
行为。任何标准默认值
和完整合规结论都依赖团队合法获得的 E37/E37.1 及相关 GEM 标准副本与
一致性测试。

Expand Down
21 changes: 15 additions & 6 deletions docs/STREAMFRAME-ADAPTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

## 上游基线

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

- [#38](https://github.com/CSJ608/StreamFrame/issues/38) / [PR #41](https://github.com/CSJ608/StreamFrame/pull/41):
未完成帧超时与 <code>IncompleteFrameTimeout</code> 诊断;
- [#39](https://github.com/CSJ608/StreamFrame/issues/39) / [PR #42](https://github.com/CSJ608/StreamFrame/pull/42):
会话编号、会话绑定发送、写出确认和带会话归属的接收消息。

2.3.1 在相同公共 API 上修复两个会话隔离缺陷:过期会话的迟到故障不会
再发布 Retry 或清零活会话编号;发送 worker 会再次校验排队条目的 Session
ID,残留的旧会话绑定消息以 <code>SessionExpiredException</code> 结束且
不会写入新 Socket。停机、Socket 释放及会话故障竞态也统一收敛到该异常
边界。详见 [2.3.1 发布说明](https://github.com/CSJ608/StreamFrame/releases/tag/v2.3.1)。

SecsFrame 不再维护基于原始字节回调的 T8 监视器、发送确认计数器、会话
信封 codec 或自建 TCP Session ID。公共 API 和内部
<code>IHsmsTransport</code> 契约保持不变,上层 HSMS 状态机不直接依赖
Expand Down Expand Up @@ -37,6 +43,9 @@ StreamFrame 的 <code>SessionExpiredException</code> 在内部边界转换为
<code>HsmsTransportSessionExpiredException</code>,并保留原异常。协议
主动关闭或 T8 关闭时,适配器按 TCP Session ID 保留首个关闭原因,使
<code>SessionClosed</code> 与尚未完成的发送观察到同一个协议异常。
当前会话的 <code>Reconnect</code> 与 transport 释放还通过独立生命周期
边界串行化:释放先阻止新的关闭请求,再等待已经开始的重连结束,避免 actor
清空迟到 Separate 输入时访问已释放的 StreamFrame 生命周期。

## T5 与 T8

Expand All @@ -60,11 +69,11 @@ StreamFrame 只在已有未完成帧时运行该超时:空闲连接和完整
## 验证边界

适配器单元测试覆盖原生 Session ID、迟到消息、发送完成、并发入队、会话
失效、显式关闭原因和 T8 诊断映射。真实 TCP 回环测试覆盖分片帧收发与
实际 T8 到期;完整套件继续覆盖 T3/T6 从发送完成点启动以及旧会话不
重放。
失效、显式关闭原因、关闭/释放竞态和 T8 诊断映射。真实 TCP 回环测试覆盖
分片帧收发、实际 T8 到期,以及会话替换时排队发送的精确失效与新 Socket
内容隔离;完整套件继续覆盖 T3/T6 从发送完成点启动以及旧会话不重放。

这些测试证明当前工程契约与 StreamFrame 2.3.0 的互操作行为,不构成
这些测试证明当前工程契约与 StreamFrame 2.3.1 的互操作行为,不构成
SEMI 合规声明。T5/T8 的标准默认值、精确启停边界和异常恢复仍须依据团队
合法获得的 SEMI E37/E37.1 版本及一致性测试核对。仓库不得提交标准正文、
表格或 Schema。
57 changes: 34 additions & 23 deletions src/SecsFrame/Transport/StreamFrameHsmsTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ internal sealed class StreamFrameHsmsTransport : IHsmsTransport
private readonly ISessionAwareStreamConnection<HsmsFrame> _connection;
private readonly Channel<HsmsTransportEvent> _events;
#if NET9_0_OR_GREATER
private readonly Lock _lifecycleGate = new();
private readonly Lock _sessionGate = new();
#else
private readonly object _lifecycleGate = new();
private readonly object _sessionGate = new();
#endif
private readonly Dictionary<long, SessionContext> _sessions = new();
Expand Down Expand Up @@ -130,6 +132,11 @@ public async ValueTask DisposeAsync()
if (Interlocked.Exchange(ref _disposed, 1) != 0)
return;

lock (_lifecycleGate)
{
// Wait for an already-started Reconnect before disposing its lifetime state.
}

await _connection.DisposeAsync().ConfigureAwait(false);

if (_messagePump is not null)
Expand All @@ -153,33 +160,37 @@ public bool TryCloseSession(
Exception? error = null)
{
ThrowIfDisposed();

Exception? previousReason;
lock (_sessionGate)
lock (_lifecycleGate)
{
if (_currentSessionId != sessionId.Value ||
_connection.CurrentSessionId != sessionId.Value ||
!_sessions.TryGetValue(sessionId.Value, out var context) ||
context.IsClosed)
ThrowIfDisposed();

Exception? previousReason;
lock (_sessionGate)
{
return false;
}
if (_currentSessionId != sessionId.Value ||
_connection.CurrentSessionId != sessionId.Value ||
!_sessions.TryGetValue(sessionId.Value, out var context) ||
context.IsClosed)
{
return false;
}

previousReason = context.CloseReason;
if (error is not null && context.CloseReason is null)
context.CloseReason = error;
}
previousReason = context.CloseReason;
if (error is not null && context.CloseReason is null)
context.CloseReason = error;
}

try
{
_connection.Reconnect();
return true;
}
catch
{
if (error is not null)
RestoreCloseReason(sessionId, error, previousReason);
throw;
try
{
_connection.Reconnect();
return true;
}
catch
{
if (error is not null)
RestoreCloseReason(sessionId, error, previousReason);
throw;
}
}
}

Expand Down
Loading
Loading