From aa78dfbf5156b15879530dff001fac58656b2f70 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:56:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E4=BF=AE=E5=A4=8D=20PR=20review?= =?UTF-8?q?=20=E5=9B=9E=E5=BD=92=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 CoroutineScheduler 在零初始容量下的扩容边界并补充回归测试 - 修复 Store dispatch 快照阶段的异常回滚逻辑并补充异常安全测试 - 更新 analyzer-warning-reduction 的 tracking 与 trace 以记录 RP-010 验证结果 --- .../Coroutine/CoroutineSchedulerTests.cs | 16 +++- .../StateManagement/StoreTests.cs | 73 ++++++++++++++++++- .../Coroutine/CoroutineScheduler.cs | 6 +- GFramework.Core/StateManagement/Store.cs | 19 +++-- .../analyzer-warning-reduction-tracking.md | 15 +++- .../analyzer-warning-reduction-trace.md | 26 +++++++ 6 files changed, 143 insertions(+), 12 deletions(-) diff --git a/GFramework.Core.Tests/Coroutine/CoroutineSchedulerTests.cs b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerTests.cs index 15fda6f0..b14833ad 100644 --- a/GFramework.Core.Tests/Coroutine/CoroutineSchedulerTests.cs +++ b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerTests.cs @@ -345,6 +345,20 @@ public class CoroutineSchedulerTests Assert.That(_scheduler.ActiveCoroutineCount, Is.EqualTo(10)); } + /// + /// 验证调度器在零初始容量下会在首次启动协程时自动扩容,而不是写入越界。 + /// + [Test] + public void Run_Should_Grow_From_Zero_Initial_Capacity() + { + var scheduler = new CoroutineScheduler(new TestTimeSource(), initialCapacity: 0); + + var handle = scheduler.Run(CreateYieldingCoroutine(new WaitOneFrame())); + + Assert.That(handle.IsValid, Is.True); + Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1)); + } + /// /// 验证协程调度器应该使用提供的时间源 /// @@ -563,4 +577,4 @@ public class TestTimeSource : ITimeSource DeltaTime = 0.1; CurrentTime += DeltaTime; } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/StateManagement/StoreTests.cs b/GFramework.Core.Tests/StateManagement/StoreTests.cs index 8ecbbdfa..b27c251e 100644 --- a/GFramework.Core.Tests/StateManagement/StoreTests.cs +++ b/GFramework.Core.Tests/StateManagement/StoreTests.cs @@ -1,3 +1,6 @@ +using System.Collections; +using System.Reflection; + namespace GFramework.Core.Tests.StateManagement; /// @@ -384,6 +387,32 @@ public class StoreTests Assert.That(logs, Is.EqualTo(new[] { "dynamic:before", "dynamic:after" })); } + /// + /// 测试当 dispatch 作用域在快照阶段抛出异常时,Store 不会残留“正在分发”标记而锁死后续调用。 + /// + [Test] + public void Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws() + { + var store = new Store( + new CounterState(0, "Player"), + actionMatchingMode: StoreActionMatchingMode.IncludeAssignableTypes); + + store.RegisterReducer((state, action) => state with { Count = state.Count + action.Amount }); + + var reducers = GetReducersDictionary(store); + var throwingActionType = new ThrowingAssignableType(typeof(IncrementAction), "simulated reducer snapshot failure"); + reducers.Add(throwingActionType, CreateEmptyReducerRegistrationList(reducers)); + + Assert.That( + () => store.Dispatch(new IncrementAction(1)), + Throws.InvalidOperationException.With.Message.EqualTo("simulated reducer snapshot failure")); + + reducers.Remove(throwingActionType); + + Assert.That(() => store.Dispatch(new IncrementAction(1)), Throws.Nothing); + Assert.That(store.State.Count, Is.EqualTo(1)); + } + /// /// 测试未命中的 action 仍会记录诊断信息,但不会改变状态。 /// @@ -681,6 +710,32 @@ public class StoreTests return store; } + /// + /// 读取 Store 内部 reducer 字典,以便在异常安全回归测试中注入受控的异常源。 + /// + /// 要读取的 Store 实例。 + /// Store 当前持有的 reducer 字典引用。 + private static IDictionary GetReducersDictionary(Store store) + { + var reducersField = typeof(Store).GetField("_reducers", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Unable to locate Store reducer dictionary field."); + + return (IDictionary)(reducersField.GetValue(store) + ?? throw new InvalidOperationException("Store reducer dictionary should not be null.")); + } + + /// + /// 创建与 Store 私有 reducer 注册列表兼容的空列表实例。 + /// + /// 现有 reducer 字典,用于推断私有列表元素类型。 + /// 可写入私有 reducer 字典的空列表。 + private static object CreateEmptyReducerRegistrationList(IDictionary reducers) + { + var valueType = reducers.GetType().GenericTypeArguments[1]; + return Activator.CreateInstance(valueType) + ?? throw new InvalidOperationException("Unable to create an empty reducer registration list."); + } + /// /// 用于测试的计数器状态。 /// 使用 record 保持逻辑不可变语义,便于 Store 基于状态快照进行比较和断言。 @@ -876,4 +931,20 @@ public class StoreTests next(); } } -} \ No newline at end of file + + /// + /// 用于在回归测试中稳定模拟 失败的代理类型。 + /// + private sealed class ThrowingAssignableType(Type delegatingType, string message) : TypeDelegator(delegatingType) + { + /// + /// 在 Store 创建 reducer 快照时抛出受控异常,验证 dispatch 作用域能够正确回滚。 + /// + /// 待比较的 action 运行时类型。 + /// 此实现不会正常返回。 + public override bool IsAssignableFrom(Type? typeInfo) + { + throw new InvalidOperationException(message); + } + } +} diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs index d34e7d54..ece62578 100644 --- a/GFramework.Core/Coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs @@ -16,7 +16,7 @@ namespace GFramework.Core.Coroutine; /// /// 缩放时间源,提供调度器默认推进所使用的时间数据。 /// 协程实例编号,用于生成带宿主前缀的句柄。 -/// 调度器初始槽位容量。 +/// 调度器初始槽位容量;允许为 0,此时首次启动协程会按需自动扩容。 /// 是否启用协程统计功能。 /// /// 非缩放时间源。 @@ -1036,7 +1036,9 @@ public sealed class CoroutineScheduler( /// private void Expand() { - Array.Resize(ref _slots, _slots.Length * 2); + // 允许构造器以 0 容量启动,用于极简场景或测试;首次分配时至少扩到 1,避免后续写槽位越界。 + var expandedLength = Math.Max(1, _slots.Length * 2); + Array.Resize(ref _slots, expandedLength); } /// diff --git a/GFramework.Core/StateManagement/Store.cs b/GFramework.Core/StateManagement/Store.cs index f7484734..a05daa21 100644 --- a/GFramework.Core/StateManagement/Store.cs +++ b/GFramework.Core/StateManagement/Store.cs @@ -828,11 +828,20 @@ public sealed class Store : IStore, IStoreDiagnostics EnsureNotDispatching(); _isDispatching = true; - var context = new StoreDispatchContext(action!, _state); - stateComparerSnapshot = _stateComparer; - middlewaresSnapshot = CreateMiddlewareSnapshotCore(); - reducersSnapshot = CreateReducerSnapshotCore(context.ActionType); - return context; + try + { + var context = new StoreDispatchContext(action!, _state); + stateComparerSnapshot = _stateComparer; + middlewaresSnapshot = CreateMiddlewareSnapshotCore(); + reducersSnapshot = CreateReducerSnapshotCore(context.ActionType); + return context; + } + catch + { + // 进入 dispatch 标记早于快照构建;如果这里抛异常,必须同步回滚标记,避免后续调用被永久判定为嵌套分发。 + _isDispatching = false; + throw; + } } } diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 5ed33a95..550a1e0e 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -7,10 +7,10 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-009` -- 当前阶段:`Phase 9` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-010` +- 当前阶段:`Phase 10` - 当前焦点: - - 当前 `MA0048` 已在 `GFramework.Core` 的 `net8.0` warnings-only 基线中清零;下一轮切到 `MA0046` + - 当前 PR #265 的 follow-up 已收口;下一轮恢复到 `MA0046` 主批次 - 后续继续按 warning 类型和数量批处理,而不是回退到按单文件切片推进 - 当某一轮主类型数量不足时,允许顺手合并其他低冲突 warning 类型,`MA0015` 与 `MA0077` 只是当前最明显的低数量示例,不构成限定 @@ -21,6 +21,7 @@ - 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理 - 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险 +- 已完成当前 PR #265 review follow-up:修复 `CoroutineScheduler` 的零容量扩容边界,并补上 `Store` dispatch 作用域的异常安全回滚 - 当前 `PauseStackManager`、`Store`、`CoroutineScheduler` 与 `GFramework.Core` 的 `MA0048` 文件/类型命名冲突已从 active 入口移除;主题内剩余 warning 主要集中在 `MA0046` delegate 形状、 `MA0016` 集合抽象接口、`MA0002` comparer 重载,以及 `MA0015` / `MA0077` 两个低数量尾项 @@ -41,6 +42,9 @@ 不同模型的 subagent 并行处理 - `RP-009` 在不改公共 API 的前提下,将同名泛型家族收拢到与类型名一致的单文件中,清空当前 `GFramework.Core` `net8.0` 基线中的 `MA0048`,并通过定向 build/test 验证 `Command`、`Query`、`Event` 路径未回归 +- `RP-010` 使用 `gframework-pr-review` 复核当前分支 PR #265 后,修复了仍在本地成立的两个 follow-up 风险: + `CoroutineScheduler` 的 `initialCapacity: 0` 扩容越界,以及 `Store` 在 dispatch 快照阶段抛异常时可能残留 + `_isDispatching = true` 的锁死问题 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -94,6 +98,11 @@ - 结果:`15 Warning(s)`,`0 Error(s)`;当前 `GFramework.Core` `net8.0` warnings-only 输出中已不再出现 `MA0048` - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CommandExecutorTests|FullyQualifiedName~AbstractAsyncCommandTests|FullyQualifiedName~QueryExecutorTests|FullyQualifiedName~AbstractAsyncQueryTests|FullyQualifiedName~EventTests" -m:1 -p:RestoreFallbackFolders="" -nologo` - 结果:`83 Passed`,`0 Failed` +- `RP-010` 的定向验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo` + - 结果:`15 Warning(s)`,`0 Error(s)`;新增修复未引入新的 `GFramework.Core` `net8.0` 构建错误 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`2 Passed`,`0 Failed` - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index 54e82d98..106a7d97 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,5 +1,31 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-21 — RP-010 + +### 阶段:PR #265 follow-up 收口(RP-010) + +- 使用 `gframework-pr-review` 抓取当前分支 PR #265 的 latest head review threads、CodeRabbit review body、MegaLinter 摘要与 CTRF + 测试结果;确认最新 unresolved thread 只剩 `CoroutineScheduler` 零容量扩容边界 +- 本地复核后确认两处仍成立的风险: + - `CoroutineScheduler.Expand()` 在 `_slots.Length == 0` 时会把容量从 `0` 扩到 `0`,首次 `Run` 写槽位会越界 + - `Store.EnterDispatchScope()` 在 `_isDispatching = true` 之后、快照构建完成之前若抛异常,会留下永久的嵌套分发误判 +- 实施最小修复: + - 将 `Expand()` 调整为 `Math.Max(1, _slots.Length * 2)`,保持已有倍增策略,只补上零容量边界 + - 为 `EnterDispatchScope()` 增加快照阶段的异常回滚,确保 `_isDispatching` 与实际 dispatch 生命周期保持一致 + - 新增回归测试覆盖零容量启动路径,以及 dispatch 快照阶段抛错后的可恢复性 +- 当前 PR 信号复核结论: + - CTRF:最新评论显示 `2135 passed / 0 failed` + - MegaLinter:唯一告警仍是 CI 中 `dotnet-format` restore 失败,未发现新的本地代码格式问题 + - 旧 review body 中提到的 `Store` 异常安全问题虽未表现为最新 open thread,但在本地代码中仍可成立,因此一并收口 +- 定向验证命令: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -nologo` + - 结果:`15 Warning(s)`,`0 Error(s)` + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~CoroutineSchedulerTests.Run_Should_Grow_From_Zero_Initial_Capacity|FullyQualifiedName~StoreTests.Dispatch_Should_Reset_Dispatching_Flag_When_Snapshot_Creation_Throws" -m:1 -p:RestoreFallbackFolders="" -nologo` + - 结果:`2 Passed`,`0 Failed` +- 下一步建议: + - 若继续本主题,恢复到 `MA0046` 主批次,不再停留在当前 PR follow-up + - 若 PR review 还出现新线程,继续遵守“只修复当前本地仍成立的问题”的策略 + ## 2026-04-21 — RP-009 ### 阶段:`MA0048` 批次收口(RP-009)