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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
## [Unreleased]

### 修复
- **终态竞速守门与发布段原子化([#68](https://github.com/CSJ608/StreamFrame/issues/68))**:并发 Dispose 与 Reconnect/连接成功竞速时,已通过停机检查但尚未写入 State 的发布被抢占,停机写入 Disconnected 后迟到的 Retry/Connected 会把终态翻转(State 停在 Retry/Connected);Retry 派发还会对停机回收的 `_lifetimeCts` 空引用。状态发布收敛为发布段(`_publishGate` 锁内原子:停机/过期纪元判定 + 编号纪元簿记 + State 写入)与通知段(锁外用户回调/等待器/派发,回调可安全重入);停机后到达的非终态发布整体丢弃;传输建立与停机竞速时新 socket 在守门处当场释放(此前泄漏到终结器)。新增迟到发布确定性门控、连接成功与停机竞速、并发关闭/重连终态不变量回归。
- **回调重入会话撕裂([#68](https://github.com/CSJ608/StreamFrame/issues/68))**:Connected 回调内同步 `Reconnect()` 时,迟到发布帧的 `StartSession` 以过期编号/纪元"兜底重建"僵尸会话,其拆除会关掉新会话刚建立的 socket(编号复用 + 会话失活,实测可复现)。改为纪元现行性门控:过期帧放弃创建,连接由取代者循环完成;`_socket` 被新连接覆盖时旧值当场释放(直连迁移路径不再泄漏)。新增回调重入(Reconnect/DisposeAsync)收敛性、合法状态转移序列与编号线性化回归。
- **基准工作量对齐([#70](https://github.com/CSJ608/StreamFrame/issues/70))**:单向不再回显;直接 TCP 与框架共享负载、定界、Codec、物化校验及完成点,覆盖双 Framer、64B/1KB/64KB 和字节/两种字符串编码,往返逐条等待回显。增加全矩阵连续批次验证;原始结果与环境随仓库保存,撤回不同工作量相减所得的框架税结论,修正指标单位和筛选命令,不修改框架实现。
- **NativeAOT 运行验收([#71](https://github.com/CSJ608/StreamFrame/issues/71))**:CI 发布后执行 linux-x64 原生产物;有界等待连接与三条消息,校验会话绑定/普通发送及内容,观察消费任务并释放资源。动态选择端口并仅在连接阶段有限重选;连接失败、少收、消费异常和内容错误以非零退出,通过受控故障反证检查有效。保留警告为错误和非必需 AOT 检查。
- **Soak 队列续发与收尾([#67](https://github.com/CSJ608/StreamFrame/issues/67))**:发送 worker 在每次出队前检查会话取消,避免上一帧写完后拆除的旧 worker 取走并丢弃待续发普通条目;新增确定性门控回归。长流测试精确排空后关闭读 socket 并观察任务,修复 net48 仅取消令牌时读任务不收敛。
Expand All @@ -16,6 +18,7 @@
- **Soak 目标矩阵([#66](https://github.com/CSJ608/StreamFrame/issues/66))**:显式选择 Ubuntu net8.0/net10.0、Windows net8.0/net10.0/net48,避免 Ubuntu 启动 .NET Framework 测试;job 名、详细日志与始终尝试上传的 TRX 产物按 OS/TFM 区分,测试断言失败继续使 job 失败。普通 CI 必需检查名不变。

### 变更
- **状态机与所有权规范([#68](https://github.com/CSJ608/StreamFrame/issues/68) PR-1)**:DESIGN 增补权威清单——状态转换全表与发布段/通知段规则、资源所有权表(会话 socket/监听器/会话五元组/消息通道)、锁与同步原语清单、允许重入点(回调内 Reconnect/Dispose、停机后调用、迟到旧会话任务)及已知限制(拆除同步等待持锁、`_socket` 字段语义——指向后续内部 Session 对象重构);与新增的 `StateOwnershipBaselineTests` 共同构成 #68 会话所有权重构的行为基线。
- 依赖升级(Dependabot,[PR #59](https://github.com/CSJ608/StreamFrame/pull/59)):soak 工作流 `actions/setup-dotnet` v4→v6(Node 24 运行时,与其余工作流对齐)。
- **驱动所有权与并发文档([#69](https://github.com/CSJ608/StreamFrame/issues/69))**:补全 ICodec/IFramer/流式定界的缓冲借用及同连接收发、新旧会话、跨连接共享约束;双语 README 增加拥有独立数据的 byte[]/ReadOnlyMemory 示例、有界队列生产配置及单客户端边界,澄清终态等待、KeepAlive 粒度和入队非确认。示例纳入三目标编译并以主动覆写缓冲验证异步消费;不改变默认值或实现公共契约。
- **Soak 可观测协议([#67](https://github.com/CSJ608/StreamFrame/issues/67))**:保持 SendAsync 仅入队契约,记录可重放 seed、动作、尝试 ID、会话及发送/读取阶段;业务测试使用 ACK、重试、去重并精确校验,重试前独立检查队列续发与会话边界。所有阶段有界等待,未启用长测报告 Skip;完整 600 秒五目标连续三次验收另行跟踪,见 docs/SOAK.md。
Expand Down
39 changes: 39 additions & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,45 @@ socket --ReceiveAsync--> pipe.Writer --[Pipe]--> FrameDecoder.RunAsync

**原始字节与诊断(1.2.0)**:`RawBytesReceived/Sent` 全量输出(含被定界器丢弃的噪声字节),发送侧按实际写出分片回调;两者与 `ConnectionChanged`、`FrameError` 的用户回调异常均被隔离,不反噬会话。`IFrameDiscardReporting`(可选接口,内置两个 codec 实现)让定界器精确上报重同步丢弃的字节,经 `FrameError(DiscardedByResync)` 交上层调试。

### 状态机与所有权规范(#68 行为基线)

> 本节是 `StreamConnection` 状态转换、资源所有权、锁范围与允许重入点的权威清单,
> 与 `StateOwnershipBaselineTests` 共同构成后续会话所有权重构的行为底线。

**状态转换全表**(`CommunicationStateChanging` 是唯一发布入口;`State` 只在发布段写入):

| 目标状态 | 触发源 | 发布点 |
|---|---|---|
| `Connecting` | `Start`/重连的 `StartAsync` 循环头(每轮尝试前) | 连接循环 |
| `Connected` | 传输建立成功(connect/accept 返回后、过停机守门) | 连接循环(唯一发布点) |
| `Retry` | 用户 `Reconnect()`;会话任务故障经 `ScheduleReconnect`(携带 epoch,过期即丢) | 用户线程 / 线程池 |
| `Disconnected` | `Shutdown`(Dispose 或生命周期令牌取消)唯一来源 | 停机路径 |

发布段规则(`_publishGate` 锁内,原子):① 停机后到达的非终态发布**整体丢弃**——终态必须是最后对外可见的状态(并发 Reconnect/连接成功与 Dispose 竞速时不被翻转);② 过期纪元的故障发布在任何对外可见效果之前丢弃;③ 会话编号分配/归零与纪元递增先于 `State` 写入(线性化点:`ConnectionChanged` 回调与 `WaitForConnectedAsync` 完成时刻读取 `CurrentSessionId` 必得一致值)。通知段(锁外):用户回调(异常隔离)、等待器完成、会话创建/拆除派发——回调可能重入,不得持有发布锁。

**会话创建的纪元现行性门控**:`StartSession` 只在"发布帧的纪元仍是最新、编号非零"时创建会话。Connected 回调内同步 `Reconnect()` 或发布窗口内的现行纪元故障会让位于取代者(其循环重建连接),迟到帧不再"兜底重建"——那曾以过期编号复用、拆除新会话刚建立的 socket(撕裂会话)。被取代帧的 socket 在新连接覆盖 `_socket` 时当场释放。

**资源所有权**:

| 资源 | 创建者 | 释放者 | 备注 |
|---|---|---|---|
| `_socket`(会话 socket) | 连接循环(connect/accept) | 会话拆除(`StopSessionCore`);新连接覆盖时;`Shutdown` 末尾兜底 | 赋值与关闭互斥于 `_sessionGate`;停机守门失败的连接当场释放 |
| `_server`(监听 socket) | `InitServer`(Bind/Listen 全部成功才发布) | 单客户端接受后关闭;`Shutdown` 释放;bind 失败自清理 | 归当代接受循环所有(代次门控,#47) |
| 会话五元组(`_sessionCts`/`_pipe`/接收、解码、发送三任务) | `StartSession` | `StopSessionCore`(cancel → 唤醒 → 置空字段 → 有界等待 ≤2s → 关 socket → 释放 cts) | 终态后不再创建(`StartSession` 停机守门) |
| `_messageRelay`(消息通道) | 构造 | 仅 `Shutdown`(连接级、跨会话存活) | 会话重建不完成它(1.2.0 假活修复) |
| `_sendQueue` / 挂起的会话绑定条目 | 业务侧入队 | `Shutdown` 完成通道;拆除时 `FaultPendingSessionSends` | 条目三态 CAS:Queued/Claimed/Cancelled/Expired |

**锁与同步原语清单**:`_publishGate`(状态发布段,锁内无 await/回调);`_sessionGate`(会话创建/拆除、`_socket` 赋值互斥;拆除等待最长 2s——已知同步等待,见"已知限制");`_listenerGate`(监听器发布与停机释放互斥);`_acceptLock`(接受尝试串行化 + 代次门控落点);`_sendLock`(socket 写串行化);CAS 字段:`_retryInProgress`(重拆防重入)、`_acceptLoopId`(接受循环代次)、`_disposeStage`/`_started`、发送条目状态字。

**允许的重入点**(用户回调可调用任意公共 API,异常一律隔离):

- `Connected` 回调内 `Reconnect()`:合法(病态但支持)。回调内 Retry 先行(纪元递增 + 派发新循环),原发布帧的会话创建被纪元门控放弃;新连接由新循环完成,编号恒非零且不复用。
- `Connected` 回调内 `DisposeAsync()`:合法。停机同步完成(会话尚未创建则无拆除等待);原发布帧随后的会话创建被停机守门放弃;挂起等待器以取消收尾。
- 停机后的任何调用:`Reconnect()`/`SendInSessionAsync` 等抛 `ObjectDisposedException` 或以会话失效收尾;迟到的内部状态发布被发布段丢弃。
- 迟到的旧会话任务/故障:入口过期纪元检查丢弃,不误杀新会话(1.2.0/2.3.1 既有保证,重入路径由纪元门控补齐)。

**已知限制(后续 PR 方向)**:① `StopSessionCore` 持 `_sessionGate` 同步等待会话任务最长 2 秒——线程池饥饿时放大停机/重连延迟(高并发压力下可观测),#68 后续以"先中止 I/O、异步等待任务"改造;② `_socket`/会话五元组是"当前值"字段语义而非会话对象所有——并发双循环的直连迁移路径靠覆盖释放与拆除收敛,集中所有权(内部 Session 对象)是重构目标。

### netstandard2.0 兼容层(2.1.0)

多目标 `netstandard2.0;net8.0;net10.0`:
Expand Down
Loading
Loading