From ff1996e81bce2437d9a827f30cb41b935cffbdb7 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:18:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor(pause):=20=E6=94=B6=E5=8F=A3=20PauseSt?= =?UTF-8?q?ackManager=20=E9=95=BF=E6=96=B9=E6=B3=95=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 PauseStackManager 的销毁与 Pop 流程,拆分锁内状态迁移与锁外通知阶段 - 新增 PauseStackManager 销毁恢复通知回归测试,覆盖多暂停组销毁补发行为 - 更新 analyzer warning reduction 主题的 active tracking 与 trace,记录 RP-005 验证结果和下一恢复点 --- .../Pause/PauseStackManagerTests.cs | 31 +- GFramework.Core/Pause/PauseStackManager.cs | 360 +++++++++++------- .../analyzer-warning-reduction-tracking.md | 29 +- .../analyzer-warning-reduction-trace.md | 23 ++ 4 files changed, 299 insertions(+), 144 deletions(-) diff --git a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs index 4705357f..6e7b0581 100644 --- a/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs +++ b/GFramework.Core.Tests/Pause/PauseStackManagerTests.cs @@ -394,6 +394,35 @@ public class PauseStackManagerTests Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False); } + /// + /// 验证销毁时会向所有仍暂停的组补发恢复通知。 + /// + [Test] + public async Task DestroyAsync_Should_NotifyResumedGroups() + { + var resumedGroups = new List(); + var mockHandler = new MockPauseHandler(); + + _manager.RegisterHandler(mockHandler); + _manager.OnPauseStateChanged += (_, e) => + { + if (!e.IsPaused) + { + resumedGroups.Add(e.Group); + } + }; + + _manager.Push("Global", PauseGroup.Global); + _manager.Push("Gameplay", PauseGroup.Gameplay); + mockHandler.Reset(); + + await _manager.DestroyAsync(); + + Assert.That(mockHandler.CallCount, Is.EqualTo(2)); + Assert.That(mockHandler.LastIsPaused, Is.False); + Assert.That(resumedGroups, Is.EquivalentTo(new[] { PauseGroup.Global, PauseGroup.Gameplay })); + } + /// /// 验证并发Push是线程安全的 /// @@ -469,4 +498,4 @@ public class PauseStackManagerTests LastIsPaused = null; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Pause/PauseStackManager.cs b/GFramework.Core/Pause/PauseStackManager.cs index dd15fe3c..665c2d3a 100644 --- a/GFramework.Core/Pause/PauseStackManager.cs +++ b/GFramework.Core/Pause/PauseStackManager.cs @@ -26,69 +26,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs public ValueTask DestroyAsync() { if (_disposed) + { return ValueTask.CompletedTask; - - List pausedGroups; - IPauseHandler[] handlersSnapshot; - - _lock.EnterWriteLock(); - try - { - if (_disposed) - return ValueTask.CompletedTask; - - _disposed = true; - - // 收集所有当前暂停的组 - pausedGroups = _pauseStacks - .Where(kvp => kvp.Value.Count > 0) - .Select(kvp => kvp.Key) - .ToList(); - - // 获取处理器快照 - handlersSnapshot = _handlers.ToArray(); - - // 清理所有数据结构 - _pauseStacks.Clear(); - _tokenMap.Clear(); - _handlers.Clear(); - - _logger.Debug("PauseStackManager destroyed"); - } - finally - { - _lock.ExitWriteLock(); } - // 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致 - foreach (var group in pausedGroups) + var destroySnapshot = TryBeginDestroy(); + if (destroySnapshot == null) { - _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false"); - - foreach (var handler in handlersSnapshot.OrderBy(h => h.Priority)) - { - try - { - handler.OnPauseStateChanged(group, false); - } - catch (Exception ex) - { - _logger.Error($"Handler {handler.GetType().Name} failed during destruction", ex); - } - } - - // 触发事件 - try - { - RaisePauseStateChanged(group, false); - } - catch (Exception ex) - { - _logger.Error($"Event subscriber failed during destruction for group {group}", ex); - } + return ValueTask.CompletedTask; } - // 释放锁资源 + NotifyDestroyedGroups(destroySnapshot.Value); _lock.Dispose(); return ValueTask.CompletedTask; @@ -163,74 +111,17 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs public bool Pop(PauseToken token) { if (!token.IsValid) + { return false; - - bool found; - bool shouldNotify = false; - PauseGroup notifyGroup = PauseGroup.Global; - - _lock.EnterWriteLock(); - try - { - ThrowIfDisposed(); - - if (!_tokenMap.TryGetValue(token.Id, out var entry)) - { - _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}"); - return false; - } - - var group = entry.Group; - var stack = _pauseStacks[group]; - var wasPaused = stack.Count > 0; - - // 从栈中移除 - var tempStack = new Stack(); - found = false; - - while (stack.Count > 0) - { - var current = stack.Pop(); - if (current.TokenId == token.Id) - { - found = true; - break; - } - - tempStack.Push(current); - } - - // 恢复栈结构 - while (tempStack.Count > 0) - { - stack.Push(tempStack.Pop()); - } - - if (found) - { - _tokenMap.Remove(token.Id); - _logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})"); - - // 状态变化检测:从暂停 → 未暂停 - if (wasPaused && stack.Count == 0) - { - shouldNotify = true; - notifyGroup = group; - } - } - } - finally - { - _lock.ExitWriteLock(); } - // 在锁外通知处理器,避免死锁 - if (shouldNotify) + var result = TryPopEntry(token); + if (result.ShouldNotify) { - NotifyHandlers(notifyGroup, false); + NotifyHandlers(result.NotifyGroup, false); } - return found; + return result.Found; } /// @@ -443,6 +334,200 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs } } + /// + /// 采集销毁所需的快照并清空内部状态。 + /// + /// + /// 成功进入销毁阶段时返回销毁快照;如果其他线程已先完成销毁,则返回 。 + /// + /// + /// 该方法只负责锁内状态迁移,把外部回调与事件派发留到锁外执行, + /// 以避免在生命周期结束阶段持锁调用用户代码。 + /// + private DestroySnapshot? TryBeginDestroy() + { + _lock.EnterWriteLock(); + try + { + if (_disposed) + { + return null; + } + + _disposed = true; + + var pausedGroups = CollectPausedGroups(); + var handlersSnapshot = CreateHandlerSnapshot(); + + _pauseStacks.Clear(); + _tokenMap.Clear(); + _handlers.Clear(); + + _logger.Debug("PauseStackManager destroyed"); + + return new DestroySnapshot(pausedGroups, handlersSnapshot); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// 在销毁后向所有先前处于暂停状态的分组补发恢复通知。 + /// + /// 销毁阶段采集的分组与处理器快照。 + private void NotifyDestroyedGroups(DestroySnapshot destroySnapshot) + { + foreach (var group in destroySnapshot.PausedGroups) + { + _logger.Debug($"Notifying handlers of destruction: Group={group}, IsPaused=false"); + + NotifyHandlersSnapshot(group, false, destroySnapshot.HandlersSnapshot, isDestroying: true); + RaiseDestroyStateChanged(group); + } + } + + /// + /// 在锁内执行令牌移除,并返回锁外通知所需的信息。 + /// + /// 要移除的暂停令牌。 + /// 包含本次弹出结果和后续通知决策的快照。 + /// + /// Pop 支持移除非栈顶令牌,因此这里会先临时转移栈元素,再恢复原有顺序, + /// 只在最后一个暂停请求被移除时触发恢复通知。 + /// + private PopResult TryPopEntry(PauseToken token) + { + _lock.EnterWriteLock(); + try + { + ThrowIfDisposed(); + + if (!_tokenMap.TryGetValue(token.Id, out var entry)) + { + _logger.Warn($"Attempted to pop invalid/expired token: {token.Id}"); + return PopResult.NotFound; + } + + var stack = _pauseStacks[entry.Group]; + var wasPaused = stack.Count > 0; + var found = RemoveEntryFromStack(stack, token.Id); + if (!found) + { + return PopResult.NotFound; + } + + _tokenMap.Remove(token.Id); + _logger.Debug($"Pause popped: {entry.Reason} (Group: {entry.Group}, Remaining: {stack.Count})"); + + return new PopResult(true, wasPaused && stack.Count == 0, entry.Group); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// 从指定暂停栈中移除目标令牌,并保持其他暂停请求的原始顺序。 + /// + /// 要修改的暂停栈。 + /// 目标令牌标识。 + /// 如果找到了目标令牌则返回 + private static bool RemoveEntryFromStack(Stack stack, Guid tokenId) + { + var tempStack = new Stack(); + var found = false; + + while (stack.Count > 0) + { + var current = stack.Pop(); + if (current.TokenId == tokenId) + { + found = true; + break; + } + + tempStack.Push(current); + } + + while (tempStack.Count > 0) + { + stack.Push(tempStack.Pop()); + } + + return found; + } + + /// + /// 收集当前仍处于暂停状态的分组列表。 + /// + /// 包含所有暂停中的分组的数组。 + private PauseGroup[] CollectPausedGroups() + { + return _pauseStacks + .Where(kvp => kvp.Value.Count > 0) + .Select(kvp => kvp.Key) + .ToArray(); + } + + /// + /// 按优先级创建处理器快照,确保锁外通知仍保持确定性顺序。 + /// + /// 已按优先级排序的处理器快照。 + private IPauseHandler[] CreateHandlerSnapshot() + { + return _handlers + .OrderBy(handler => handler.Priority) + .ToArray(); + } + + /// + /// 统一使用给定的处理器快照派发暂停状态变化通知。 + /// + /// 发生状态变化的暂停组。 + /// 新的暂停状态。 + /// 要通知的处理器快照。 + /// 是否处于销毁补发路径。 + private void NotifyHandlersSnapshot( + PauseGroup group, + bool isPaused, + IReadOnlyList handlersSnapshot, + bool isDestroying) + { + foreach (var handler in handlersSnapshot) + { + try + { + handler.OnPauseStateChanged(group, isPaused); + } + catch (Exception ex) + { + var message = isDestroying + ? $"Handler {handler.GetType().Name} failed during destruction" + : $"Handler {handler.GetType().Name} failed"; + _logger.Error(message, ex); + } + } + } + + /// + /// 在销毁路径中独立保护事件通知,避免订阅方异常中断其他分组的恢复信号。 + /// + /// 需要补发恢复事件的暂停组。 + private void RaiseDestroyStateChanged(PauseGroup group) + { + try + { + RaisePauseStateChanged(group, false); + } + catch (Exception ex) + { + _logger.Error($"Event subscriber failed during destruction for group {group}", ex); + } + } + /// /// 内部查询暂停状态的方法,不加锁。 /// @@ -467,7 +552,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs _lock.EnterReadLock(); try { - handlersSnapshot = _handlers.OrderBy(h => h.Priority).ToArray(); + handlersSnapshot = CreateHandlerSnapshot(); } finally { @@ -475,17 +560,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs } // 在锁外遍历快照并通知处理器 - foreach (var handler in handlersSnapshot) - { - try - { - handler.OnPauseStateChanged(group, isPaused); - } - catch (Exception ex) - { - _logger.Error($"Handler {handler.GetType().Name} failed", ex); - } - } + NotifyHandlersSnapshot(group, isPaused, handlersSnapshot, isDestroying: false); // 触发事件 RaisePauseStateChanged(group, isPaused); @@ -508,4 +583,25 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs protected override void OnInit() { } -} \ No newline at end of file + + /// + /// 锁内采集的销毁快照,供锁外补发恢复通知使用。 + /// + /// 销毁前仍处于暂停状态的分组。 + /// 按优先级排序后的处理器快照。 + private readonly record struct DestroySnapshot(PauseGroup[] PausedGroups, IPauseHandler[] HandlersSnapshot); + + /// + /// Pop 操作的锁内结果快照。 + /// + /// 是否成功移除了目标令牌。 + /// 是否需要在锁外发出恢复通知。 + /// 需要通知的暂停组。 + private readonly record struct PopResult(bool Found, bool ShouldNotify, PauseGroup NotifyGroup) + { + /// + /// 表示未找到目标令牌时的默认结果。 + /// + public static PopResult NotFound { get; } = new(false, false, PauseGroup.Global); + } +} 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 4586470a..2e7b710f 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,21 +7,22 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-004` -- 当前阶段:`Phase 4` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-005` +- 当前阶段:`Phase 5` - 当前焦点: - - 已完成当前分支 PR #263 的最新 review follow-up,本地确认并修复 `GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs` - 的 `null!` 可空契约问题,同时消除 active trace 的重复标题 `MD024` - - 已确认 PR 上的测试信号为 `2134 Passed / 0 Failed`;MegaLinter 唯一告警来自 CI 中 `dotnet-format` restore 失败, - 当前本地 follow-up 不需要额外处理 - - 下一轮若继续推进,优先从 `PauseStackManager`、`CoroutineScheduler` 或 `Store` 的剩余 `MA0051` 中只选一个切入点 + - 已完成 `GFramework.Core/Pause/PauseStackManager.cs` 的 `MA0051` 收口:将 `DestroyAsync` 与 `Pop` 拆分为锁内状态迁移、 + 栈调整和锁外通知三个阶段,同时保持日志、事件与销毁补发语义不变 + - 已为销毁路径补充 `PauseStackManagerTests.DestroyAsync_Should_NotifyResumedGroups` 回归测试,覆盖“销毁时向所有仍暂停组补发恢复通知” + - 下一轮若继续推进,优先在 `CoroutineScheduler` 或 `Store` 的剩余 `MA0051` 中只选一个切入点,不回到已完成的 + `PauseStackManager` ## 当前状态摘要 - 已完成 `GFramework.Core`、`GFramework.Cqrs`、`GFramework.Godot` 与部分 source generator 的低风险 warning 清理 - 已完成多轮 CodeRabbit follow-up 修复,并用定向测试与项目/解决方案构建验证了关键回归风险 -- 当前 `GFramework.Cqrs` 的剩余 warning 热点已从 active 入口移除;主题内剩余 warning 主要集中在 `GFramework.Core` 长方法、 - 文件/类型命名冲突、delegate 形状和少量公共集合抽象接口问题 +- 当前 `PauseStackManager` 的长方法 warning 已从 active 入口移除;主题内剩余 warning 主要集中在 + `GFramework.Core/Coroutine/CoroutineScheduler.cs`、`GFramework.Core/StateManagement/Store.cs`、文件/类型命名冲突、 + delegate 形状和少量公共集合抽象接口问题 ## 当前活跃事实 @@ -30,6 +31,7 @@ - `RP-002` 已在不改公共契约的前提下完成 `CqrsHandlerRegistrar` 结构拆分,并通过定向 build/test 验证 - `RP-003` 已在不改生命周期契约的前提下完成 `ArchitectureLifecycle` 初始化主流程拆分,并通过定向 build/test 验证 - `RP-004` 已完成当前 PR review follow-up:修复 `TryCreateGeneratedRegistry` 的可空 `out` 契约并清理 trace 文档重复标题 +- `RP-005` 已在不改公共 API 的前提下完成 `PauseStackManager` 两个 `MA0051` 的结构拆分,并补充销毁通知回归测试 - 当前工作树分支 `fix/analyzer-warning-reduction-batch` 已在 `ai-plan/public/README.md` 建立 topic 映射 ## 当前风险 @@ -58,11 +60,16 @@ - `RP-004` 的定向验证结果: - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:UseSharedCompilation=false -p:RestoreFallbackFolders=` - 结果:`0 Warning(s)`,`0 Error(s)` +- `RP-005` 的定向验证结果: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表中 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=` + - 结果:`25 Passed`,`0 Failed` - active 跟踪文件只保留当前恢复点、活跃事实、风险与下一步,不再重复保存已完成阶段的长篇历史 ## 下一步 1. 若要继续该主题,先读 active tracking,再按需展开历史归档中的 warning 热点与验证记录 -2. 优先在 `GFramework.Core/Pause/PauseStackManager.cs`、`GFramework.Core/Coroutine/CoroutineScheduler.cs` 与 - `GFramework.Core/StateManagement/Store.cs` 的 `MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面 +2. 优先在 `GFramework.Core/Coroutine/CoroutineScheduler.cs` 与 `GFramework.Core/StateManagement/Store.cs` + 的 `MA0051` 中只选一个继续,不要在同一轮同时扩多个风险面 3. 若本主题确认暂缓,可保持当前归档状态,不需要再恢复 `local-plan/` 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 58f5485b..33fb83c8 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,28 @@ # Analyzer Warning Reduction 追踪 +## 2026-04-21 — RP-005 + +### 阶段:PauseStackManager `MA0051` 收口(RP-005) + +- 按 active tracking 中“继续只选一个 `GFramework.Core` 结构性切入点”的约束,本轮选择 + `GFramework.Core/Pause/PauseStackManager.cs`,因为该文件体量明显小于 `CoroutineScheduler` 和 `Store`, + 且已有稳定的 `PauseStackManagerTests` 覆盖暂停栈、跨组独立性、事件通知与并发 `Push/Pop` 行为 +- 先用 `warnings-only` 定向构建确认 `DestroyAsync` 与 `Pop` 仍分别命中 `MA0051`,再把逻辑拆分为: + - `TryBeginDestroy` + - `NotifyDestroyedGroups` + - `TryPopEntry` + - `RemoveEntryFromStack` +- 额外抽出 `CreateHandlerSnapshot` 与 `NotifyHandlersSnapshot`,统一普通通知与销毁补发路径的处理器排序和异常日志, + 保持原有“锁内采集快照、锁外调用处理器与事件”的并发策略不变 +- 为销毁路径新增 `DestroyAsync_Should_NotifyResumedGroups`,验证当多个暂停组在销毁前仍为暂停态时, + 处理器和事件订阅者都会收到 `IsPaused=false` 的恢复信号 +- 验证通过: + - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release -t:Rebuild --no-restore -p:UseSharedCompilation=false -p:TargetFramework=net8.0 -p:RestoreFallbackFolders= -nologo -clp:"Summary;WarningsOnly"` + - 结果:`27 Warning(s)`,`0 Error(s)`;`PauseStackManager.cs` 已不再出现在 `MA0051` 列表 + - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter FullyQualifiedName~PauseStackManagerTests -p:RestoreFallbackFolders=` + - 结果:`25 Passed`,`0 Failed` +- 下一步保持原节奏:只在 `CoroutineScheduler` 或 `Store` 中二选一继续,不与其他 warning 家族混做 + ## 2026-04-21 — RP-003 ### 阶段:Architecture 生命周期 `MA0051` 收口(RP-003)