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)