Skip to content

Feature/pause stack manager#64

Merged
GeWuYou merged 4 commits into
mainfrom
feature/pause-stack-manager
Mar 2, 2026
Merged

Feature/pause stack manager#64
GeWuYou merged 4 commits into
mainfrom
feature/pause-stack-manager

Conversation

@GeWuYou

@GeWuYou GeWuYou commented Mar 1, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

引入一个支持分组的基于令牌的暂停栈管理系统,并集成 Godot,同时提供全面的测试以及函数式命名空间规范化。

New Features:

  • 添加抽象与实现,用于一个分组的、基于令牌的暂停栈管理器,支持嵌套暂停、作用域生命周期以及暂停状态事件。
  • 提供一个 Godot 专用的暂停处理器,将暂停栈状态桥接到 SceneTree 的全局暂停。

Enhancements:

  • 规范与 functional 相关的命名空间和测试命名空间,统一为小写的 functional.* 约定。

Tests:

  • PauseStackManager 添加大量单元测试,覆盖令牌、分组、作用域暂停、处理器、事件、清理以及并发操作。
Original summary in English

Summary by Sourcery

Introduce a token-based pause stack management system with group support and Godot integration, along with comprehensive tests and functional namespace normalization.

New Features:

  • Add abstractions and implementation for a grouped, token-based pause stack manager supporting nested pauses, scoped lifetimes, and pause state events.
  • Provide a Godot-specific pause handler that bridges pause stack state to SceneTree global pause.

Enhancements:

  • Normalize functional-related namespaces and test namespaces to the lowercased functional.* convention.

Tests:

  • Add extensive unit tests for PauseStackManager covering tokens, grouping, scoped pauses, handlers, events, cleanup, and concurrent operations.
Original summary in English

Summary by Sourcery

引入一个支持分组的基于令牌的暂停栈管理系统,并集成 Godot,同时提供全面的测试以及函数式命名空间规范化。

New Features:

  • 添加抽象与实现,用于一个分组的、基于令牌的暂停栈管理器,支持嵌套暂停、作用域生命周期以及暂停状态事件。
  • 提供一个 Godot 专用的暂停处理器,将暂停栈状态桥接到 SceneTree 的全局暂停。

Enhancements:

  • 规范与 functional 相关的命名空间和测试命名空间,统一为小写的 functional.* 约定。

Tests:

  • PauseStackManager 添加大量单元测试,覆盖令牌、分组、作用域暂停、处理器、事件、清理以及并发操作。
Original summary in English

Summary by Sourcery

Introduce a token-based pause stack management system with group support and Godot integration, along with comprehensive tests and functional namespace normalization.

New Features:

  • Add abstractions and implementation for a grouped, token-based pause stack manager supporting nested pauses, scoped lifetimes, and pause state events.
  • Provide a Godot-specific pause handler that bridges pause stack state to SceneTree global pause.

Enhancements:

  • Normalize functional-related namespaces and test namespaces to the lowercased functional.* convention.

Tests:

  • Add extensive unit tests for PauseStackManager covering tokens, grouping, scoped pauses, handlers, events, cleanup, and concurrent operations.

新特性:

  • 添加 IPauseStackManagerPauseTokenPauseGroupIPauseHandler 抽象,用于建模基于令牌、分组的暂停状态管理,并支持事件通知。
  • 实现 PauseStackManagerPauseScope,用于按分组管理嵌套暂停栈,暴露暂停原因和深度,并提供基于作用域的暂停生命周期管理。
  • 添加 GodotPauseHandler,用于在暂停栈管理器与 Godot 的 SceneTree 全局暂停状态之间建立桥接。

改进:

  • 规范功能命名空间及相关测试命名空间/using 为小写的 functional.* 命名约定。

测试:

  • 新增全面的 PauseStackManager 单元测试,覆盖令牌有效性、嵌套与分组暂停、处理程序与事件回调、基于作用域的暂停、清理行为以及并发压栈/弹栈操作。
Original summary in English

Summary by Sourcery

引入一个支持分组的基于令牌的暂停栈管理系统,并集成 Godot,同时提供全面的测试以及函数式命名空间规范化。

New Features:

  • 添加抽象与实现,用于一个分组的、基于令牌的暂停栈管理器,支持嵌套暂停、作用域生命周期以及暂停状态事件。
  • 提供一个 Godot 专用的暂停处理器,将暂停栈状态桥接到 SceneTree 的全局暂停。

Enhancements:

  • 规范与 functional 相关的命名空间和测试命名空间,统一为小写的 functional.* 约定。

Tests:

  • PauseStackManager 添加大量单元测试,覆盖令牌、分组、作用域暂停、处理器、事件、清理以及并发操作。
Original summary in English

Summary by Sourcery

Introduce a token-based pause stack management system with group support and Godot integration, along with comprehensive tests and functional namespace normalization.

New Features:

  • Add abstractions and implementation for a grouped, token-based pause stack manager supporting nested pauses, scoped lifetimes, and pause state events.
  • Provide a Godot-specific pause handler that bridges pause stack state to SceneTree global pause.

Enhancements:

  • Normalize functional-related namespaces and test namespaces to the lowercased functional.* convention.

Tests:

  • Add extensive unit tests for PauseStackManager covering tokens, grouping, scoped pauses, handlers, events, cleanup, and concurrent operations.
Original summary in English

Summary by Sourcery

引入一个支持分组的基于令牌的暂停栈管理系统,并集成 Godot,同时提供全面的测试以及函数式命名空间规范化。

New Features:

  • 添加抽象与实现,用于一个分组的、基于令牌的暂停栈管理器,支持嵌套暂停、作用域生命周期以及暂停状态事件。
  • 提供一个 Godot 专用的暂停处理器,将暂停栈状态桥接到 SceneTree 的全局暂停。

Enhancements:

  • 规范与 functional 相关的命名空间和测试命名空间,统一为小写的 functional.* 约定。

Tests:

  • PauseStackManager 添加大量单元测试,覆盖令牌、分组、作用域暂停、处理器、事件、清理以及并发操作。
Original summary in English

Summary by Sourcery

Introduce a token-based pause stack management system with group support and Godot integration, along with comprehensive tests and functional namespace normalization.

New Features:

  • Add abstractions and implementation for a grouped, token-based pause stack manager supporting nested pauses, scoped lifetimes, and pause state events.
  • Provide a Godot-specific pause handler that bridges pause stack state to SceneTree global pause.

Enhancements:

  • Normalize functional-related namespaces and test namespaces to the lowercased functional.* convention.

Tests:

  • Add extensive unit tests for PauseStackManager covering tokens, grouping, scoped pauses, handlers, events, cleanup, and concurrent operations.

@deepsource-io

deepsource-io Bot commented Mar 1, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in e391bab...0534a27 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade  

Focus Area: Reliability
Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
C# Mar 2, 2026 1:47p.m. Review ↗
Secrets Mar 2, 2026 1:47p.m. Review ↗

@sourcery-ai

sourcery-ai Bot commented Mar 1, 2026

Copy link
Copy Markdown

审阅者指南

实现了一个基于 tokengroupPauseStackManager,包含抽象层和 Godot 集成;同时规范了与 functional 相关的命名空间/using,并为暂停行为与并发添加了完备的单元测试。

新暂停管理系统的类图

classDiagram
    direction LR

    class IContextUtility
    class IAsyncDestroyable {
        +DestroyAsync() ValueTask
    }

    class IPauseStackManager {
        <<interface>>
        +Push(reason string, group PauseGroup) PauseToken
        +Pop(token PauseToken) bool
        +IsPaused(group PauseGroup) bool
        +GetPauseDepth(group PauseGroup) int
        +GetPauseReasons(group PauseGroup) IReadOnlyList~string~
        +PauseScope(reason string, group PauseGroup) IDisposable
        +ClearGroup(group PauseGroup) void
        +ClearAll() void
        +RegisterHandler(handler IPauseHandler) void
        +UnregisterHandler(handler IPauseHandler) void
        +OnPauseStateChanged Action~PauseGroup,bool~
    }

    class IPauseHandler {
        <<interface>>
        +Priority int
        +OnPauseStateChanged(group PauseGroup, isPaused bool) void
    }

    class PauseToken {
        +Id Guid
        +IsValid bool
        +PauseToken(id Guid)
        +Invalid PauseToken
        +Equals(other PauseToken) bool
        +Equals(obj object) bool
        +GetHashCode() int
        +ToString() string
    }

    class PauseGroup {
        <<enum>>
        Global
        Gameplay
        Animation
        Audio
        Custom1
        Custom2
        Custom3
    }

    class PauseEntry {
        -TokenId Guid
        -Reason string
        -Group PauseGroup
        -Timestamp DateTime
    }

    class AbstractContextUtility {
        #OnInit() void
    }

    class ILogger {
        <<interface>>
        +Debug(message string) void
        +Warn(message string) void
        +Error(message string, ex Exception) void
    }

    class PauseStackManager {
        -handlers List~IPauseHandler~
        -lock ReaderWriterLockSlim
        -logger ILogger
        -pauseStacks Dictionary~PauseGroup,Stack~PauseEntry~~
        -tokenMap Dictionary~Guid,PauseEntry~
        -disposed bool
        +OnPauseStateChanged Action~PauseGroup,bool~
        +DestroyAsync() ValueTask
        +Push(reason string, group PauseGroup) PauseToken
        +Pop(token PauseToken) bool
        +IsPaused(group PauseGroup) bool
        +GetPauseDepth(group PauseGroup) int
        +GetPauseReasons(group PauseGroup) IReadOnlyList~string~
        +PauseScope(reason string, group PauseGroup) IDisposable
        +ClearGroup(group PauseGroup) void
        +ClearAll() void
        +RegisterHandler(handler IPauseHandler) void
        +UnregisterHandler(handler IPauseHandler) void
        -ThrowIfDisposed() void
        -IsPausedInternal(group PauseGroup) bool
        -NotifyHandlers(group PauseGroup, isPaused bool) void
        #OnInit() void
    }

    class PauseScope {
        -manager IPauseStackManager
        -token PauseToken
        -disposed bool
        +PauseScope(manager IPauseStackManager, reason string, group PauseGroup)
        +Dispose() void
        #Dispose(disposing bool) void
    }

    class GodotPauseHandler {
        -tree SceneTree
        +Priority int
        +GodotPauseHandler(tree SceneTree)
        +OnPauseStateChanged(group PauseGroup, isPaused bool) void
    }

    IPauseStackManager ..|> IContextUtility
    PauseStackManager ..|> IPauseStackManager
    PauseStackManager ..|> IAsyncDestroyable
    PauseStackManager --|> AbstractContextUtility

    PauseStackManager o--> PauseEntry
    PauseStackManager o--> PauseToken
    PauseStackManager o--> PauseGroup
    PauseStackManager o--> IPauseHandler
    PauseStackManager --> ILogger

    PauseScope --> IPauseStackManager
    PauseScope --> PauseToken
    PauseScope --> PauseGroup

    GodotPauseHandler ..|> IPauseHandler
    GodotPauseHandler --> PauseGroup

    IPauseHandler --> PauseGroup
    IPauseStackManager --> PauseToken
    IPauseStackManager --> PauseGroup
Loading

文件级变更

变更 详情 文件
引入与引擎无关的暂停抽象和相关支持类型,用于建模按分组的、基于 token 的暂停状态及其处理器。
  • 定义 IPauseStackManager 接口,用于 push/pop、状态查询、作用域暂停、清理单个分组/全部分组、处理器注册,以及暂停状态事件。
  • 添加 PauseToken 值类型,用于唯一标识暂停请求并表示其有效性,包括一个 Invalid 标记值。
  • 添加 PauseGroup 枚举,用于表示全局、游戏玩法、动画、音频以及自定义的暂停分组。
  • 定义 IPauseHandler 接口,包含 Priority 优先级和 OnPauseStateChanged 回调,用于引擎层的暂停处理。
GFramework.Core.Abstractions/pause/IPauseStackManager.cs
GFramework.Core.Abstractions/pause/PauseToken.cs
GFramework.Core.Abstractions/pause/PauseGroup.cs
GFramework.Core.Abstractions/pause/IPauseHandler.cs
提供一个具体的、线程安全的暂停栈管理器实现,支持作用域暂停及内部数据结构。
  • 实现 PauseStackManager,使用按分组的栈、token 映射和处理器列表,并通过 ReaderWriterLockSlim 控制并发。
  • 实现 Push/Pop 来管理 PauseEntry 栈,支持移除非栈顶的 token,并仅在状态发生变化时触发通知。
  • 暴露查询 API,用于获取暂停状态、深度和原因,并支持按分组或全部清理,并进行相应通知。
  • 通过 DestroyAsyncObjectDisposedException 检查管理生命周期,防止销毁后的继续使用,并为操作和处理器失败集成日志记录。
  • 添加内部 PauseEntry 类表示栈条目,以及 PauseScope 可释放对象,用于基于 using 的作用域暂停。
GFramework.Core/pause/PauseStackManager.cs
GFramework.Core/pause/PauseEntry.cs
GFramework.Core/pause/PauseScope.cs
通过实现处理器,将暂停系统与 Godot 集成,把全局暂停状态映射到 SceneTree.Paused
  • 实现 GodotPauseHandler,监听暂停事件,并基于 Global 暂停分组设置 SceneTree.Paused
  • 通过 GD.Print 记录 SceneTree 暂停状态变化,并遵循处理器 Priority 的语义。
GFramework.Godot/pause/GodotPauseHandler.cs
将 functional 命名空间及测试中的命名空间/using 统一规范为小写的 functional.* 约定。
  • OptionResult、异步扩展和函数扩展中,将核心 functional 命名空间从 GFramework.Core.Functional* 改为 GFramework.Core.functional*
  • 更新测试中的 using 和命名空间以引用新的小写 functional 命名空间,并与文件夹命名(extensions/functional)保持一致。
  • 移除已不再需要的旧 Functional 命名空间的冗余 using。
GFramework.Core/functional/Option.cs
GFramework.Core/functional/Result.T.cs
GFramework.Core/functional/Result.cs
GFramework.Core/functional/async/AsyncFunctionalExtensions.cs
GFramework.Core/functional/result/ResultExtensions.cs
GFramework.Core/functional/functions/FunctionExtensions.cs
GFramework.Core.Tests/extensions/ResultExtensionsTests.cs
GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs
GFramework.Core.Tests/functional/OptionTests.cs
GFramework.Core.Tests/functional/ResultTTests.cs
GFramework.Core.Tests/functional/ResultTests.cs
PauseStackManager 添加覆盖正确性、事件、处理器、作用域和并发行为的完整单元测试。
  • 测试 token 有效性、针对无效/过期 token 的 pop 行为,以及移除非栈顶 token 时对剩余条目的保持。
  • 验证 IsPausedGetPauseDepthGetPauseReasons 在单分组和多分组场景下的行为,包括空栈时的表现。
  • 确保事件和处理器仅在状态变化时触发,遵循优先级顺序,并在暂停与恢复时行为正确。
  • 验证 PauseScopeDispose 时自动执行 pop,及嵌套作用域对深度的正确调整。
  • 测试 ClearGroupClearAll 的语义,并通过使用任务和 mock 处理器实现验证并发 push/pop 操作的线程安全性。
GFramework.Core.Tests/pause/PauseStackManagerTests.cs

技巧与命令

与 Sourcery 交互

  • 触发新的审查: 在 Pull Request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在某条审查评论下回复,请求 Sourcery 从该评论创建一个 issue。你也可以直接回复该评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 Pull Request 标题: 在 Pull Request 标题的任意位置写上 @sourcery-ai,即可在任意时间生成标题。也可以在 Pull Request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 Pull Request 摘要: 在 Pull Request 正文的任意位置写上 @sourcery-ai summary,即可在你希望的位置生成 PR 摘要。也可以评论 @sourcery-ai summary 来在任意时间(重新)生成摘要。
  • 生成审阅者指南: 在 Pull Request 中评论 @sourcery-ai guide,可在任意时间(重新)生成审阅者指南。
  • 解决所有 Sourcery 评论: 在 Pull Request 中评论 @sourcery-ai resolve,将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不想再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在 Pull Request 中评论 @sourcery-ai dismiss,忽略所有现有的 Sourcery 审查。特别适合在你希望从一次全新的审查开始时使用——记得之后评论 @sourcery-ai review 来触发新的审查!

自定义你的体验

访问你的 控制面板 以:

  • 启用或禁用诸如 Sourcery 生成的 Pull Request 摘要、审阅者指南等审查功能。
  • 修改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查相关设置。

获取帮助

Original review guide in English

Reviewer's Guide

Implements a token- and group-based PauseStackManager with abstractions and Godot integration, plus normalizes functional-related namespaces/usings and adds comprehensive unit tests for pause behavior and concurrency.

Class diagram for the new pause management system

classDiagram
    direction LR

    class IContextUtility
    class IAsyncDestroyable {
        +DestroyAsync() ValueTask
    }

    class IPauseStackManager {
        <<interface>>
        +Push(reason string, group PauseGroup) PauseToken
        +Pop(token PauseToken) bool
        +IsPaused(group PauseGroup) bool
        +GetPauseDepth(group PauseGroup) int
        +GetPauseReasons(group PauseGroup) IReadOnlyList~string~
        +PauseScope(reason string, group PauseGroup) IDisposable
        +ClearGroup(group PauseGroup) void
        +ClearAll() void
        +RegisterHandler(handler IPauseHandler) void
        +UnregisterHandler(handler IPauseHandler) void
        +OnPauseStateChanged Action~PauseGroup,bool~
    }

    class IPauseHandler {
        <<interface>>
        +Priority int
        +OnPauseStateChanged(group PauseGroup, isPaused bool) void
    }

    class PauseToken {
        +Id Guid
        +IsValid bool
        +PauseToken(id Guid)
        +Invalid PauseToken
        +Equals(other PauseToken) bool
        +Equals(obj object) bool
        +GetHashCode() int
        +ToString() string
    }

    class PauseGroup {
        <<enum>>
        Global
        Gameplay
        Animation
        Audio
        Custom1
        Custom2
        Custom3
    }

    class PauseEntry {
        -TokenId Guid
        -Reason string
        -Group PauseGroup
        -Timestamp DateTime
    }

    class AbstractContextUtility {
        #OnInit() void
    }

    class ILogger {
        <<interface>>
        +Debug(message string) void
        +Warn(message string) void
        +Error(message string, ex Exception) void
    }

    class PauseStackManager {
        -handlers List~IPauseHandler~
        -lock ReaderWriterLockSlim
        -logger ILogger
        -pauseStacks Dictionary~PauseGroup,Stack~PauseEntry~~
        -tokenMap Dictionary~Guid,PauseEntry~
        -disposed bool
        +OnPauseStateChanged Action~PauseGroup,bool~
        +DestroyAsync() ValueTask
        +Push(reason string, group PauseGroup) PauseToken
        +Pop(token PauseToken) bool
        +IsPaused(group PauseGroup) bool
        +GetPauseDepth(group PauseGroup) int
        +GetPauseReasons(group PauseGroup) IReadOnlyList~string~
        +PauseScope(reason string, group PauseGroup) IDisposable
        +ClearGroup(group PauseGroup) void
        +ClearAll() void
        +RegisterHandler(handler IPauseHandler) void
        +UnregisterHandler(handler IPauseHandler) void
        -ThrowIfDisposed() void
        -IsPausedInternal(group PauseGroup) bool
        -NotifyHandlers(group PauseGroup, isPaused bool) void
        #OnInit() void
    }

    class PauseScope {
        -manager IPauseStackManager
        -token PauseToken
        -disposed bool
        +PauseScope(manager IPauseStackManager, reason string, group PauseGroup)
        +Dispose() void
        #Dispose(disposing bool) void
    }

    class GodotPauseHandler {
        -tree SceneTree
        +Priority int
        +GodotPauseHandler(tree SceneTree)
        +OnPauseStateChanged(group PauseGroup, isPaused bool) void
    }

    IPauseStackManager ..|> IContextUtility
    PauseStackManager ..|> IPauseStackManager
    PauseStackManager ..|> IAsyncDestroyable
    PauseStackManager --|> AbstractContextUtility

    PauseStackManager o--> PauseEntry
    PauseStackManager o--> PauseToken
    PauseStackManager o--> PauseGroup
    PauseStackManager o--> IPauseHandler
    PauseStackManager --> ILogger

    PauseScope --> IPauseStackManager
    PauseScope --> PauseToken
    PauseScope --> PauseGroup

    GodotPauseHandler ..|> IPauseHandler
    GodotPauseHandler --> PauseGroup

    IPauseHandler --> PauseGroup
    IPauseStackManager --> PauseToken
    IPauseStackManager --> PauseGroup
Loading

File-Level Changes

Change Details Files
Introduce engine-agnostic pause abstractions and supporting types to model grouped, token-based pause state with handlers.
  • Define IPauseStackManager interface for push/pop, query, scoped pauses, clearing groups/all, handler registration, and pause state events.
  • Add PauseToken value type to uniquely identify pause requests and represent validity, including an Invalid sentinel.
  • Add PauseGroup enum to represent global, gameplay, animation, audio, and custom pause groups.
  • Define IPauseHandler interface with Priority and OnPauseStateChanged callback for engine-layer pause handling.
GFramework.Core.Abstractions/pause/IPauseStackManager.cs
GFramework.Core.Abstractions/pause/PauseToken.cs
GFramework.Core.Abstractions/pause/PauseGroup.cs
GFramework.Core.Abstractions/pause/IPauseHandler.cs
Provide a concrete, thread-safe pause stack manager implementation with scoped pause support and internal data structures.
  • Implement PauseStackManager with per-group stacks, token map, and handler list using ReaderWriterLockSlim for concurrency control.
  • Implement Push/Pop to manage PauseEntry stacks, handle non-top token removal, and trigger notifications only on state transitions.
  • Expose query APIs for paused state, depth, and reasons, and support clearing per group or all groups with appropriate notifications.
  • Manage lifecycle via DestroyAsync and ObjectDisposedException checks to prevent use-after-destroy, and integrate logging for operations and handler failures.
  • Add internal PauseEntry class to represent stack entries and PauseScope disposable to provide using-based scoped pauses.
GFramework.Core/pause/PauseStackManager.cs
GFramework.Core/pause/PauseEntry.cs
GFramework.Core/pause/PauseScope.cs
Integrate pause system with Godot by implementing a handler that maps global pause state to SceneTree.Paused.
  • Implement GodotPauseHandler that observes pause events and sets SceneTree.Paused based on the Global pause group.
  • Log SceneTree pause changes via GD.Print and respect handler Priority semantics.
GFramework.Godot/pause/GodotPauseHandler.cs
Normalize functional namespaces and test namespaces/usings to lower-case functional.* convention.
  • Change core functional namespaces from GFramework.Core.Functional* to GFramework.Core.functional* in Option, Result, async extensions, and function extensions.
  • Update test usings and namespaces to reference the new lower-case functional namespaces and match folder naming (extensions/functional).
  • Remove redundant using of the old Functional namespace where no longer needed.
GFramework.Core/functional/Option.cs
GFramework.Core/functional/Result.T.cs
GFramework.Core/functional/Result.cs
GFramework.Core/functional/async/AsyncFunctionalExtensions.cs
GFramework.Core/functional/result/ResultExtensions.cs
GFramework.Core/functional/functions/FunctionExtensions.cs
GFramework.Core.Tests/extensions/ResultExtensionsTests.cs
GFramework.Core.Tests/extensions/AsyncExtensionsTests.cs
GFramework.Core.Tests/functional/OptionTests.cs
GFramework.Core.Tests/functional/ResultTTests.cs
GFramework.Core.Tests/functional/ResultTests.cs
Add comprehensive unit tests for PauseStackManager covering correctness, events, handlers, scopes, and concurrency.
  • Test token validity, pop behavior for invalid/expired tokens, and non-top token removal while preserving remaining entries.
  • Verify IsPaused, GetPauseDepth, GetPauseReasons for single and multiple groups, including empty-stack behavior.
  • Ensure events and handlers fire only on state transitions, respect priority ordering, and behave correctly on pause and resume.
  • Validate PauseScope automatic pop on dispose and nested scopes adjusting depth correctly.
  • Exercise ClearGroup and ClearAll semantics and verify thread safety of concurrent push/pop operations using tasks and a mock handler implementation.
GFramework.Core.Tests/pause/PauseStackManagerTests.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Comment on lines +414 to +417
lock (lockObj)
{
tokens.Add(token);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock held in an improper manner


Trying to acquire a lock on a local variable is error-prone as different threads may end up locking on different instances of the same variable. Instead, you should be locking on a class field that is preferably designated as private and readonly, i.e. a dedicated object. Microsoft guidelines explicitly state that you should avoid locking on:

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 2 个问题,并给出了一些整体性的反馈:

  • PauseStackManager 中,NotifyHandlers(以及 OnPauseStateChanged)是在 PushPopClearGroupClearAll 中持有写锁的情况下被调用的。如果处理程序或订阅者回调到该 manager,会有死锁或可重入问题的风险;建议在锁内只捕获当前状态,然后释放锁,再在锁外调用处理程序和事件。
  • DestroyAsync 目前只释放了 ReaderWriterLockSlim,但没有清理 _pauseStacks_tokenMap_handlers;如果实例在销毁后还有被使用的可能,那么第一次执行加锁操作就会抛异常——建议要么在内部清理数据并显式阻止后续使用,要么在文档/代码层面明确并强制保证在调用 DestroyAsync 后不再调用任何方法。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- In `PauseStackManager`, `NotifyHandlers` (and thus `OnPauseStateChanged`) is invoked while holding the write lock in `Push`, `Pop`, `ClearGroup`, and `ClearAll`, which risks deadlocks or re-entrancy issues if handlers or subscribers call back into the manager; consider capturing the state under the lock, releasing it, then invoking handlers and events outside the lock.
- `DestroyAsync` currently only disposes the `ReaderWriterLockSlim` without clearing `_pauseStacks`, `_tokenMap`, or `_handlers`; if there is any chance the instance is used after destruction this will throw on the first lock operation—consider either clearing internal data and preventing further use explicitly or documenting/enforcing that no methods may be called after `DestroyAsync`.

## Individual Comments

### Comment 1
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="69-71" />
<code_context>
+            _logger.Debug($"Pause pushed: {reason} (Group: {group}, Depth: {stack.Count})");
+
+            // 状态变化检测:从未暂停 → 暂停
+            if (!wasPaused)
+            {
+                NotifyHandlers(group, true);
+            }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid invoking external handlers/events while holding the write lock to prevent deadlocks and contention.

These methods call `NotifyHandlers` while holding the `ReaderWriterLockSlim` write lock, but `NotifyHandlers` runs arbitrary user code (`IPauseHandler` and event subscribers). This risks deadlocks (e.g., if a handler calls back into `IPauseStackManager`) and extended lock contention. Instead, compute the state change and capture the handler list under the lock, then release the lock and invoke handlers/events afterwards to avoid re-entrancy inside the critical section.
</issue_to_address>

### Comment 2
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="25-27" />
<code_context>
+    /// 异步销毁方法,在组件销毁时调用。
+    /// </summary>
+    /// <returns>表示异步操作完成的任务。</returns>
+    public ValueTask DestroyAsync()
+    {
+        _lock.Dispose();
+        return ValueTask.CompletedTask;
+    }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clarify and enforce post-destruction usage semantics; disposing the lock without guarding other methods can cause runtime exceptions.

`DestroyAsync` only disposes `_lock`, while all other public methods keep using it without checks. Any call after destruction will throw `ObjectDisposedException` from `ReaderWriterLockSlim`, and concurrent use during `DestroyAsync` is data-racey. Either add a `_disposed` flag and have public APIs short-circuit (no-op or explicit error/logging), or clearly document and enforce that instances are never used after destruction. The current failure mode is implicit and hard to diagnose.

Suggested implementation:

```csharp
    private readonly List<IPauseHandler> _handlers = new();
    private readonly ReaderWriterLockSlim _lock = new();
    private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager));
    private readonly Dictionary<PauseGroup, Stack<PauseEntry>> _pauseStacks = new();
    private readonly Dictionary<Guid, PauseEntry> _tokenMap = new();

    /// <summary>
    /// 组件的销毁标记,一旦为 <c>true</c>,表示不再允许对暂停栈进行任何操作。
    /// </summary>
    private bool _disposed;

    /// <summary>
    /// 异步销毁方法,在组件销毁时调用。
    /// 调用后不允许再使用本实例的任何公开方法;后续调用将被忽略或记录日志,而不会抛出锁已释放异常。
    /// </summary>
    /// <returns>表示异步操作完成的任务。</returns>
    public ValueTask DestroyAsync()
    {
        // 使用写锁保证销毁过程与并发访问互斥
        _lock.EnterWriteLock();
        try
        {
            if (_disposed)
            {
                // 已经销毁则直接返回,避免重复 Dispose 造成异常
                return ValueTask.CompletedTask;
            }

            _disposed = true;

            // 此处仅修改内部状态,不再对外暴露任何可用操作
            _pauseStacks.Clear();
            _tokenMap.Clear();
            _handlers.Clear();
        }
        finally
        {
            _lock.ExitWriteLock();
        }

        // 在不持有锁的情况下释放底层锁资源,避免死锁风险
        _lock.Dispose();
        return ValueTask.CompletedTask;
    }

```

为彻底落实“销毁后不再使用实例”的语义,并避免在其他公开方法中因锁已释放导致的 `ObjectDisposedException`,需要在文件中做以下补充修改(这些方法未在当前片段中展示,需在完整文件中按现有风格修改):

1. 在类内部添加一个私有辅助方法,用于在所有公开 API 开头快速检查 `_disposed` 状态并做统一处理,例如:
   - 如果偏向静默失败:直接返回(或返回默认值)并记录一条 `Warning` 日志;
   - 如果偏向显式失败:抛出一个更易理解的异常(例如 `InvalidOperationException("PauseStackManager has been destroyed.")`),而不是由 `ReaderWriterLockSlim` 抛出 `ObjectDisposedException`。
   示例(需要放在类体中合适位置):
   ```csharp
   private void ThrowIfDisposed()
   {
       if (_disposed)
       {
           _logger.LogWarning("尝试在 PauseStackManager 已销毁后访问其方法,将忽略该操作。");
           throw new InvalidOperationException("PauseStackManager 已被销毁,不能再使用。");
       }
   }
   ```
   或者如果希望静默忽略,可以改成返回 `bool` 表示是否可继续执行:
   ```csharp
   private bool EnsureNotDisposed()
   {
       if (_disposed)
       {
           _logger.LogWarning("尝试在 PauseStackManager 已销毁后访问其方法,将忽略该操作。");
           return false;
       }
       return true;
   }
   ```

2. 在所有公开方法(例如推入/弹出暂停、注册/注销 `IPauseHandler`、查询当前暂停状态等)的开头,在尝试进入 `_lock` 之前调用上述辅助方法:
   - 若使用 `ThrowIfDisposed()`:直接在方法顶部调用 `ThrowIfDisposed();`- 若使用 `EnsureNotDisposed()`:在方法顶部写 `if (!EnsureNotDisposed()) return;``return default;`(视方法返回类型而定)。

3. 如果类实现了其它生命周期接口(例如 `IDisposable` 或框架特定接口),需要保证它们与 `DestroyAsync` 的语义一致,通常做法:
   - `Dispose()` 内部调用 `DestroyAsync().GetAwaiter().GetResult();` 或者提取共享的同步销毁逻辑;
   - 文档注释中统一说明:一旦调用销毁方法/Dispose,实例不可再使用。

4. 如果外部调用方目前在销毁后仍可能持有并使用 `PauseStackManager` 实例,建议在调用点增加空/状态检查或更新生命周期管理(例如通过 DI 容器注册为 Scoped/Singleton,并在作用域结束前确保不再被访问)。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这些评审有帮助,可以考虑分享一下 ✨
帮助我变得更有用!请对每条评论点 👍 或 👎,我会根据这些反馈来改进评审质量。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In PauseStackManager, NotifyHandlers (and thus OnPauseStateChanged) is invoked while holding the write lock in Push, Pop, ClearGroup, and ClearAll, which risks deadlocks or re-entrancy issues if handlers or subscribers call back into the manager; consider capturing the state under the lock, releasing it, then invoking handlers and events outside the lock.
  • DestroyAsync currently only disposes the ReaderWriterLockSlim without clearing _pauseStacks, _tokenMap, or _handlers; if there is any chance the instance is used after destruction this will throw on the first lock operation—consider either clearing internal data and preventing further use explicitly or documenting/enforcing that no methods may be called after DestroyAsync.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PauseStackManager`, `NotifyHandlers` (and thus `OnPauseStateChanged`) is invoked while holding the write lock in `Push`, `Pop`, `ClearGroup`, and `ClearAll`, which risks deadlocks or re-entrancy issues if handlers or subscribers call back into the manager; consider capturing the state under the lock, releasing it, then invoking handlers and events outside the lock.
- `DestroyAsync` currently only disposes the `ReaderWriterLockSlim` without clearing `_pauseStacks`, `_tokenMap`, or `_handlers`; if there is any chance the instance is used after destruction this will throw on the first lock operation—consider either clearing internal data and preventing further use explicitly or documenting/enforcing that no methods may be called after `DestroyAsync`.

## Individual Comments

### Comment 1
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="69-71" />
<code_context>
+            _logger.Debug($"Pause pushed: {reason} (Group: {group}, Depth: {stack.Count})");
+
+            // 状态变化检测:从未暂停 → 暂停
+            if (!wasPaused)
+            {
+                NotifyHandlers(group, true);
+            }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid invoking external handlers/events while holding the write lock to prevent deadlocks and contention.

These methods call `NotifyHandlers` while holding the `ReaderWriterLockSlim` write lock, but `NotifyHandlers` runs arbitrary user code (`IPauseHandler` and event subscribers). This risks deadlocks (e.g., if a handler calls back into `IPauseStackManager`) and extended lock contention. Instead, compute the state change and capture the handler list under the lock, then release the lock and invoke handlers/events afterwards to avoid re-entrancy inside the critical section.
</issue_to_address>

### Comment 2
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="25-27" />
<code_context>
+    /// 异步销毁方法,在组件销毁时调用。
+    /// </summary>
+    /// <returns>表示异步操作完成的任务。</returns>
+    public ValueTask DestroyAsync()
+    {
+        _lock.Dispose();
+        return ValueTask.CompletedTask;
+    }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clarify and enforce post-destruction usage semantics; disposing the lock without guarding other methods can cause runtime exceptions.

`DestroyAsync` only disposes `_lock`, while all other public methods keep using it without checks. Any call after destruction will throw `ObjectDisposedException` from `ReaderWriterLockSlim`, and concurrent use during `DestroyAsync` is data-racey. Either add a `_disposed` flag and have public APIs short-circuit (no-op or explicit error/logging), or clearly document and enforce that instances are never used after destruction. The current failure mode is implicit and hard to diagnose.

Suggested implementation:

```csharp
    private readonly List<IPauseHandler> _handlers = new();
    private readonly ReaderWriterLockSlim _lock = new();
    private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager));
    private readonly Dictionary<PauseGroup, Stack<PauseEntry>> _pauseStacks = new();
    private readonly Dictionary<Guid, PauseEntry> _tokenMap = new();

    /// <summary>
    /// 组件的销毁标记,一旦为 <c>true</c>,表示不再允许对暂停栈进行任何操作。
    /// </summary>
    private bool _disposed;

    /// <summary>
    /// 异步销毁方法,在组件销毁时调用。
    /// 调用后不允许再使用本实例的任何公开方法;后续调用将被忽略或记录日志,而不会抛出锁已释放异常。
    /// </summary>
    /// <returns>表示异步操作完成的任务。</returns>
    public ValueTask DestroyAsync()
    {
        // 使用写锁保证销毁过程与并发访问互斥
        _lock.EnterWriteLock();
        try
        {
            if (_disposed)
            {
                // 已经销毁则直接返回,避免重复 Dispose 造成异常
                return ValueTask.CompletedTask;
            }

            _disposed = true;

            // 此处仅修改内部状态,不再对外暴露任何可用操作
            _pauseStacks.Clear();
            _tokenMap.Clear();
            _handlers.Clear();
        }
        finally
        {
            _lock.ExitWriteLock();
        }

        // 在不持有锁的情况下释放底层锁资源,避免死锁风险
        _lock.Dispose();
        return ValueTask.CompletedTask;
    }

```

为彻底落实“销毁后不再使用实例”的语义,并避免在其他公开方法中因锁已释放导致的 `ObjectDisposedException`,需要在文件中做以下补充修改(这些方法未在当前片段中展示,需在完整文件中按现有风格修改):

1. 在类内部添加一个私有辅助方法,用于在所有公开 API 开头快速检查 `_disposed` 状态并做统一处理,例如:
   - 如果偏向静默失败:直接返回(或返回默认值)并记录一条 `Warning` 日志;
   - 如果偏向显式失败:抛出一个更易理解的异常(例如 `InvalidOperationException("PauseStackManager has been destroyed.")`),而不是由 `ReaderWriterLockSlim` 抛出 `ObjectDisposedException`。
   示例(需要放在类体中合适位置):
   ```csharp
   private void ThrowIfDisposed()
   {
       if (_disposed)
       {
           _logger.LogWarning("尝试在 PauseStackManager 已销毁后访问其方法,将忽略该操作。");
           throw new InvalidOperationException("PauseStackManager 已被销毁,不能再使用。");
       }
   }
   ```
   或者如果希望静默忽略,可以改成返回 `bool` 表示是否可继续执行:
   ```csharp
   private bool EnsureNotDisposed()
   {
       if (_disposed)
       {
           _logger.LogWarning("尝试在 PauseStackManager 已销毁后访问其方法,将忽略该操作。");
           return false;
       }
       return true;
   }
   ```

2. 在所有公开方法(例如推入/弹出暂停、注册/注销 `IPauseHandler`、查询当前暂停状态等)的开头,在尝试进入 `_lock` 之前调用上述辅助方法:
   - 若使用 `ThrowIfDisposed()`:直接在方法顶部调用 `ThrowIfDisposed();`- 若使用 `EnsureNotDisposed()`:在方法顶部写 `if (!EnsureNotDisposed()) return;``return default;`(视方法返回类型而定)。

3. 如果类实现了其它生命周期接口(例如 `IDisposable` 或框架特定接口),需要保证它们与 `DestroyAsync` 的语义一致,通常做法:
   - `Dispose()` 内部调用 `DestroyAsync().GetAwaiter().GetResult();` 或者提取共享的同步销毁逻辑;
   - 文档注释中统一说明:一旦调用销毁方法/Dispose,实例不可再使用。

4. 如果外部调用方目前在销毁后仍可能持有并使用 `PauseStackManager` 实例,建议在调用点增加空/状态检查或更新生命周期管理(例如通过 DI 容器注册为 Scoped/Singleton,并在作用域结束前确保不再被访问)。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread GFramework.Core/pause/PauseStackManager.cs Outdated
Comment thread GFramework.Core/pause/PauseStackManager.cs
GeWuYou added 2 commits March 2, 2026 21:21
- 将 GFramework.Core.Functional 命名空间统一改为 GFramework.Core.functional
- 更新 AsyncFunctionalExtensions.cs 中的命名空间声明
- 移除不必要的 using 指令,包括 GFramework.Core.Functional 的导入
- 调整测试文件中的命名空间引用以匹配新的命名空间格式
- 确保所有相关文件都使用一致的小写命名空间约定
- 实现了 PauseStackManager 核心管理器,支持嵌套暂停和分组管理
- 添加了 PauseToken 暂停令牌和 PauseGroup 暂停组枚举
- 创建了 PauseScope 作用域类,支持 using 语法自动管理暂停生命周期
- 实现了线程安全的暂停栈操作,包括 Push、Pop 和状态查询
- 添加了暂停处理器接口 IPauseHandler 和 Godot 平台具体实现
- 提供了完整的单元测试覆盖基础功能、嵌套暂停、分组管理和线程安全场景
@GeWuYou GeWuYou force-pushed the feature/pause-stack-manager branch from a574e46 to 903241f Compare March 2, 2026 13:27
@GeWuYou

GeWuYou commented Mar 2, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

- 添加了_disposed标志位用于跟踪对象销毁状态
- 实现了完整的DestroyAsync方法进行资源清理和数据结构清空
- 添加了ThrowIfDisposed方法防止对象销毁后被使用
- 将状态变更通知移到锁外部以避免死锁问题
- 在多个公共方法中添加了销毁状态检查
- 修复了ClearGroup和ClearAll方法中的死锁风险
- 改进了异常处理机制,确保线程安全

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了两个问题,并留下了一些总体性的反馈:

  • PauseStackManager 中,NotifyHandlers 是在持有写锁的情况下被调用的(例如从 PushClearGroupClearAll 中调用),如果处理器或事件订阅者回调到该管理器中,很容易导致死锁;建议在锁内只获取处理器集合和 OnPauseStateChanged 委托的快照,然后释放锁,再在锁外执行回调。
  • DestroyAsync 会释放内部的 ReaderWriterLockSlim,但并不会阻止之后对 Push/Pop 等方法的调用,这些调用会抛出 ObjectDisposedException;如果这是一个生命周期结束的方法,可能更安全的做法是将管理器标记为已释放并在各公开方法中进行短路(或者在文档中明确说明在销毁后不应再使用)。
  • 处理器的调用顺序在每次状态变化时都会通过 OrderBy(h => h.Priority) 重新计算;如果暂停事件比较频繁而处理器列表又相对稳定,可以考虑在注册时就按优先级维护 _handlers 列表的有序性,从而避免在每次通知时重复排序。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PauseStackManager`, `NotifyHandlers` is invoked while holding the write lock (e.g., from `Push`, `ClearGroup`, `ClearAll`), which can easily lead to deadlocks if handlers or event subscribers call back into the manager; consider capturing a snapshot of handlers and the `OnPauseStateChanged` delegate under lock, releasing the lock, and only then invoking them.
- `DestroyAsync` disposes the internal `ReaderWriterLockSlim` but does not prevent subsequent calls to methods like `Push`/`Pop`, which will then throw `ObjectDisposedException`; if this is a lifecycle method, it may be safer to mark the manager as disposed and short‑circuit public methods (or document that it must not be used after destruction).
- Handler invocation order is recomputed via `OrderBy(h => h.Priority)` on every state change; if pause events are frequent and handlers are relatively stable, you could maintain the `_handlers` list in priority order on registration to avoid repeated sorting on each notification.

## Individual Comments

### Comment 1
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="68-71" />
<code_context>
+
+            _logger.Debug($"Pause pushed: {reason} (Group: {group}, Depth: {stack.Count})");
+
+            // 状态变化检测:从未暂停 → 暂停
+            if (!wasPaused)
+            {
+                NotifyHandlers(group, true);
+            }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid calling NotifyHandlers while holding the write-lock to prevent deadlocks and handler-induced contention.

Push, Pop, ClearGroup and ClearAll currently invoke NotifyHandlers while holding the ReaderWriterLockSlim write-lock. Because NotifyHandlers calls arbitrary user code (IPauseHandler implementations and event subscribers), this can cause deadlocks or long lock hold times if that code re-enters PauseStackManager or blocks.

Instead, under the lock, compute whether a notification is needed and capture a snapshot of the handlers/event delegate; then release the lock and invoke them using the snapshot. This keeps internal state thread-safe while avoiding lock re-entrancy and reducing the impact of external code on the lock duration.
</issue_to_address>

### Comment 2
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="25-27" />
<code_context>
+    /// 异步销毁方法,在组件销毁时调用。
+    /// </summary>
+    /// <returns>表示异步操作完成的任务。</returns>
+    public ValueTask DestroyAsync()
+    {
+        _lock.Dispose();
+        return ValueTask.CompletedTask;
+    }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using the manager after DestroyAsync will throw due to the disposed ReaderWriterLockSlim; consider adding a disposal guard.

Since DestroyAsync disposes the ReaderWriterLockSlim but other methods don’t check for disposal, any later call into Push/Pop/IsPaused/etc. will try to take a disposed lock and can throw ObjectDisposedException. If the lifecycle truly guarantees no calls after DestroyAsync, this might be acceptable but is fragile.

To harden this, consider introducing a `_disposed` flag set in DestroyAsync and checking it at the start of each public method, either throwing a clear ObjectDisposedException or no-op’ing where appropriate. Alternatively, if DestroyAsync is only a logical teardown, you could avoid disposing the lock explicitly and rely on GC instead.

Suggested implementation:

```csharp
    private readonly List<IPauseHandler> _handlers = new();
    private readonly ReaderWriterLockSlim _lock = new();
    private bool _disposed;
    private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager));

```

```csharp
    /// <summary>
    /// 异步销毁方法,在组件销毁时调用。
    /// </summary>
    /// <returns>表示异步操作完成的任务。</returns>
    public ValueTask DestroyAsync()
    {
        if (_disposed)
        {
            return ValueTask.CompletedTask;
        }

        _disposed = true;
        _lock.Dispose();

        return ValueTask.CompletedTask;
    }

```

To fully implement the disposal guard and avoid using a disposed `ReaderWriterLockSlim`, you should:

1. At the very beginning of each public method that uses `_lock` or other internal state (e.g. `Push(...)`, `Pop(...)`, `IsPaused(...)`, any `Try...` methods, etc.), add a guard like:
   ```csharp
   if (_disposed)
   {
       throw new ObjectDisposedException(nameof(PauseStackManager));
   }
   ```
   or, if more appropriate for some methods (e.g. query methods), you can return a safe default instead of throwing.

2. If there are any async methods or callbacks that might be scheduled to run after `DestroyAsync`, ensure they either:
   - Check `_disposed` before taking `_lock`, or
   - Are cancelled/unsubscribed as part of `DestroyAsync`.

3. If there is an interface (e.g. `IPauseStackManager`) that mandates `DestroyAsync`, consider documenting in XML comments that calling any other member after `DestroyAsync` results in `ObjectDisposedException`, to make the lifecycle contract explicit.
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得这次 review 有帮助,欢迎分享 ✨
请帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续的 review。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In PauseStackManager, NotifyHandlers is invoked while holding the write lock (e.g., from Push, ClearGroup, ClearAll), which can easily lead to deadlocks if handlers or event subscribers call back into the manager; consider capturing a snapshot of handlers and the OnPauseStateChanged delegate under lock, releasing the lock, and only then invoking them.
  • DestroyAsync disposes the internal ReaderWriterLockSlim but does not prevent subsequent calls to methods like Push/Pop, which will then throw ObjectDisposedException; if this is a lifecycle method, it may be safer to mark the manager as disposed and short‑circuit public methods (or document that it must not be used after destruction).
  • Handler invocation order is recomputed via OrderBy(h => h.Priority) on every state change; if pause events are frequent and handlers are relatively stable, you could maintain the _handlers list in priority order on registration to avoid repeated sorting on each notification.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PauseStackManager`, `NotifyHandlers` is invoked while holding the write lock (e.g., from `Push`, `ClearGroup`, `ClearAll`), which can easily lead to deadlocks if handlers or event subscribers call back into the manager; consider capturing a snapshot of handlers and the `OnPauseStateChanged` delegate under lock, releasing the lock, and only then invoking them.
- `DestroyAsync` disposes the internal `ReaderWriterLockSlim` but does not prevent subsequent calls to methods like `Push`/`Pop`, which will then throw `ObjectDisposedException`; if this is a lifecycle method, it may be safer to mark the manager as disposed and short‑circuit public methods (or document that it must not be used after destruction).
- Handler invocation order is recomputed via `OrderBy(h => h.Priority)` on every state change; if pause events are frequent and handlers are relatively stable, you could maintain the `_handlers` list in priority order on registration to avoid repeated sorting on each notification.

## Individual Comments

### Comment 1
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="68-71" />
<code_context>
+
+            _logger.Debug($"Pause pushed: {reason} (Group: {group}, Depth: {stack.Count})");
+
+            // 状态变化检测:从未暂停 → 暂停
+            if (!wasPaused)
+            {
+                NotifyHandlers(group, true);
+            }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid calling NotifyHandlers while holding the write-lock to prevent deadlocks and handler-induced contention.

Push, Pop, ClearGroup and ClearAll currently invoke NotifyHandlers while holding the ReaderWriterLockSlim write-lock. Because NotifyHandlers calls arbitrary user code (IPauseHandler implementations and event subscribers), this can cause deadlocks or long lock hold times if that code re-enters PauseStackManager or blocks.

Instead, under the lock, compute whether a notification is needed and capture a snapshot of the handlers/event delegate; then release the lock and invoke them using the snapshot. This keeps internal state thread-safe while avoiding lock re-entrancy and reducing the impact of external code on the lock duration.
</issue_to_address>

### Comment 2
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="25-27" />
<code_context>
+    /// 异步销毁方法,在组件销毁时调用。
+    /// </summary>
+    /// <returns>表示异步操作完成的任务。</returns>
+    public ValueTask DestroyAsync()
+    {
+        _lock.Dispose();
+        return ValueTask.CompletedTask;
+    }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using the manager after DestroyAsync will throw due to the disposed ReaderWriterLockSlim; consider adding a disposal guard.

Since DestroyAsync disposes the ReaderWriterLockSlim but other methods don’t check for disposal, any later call into Push/Pop/IsPaused/etc. will try to take a disposed lock and can throw ObjectDisposedException. If the lifecycle truly guarantees no calls after DestroyAsync, this might be acceptable but is fragile.

To harden this, consider introducing a `_disposed` flag set in DestroyAsync and checking it at the start of each public method, either throwing a clear ObjectDisposedException or no-op’ing where appropriate. Alternatively, if DestroyAsync is only a logical teardown, you could avoid disposing the lock explicitly and rely on GC instead.

Suggested implementation:

```csharp
    private readonly List<IPauseHandler> _handlers = new();
    private readonly ReaderWriterLockSlim _lock = new();
    private bool _disposed;
    private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager));

```

```csharp
    /// <summary>
    /// 异步销毁方法,在组件销毁时调用。
    /// </summary>
    /// <returns>表示异步操作完成的任务。</returns>
    public ValueTask DestroyAsync()
    {
        if (_disposed)
        {
            return ValueTask.CompletedTask;
        }

        _disposed = true;
        _lock.Dispose();

        return ValueTask.CompletedTask;
    }

```

To fully implement the disposal guard and avoid using a disposed `ReaderWriterLockSlim`, you should:

1. At the very beginning of each public method that uses `_lock` or other internal state (e.g. `Push(...)`, `Pop(...)`, `IsPaused(...)`, any `Try...` methods, etc.), add a guard like:
   ```csharp
   if (_disposed)
   {
       throw new ObjectDisposedException(nameof(PauseStackManager));
   }
   ```
   or, if more appropriate for some methods (e.g. query methods), you can return a safe default instead of throwing.

2. If there are any async methods or callbacks that might be scheduled to run after `DestroyAsync`, ensure they either:
   - Check `_disposed` before taking `_lock`, or
   - Are cancelled/unsubscribed as part of `DestroyAsync`.

3. If there is an interface (e.g. `IPauseStackManager`) that mandates `DestroyAsync`, consider documenting in XML comments that calling any other member after `DestroyAsync` results in `ObjectDisposedException`, to make the lifecycle contract explicit.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread GFramework.Core/pause/PauseStackManager.cs Outdated
Comment thread GFramework.Core/pause/PauseStackManager.cs
/// <returns>表示此次暂停请求的令牌。</returns>
public PauseToken Push(string reason, PauseGroup group = PauseGroup.Global)
{
PauseToken token;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable `token` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

if (!token.IsValid)
return false;

bool found;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable `found` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

/// </summary>
public void ClearAll()
{
List<PauseGroup> pausedGroups;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable `pausedGroups` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

@GeWuYou

GeWuYou commented Mar 2, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并给出了一些总体反馈:

  • NotifyHandlers 在没有任何同步措施的情况下遍历 _handlers,而 RegisterHandler/UnregisterHandler 则在写锁下修改该列表;建议在锁内对 _handlers 进行快照(例如复制到数组)并遍历该快照,以避免并发修改时可能出现的竞态条件或 InvalidOperationException。
  • DestroyAsync 会清除所有暂停状态,但不会发出任何 OnPauseStateChanged 通知,这意味着处理器和订阅方无法感知到状态从“暂停”到“未暂停”的转换;如果你希望整个生命周期的信号保持一致,建议在内部调用 ClearAll(),或者在销毁前显式通知受影响的分组。
面向 AI Agents 的提示
请根据这次代码评审中的评论进行修改:

## 总体评论
- NotifyHandlers 在没有任何同步措施的情况下遍历 _handlers,而 RegisterHandler/UnregisterHandler 则在写锁下修改该列表;建议在锁内对 _handlers 进行快照(例如复制到数组)并遍历该快照,以避免并发修改时可能出现的竞态条件或 InvalidOperationException。
- DestroyAsync 会清除所有暂停状态,但不会发出任何 OnPauseStateChanged 通知,这意味着处理器和订阅方无法感知到状态从“暂停”到“未暂停”的转换;如果你希望整个生命周期的信号保持一致,建议在内部调用 ClearAll(),或者在销毁前显式通知受影响的分组。

## 单条评论

### 评论 1
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="421-426" />
<code_context>
+    /// </summary>
+    /// <param name="group">发生状态变化的暂停组。</param>
+    /// <param name="isPaused">新的暂停状态。</param>
+    private void NotifyHandlers(PauseGroup group, bool isPaused)
+    {
+        _logger.Debug($"Notifying handlers: Group={group}, IsPaused={isPaused}");
+
+        // 按优先级排序后通知
+        foreach (var handler in _handlers.OrderBy(h => h.Priority))
+        {
+            try
</code_context>
<issue_to_address>
**issue (bug_risk):** 在没有同步的情况下枚举 _handlers,会在并发注册/注销时带来 InvalidOperationException 的风险。

`NotifyHandlers` 在没有加锁的情况下枚举 `_handlers`,而 `RegisterHandler`/`UnregisterHandler` 会在写锁之下修改 `_handlers`。在枚举期间发生并发修改可能抛出 `InvalidOperationException`。为了在调用处理器时保持线程安全且不长时间持有锁,建议在锁内先获取一个快照(例如 `var handlers = _handlers.OrderBy(h => h.Priority).ToArray();`),然后遍历该快照。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • NotifyHandlers iterates over _handlers without any synchronization while RegisterHandler/UnregisterHandler mutate the list under a write lock; consider taking a snapshot of _handlers under the lock (e.g., copy to an array) and iterating that snapshot to avoid potential race conditions or InvalidOperationException during concurrent modification.
  • DestroyAsync clears all pause state without emitting any OnPauseStateChanged notifications, which means handlers and subscribers won’t see the transition to an unpaused state; consider either calling ClearAll() internally or explicitly notifying affected groups before disposal if you want a consistent lifecycle signal.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- NotifyHandlers iterates over _handlers without any synchronization while RegisterHandler/UnregisterHandler mutate the list under a write lock; consider taking a snapshot of _handlers under the lock (e.g., copy to an array) and iterating that snapshot to avoid potential race conditions or InvalidOperationException during concurrent modification.
- DestroyAsync clears all pause state without emitting any OnPauseStateChanged notifications, which means handlers and subscribers won’t see the transition to an unpaused state; consider either calling ClearAll() internally or explicitly notifying affected groups before disposal if you want a consistent lifecycle signal.

## Individual Comments

### Comment 1
<location path="GFramework.Core/pause/PauseStackManager.cs" line_range="421-426" />
<code_context>
+    /// </summary>
+    /// <param name="group">发生状态变化的暂停组。</param>
+    /// <param name="isPaused">新的暂停状态。</param>
+    private void NotifyHandlers(PauseGroup group, bool isPaused)
+    {
+        _logger.Debug($"Notifying handlers: Group={group}, IsPaused={isPaused}");
+
+        // 按优先级排序后通知
+        foreach (var handler in _handlers.OrderBy(h => h.Priority))
+        {
+            try
</code_context>
<issue_to_address>
**issue (bug_risk):** Enumerating _handlers without synchronization risks InvalidOperationException during concurrent register/unregister.

`NotifyHandlers` enumerates `_handlers` without a lock, while `RegisterHandler`/`UnregisterHandler` mutate `_handlers` under a write lock. Concurrent modification during enumeration can throw `InvalidOperationException`. To stay thread-safe without holding the lock while invoking handlers, take a snapshot under the lock (e.g. `var handlers = _handlers.OrderBy(h => h.Priority).ToArray();`) and iterate over that snapshot instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread GFramework.Core/pause/PauseStackManager.cs Outdated
- 在销毁过程中收集暂停组和处理器快照,避免并发修改异常
- 在锁外通知所有之前暂停的组恢复,保持生命周期信号一致性
- 添加异常处理确保单个处理器失败不影响其他处理器的通知
- 修改文件过滤规则以正确包含测试文件路径
if (_disposed)
return ValueTask.CompletedTask;

List<PauseGroup> pausedGroups;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable `pausedGroups` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

return ValueTask.CompletedTask;

List<PauseGroup> pausedGroups;
IPauseHandler[] handlersSnapshot;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable `handlersSnapshot` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

_logger.Debug($"Notifying handlers: Group={group}, IsPaused={isPaused}");

// 在锁内获取处理器快照,避免并发修改异常
IPauseHandler[] handlersSnapshot;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable `handlersSnapshot` is uninitialized


While some variables such as fields can be initialized and be assigned a default value, it is possible for local variables to not be initialized. This however is not a good practice as it is capable of critically altering your program's path, thereby affecting its logic.

It is therefore recommended that you always initialize variables, ideally where they're being declared.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant