Skip to content

refactor(storage): 重构文件存储实现以支持异步安全锁和原子写入#98

Merged
GeWuYou merged 3 commits into
mainfrom
refactor/storage-async-safety
Mar 11, 2026
Merged

refactor(storage): 重构文件存储实现以支持异步安全锁和原子写入#98
GeWuYou merged 3 commits into
mainfrom
refactor/storage-async-safety

Conversation

@GeWuYou
Copy link
Copy Markdown
Owner

@GeWuYou GeWuYou commented Mar 11, 2026

  • 替换同步锁机制为异步键锁管理器,提升并发性能
  • 实现原子写入功能,通过临时文件防止写入过程中的数据损坏
  • 添加资源释放接口(IDisposable)和对象销毁检查
  • 集成异步IO操作,包括缓冲区大小配置选项
  • 更新Godot文件存储适配器以匹配新的异步安全机制
  • 优化文件读取操作,支持异步文本读取避免阻塞
  • 移除旧的并发字典锁实现,统一使用新的锁管理器

Summary by Sourcery

重构基于文件的存储实现,以使用异步键锁管理器、异步 I/O 和销毁保护机制,并为游戏文件存储添加原子写入支持。

新功能:

  • 在游戏文件存储中,通过使用临时文件和原子替换操作,支持原子文件写入。
  • 允许为游戏文件存储中的异步文件读写配置 I/O 缓冲区大小。
  • 为 Godot 和游戏文件存储实现开放可选的依赖注入式异步键锁管理器。

增强:

  • 在两个存储实现中,将按键锁从内存锁对象迁移到异步安全的键锁管理器上。
  • 将同步存储 API 转换为对异步方法的轻量封装,确保一致的“异步优先”行为。
  • 通过使用异步文本读取和共享锁优化文件读取操作,适用于 Godot 和游戏存储后端。
  • 添加 IDisposable 支持及销毁检查,以便干净地释放锁管理器资源并防止销毁后的继续使用。
Original summary in English

Summary by Sourcery

Refactor file-based storage implementations to use an async key-lock manager, async I/O, and disposal safeguards while adding atomic write support for game file storage.

New Features:

  • Support atomic file writes in the game file storage using temporary files and atomic replace operations.
  • Allow configuration of I/O buffer size for async file reads and writes in the game file storage.
  • Expose optional dependency-injected async key lock managers for both Godot and game file storage implementations.

Enhancements:

  • Migrate per-key locking from in-memory lock objects to an async-safe key lock manager in both storage implementations.
  • Convert synchronous storage APIs to thin wrappers over async methods, ensuring consistent async-first behavior.
  • Optimize file read operations by using async text reads and shared locking for both Godot and game storage backends.
  • Add IDisposable support with disposal checks to cleanly release lock manager resources and guard against use-after-dispose.

- 替换同步锁机制为异步键锁管理器,提升并发性能
- 实现原子写入功能,通过临时文件防止写入过程中的数据损坏
- 添加资源释放接口(IDisposable)和对象销毁检查
- 集成异步IO操作,包括缓冲区大小配置选项
- 更新Godot文件存储适配器以匹配新的异步安全机制
- 优化文件读取操作,支持异步文本读取避免阻塞
- 移除旧的并发字典锁实现,统一使用新的锁管理器
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 11, 2026

审阅者指南

重构 Godot 与游戏文件存储实现,使其使用共享的异步基于键的锁管理器,添加释放与对象已释放检查,引入真正的可配置缓冲的异步 I/O,并通过临时文件为游戏文件存储实现原子写入。

使用异步锁实现 FileStorage.WriteAsync 原子写入的时序图

sequenceDiagram
    actor GameSystem
    participant FileStorage
    participant IAsyncKeyLockManager
    participant AsyncKeyLockScope as IAsyncDisposable
    participant FileSystem

    GameSystem->>FileStorage: WriteAsync(key, value)
    FileStorage->>FileStorage: ObjectDisposedException.ThrowIf(_disposed, this)
    FileStorage->>FileStorage: path = ToPath(key)
    FileStorage->>FileStorage: tempPath = path + .tmp

    FileStorage->>IAsyncKeyLockManager: AcquireLockAsync(path)
    IAsyncKeyLockManager-->>FileStorage: AsyncKeyLockScope
    activate FileStorage
    FileStorage->>AsyncKeyLockScope: await using scope

    FileStorage->>FileStorage: content = _serializer.Serialize(value)

    FileStorage->>FileSystem: open FileStream(tempPath, Create, Write, None, _bufferSize, useAsync)
    FileStorage->>FileSystem: create StreamWriter and WriteAsync(content)
    FileStorage->>FileSystem: FlushAsync and close

    FileStorage->>FileSystem: File.Move(tempPath, path, overwrite true)

    AsyncKeyLockScope-->>IAsyncKeyLockManager: DisposeAsync
    deactivate FileStorage
    FileStorage-->>GameSystem: Task completed

    note over FileStorage,FileSystem: If any exception occurs, tempPath is deleted before rethrowing
Loading

使用异步键锁的 GodotFileStorage.ExistsAsync 时序图

sequenceDiagram
    actor GameCode
    participant GodotFileStorage
    participant IAsyncKeyLockManager
    participant AsyncKeyLockScope as IAsyncDisposable
    participant FileSystem
    participant GodotFS as Godot.FileAccess

    GameCode->>GodotFileStorage: ExistsAsync(key)
    GodotFileStorage->>GodotFileStorage: ObjectDisposedException.ThrowIf(_disposed, this)
    GodotFileStorage->>GodotFileStorage: path = ToAbsolutePath(key)

    GodotFileStorage->>IAsyncKeyLockManager: AcquireLockAsync(path)
    IAsyncKeyLockManager-->>GodotFileStorage: AsyncKeyLockScope
    GodotFileStorage->>AsyncKeyLockScope: await using scope

    alt path is normal file path
        GodotFileStorage->>FileSystem: File.Exists(path)
        FileSystem-->>GodotFileStorage: exists bool
    else path is Godot path
        GodotFileStorage->>GodotFS: FileAccess.Open(path, Read)
        GodotFS-->>GodotFileStorage: file handle or null
        GodotFileStorage->>GodotFileStorage: exists = file != null
    end

    AsyncKeyLockScope-->>IAsyncKeyLockManager: DisposeAsync
    GodotFileStorage-->>GameCode: exists bool
Loading

重构后的异步文件存储与锁的类图

classDiagram
    class IStorage
    class IFileStorage
    class ISerializer
    class IAsyncKeyLockManager {
        <<interface>>
        AcquireLockAsync(key string) Task~IAsyncDisposable~
        Dispose() void
    }

    class AsyncKeyLockManager {
        AsyncKeyLockManager()
        AcquireLockAsync(key string) Task~IAsyncDisposable~
        Dispose() void
    }

    class GodotFileStorage {
        <<sealed>>
        - _lockManager IAsyncKeyLockManager
        - _serializer ISerializer
        - _disposed bool
        + GodotFileStorage(serializer ISerializer, lockManager IAsyncKeyLockManager)
        + Dispose() void
        + Delete(key string) void
        + DeleteAsync(key string) Task
        + Exists(key string) bool
        + ExistsAsync(key string) Task~bool~
        + Read~T~(key string) T
        + Read~T~(key string, defaultValue T) T
        + ReadAsync~T~(key string) Task~T~
        + Write~T~(key string, value T) void
        + WriteAsync~T~(key string, value T) Task
        - ToAbsolutePath(key string) string
    }

    class FileStorage {
        <<sealed>>
        - _bufferSize int
        - _extension string
        - _lockManager IAsyncKeyLockManager
        - _rootPath string
        - _serializer ISerializer
        - _disposed bool
        + FileStorage(rootPath string, serializer ISerializer, extension string, bufferSize int, lockManager IAsyncKeyLockManager)
        + Dispose() void
        + Delete(key string) void
        + DeleteAsync(key string) Task
        + Exists(key string) bool
        + ExistsAsync(key string) Task~bool~
        + Read~T~(key string) T
        + Read~T~(key string, defaultValue T) T
        + ReadAsync~T~(key string) Task~T~
        + Write~T~(key string, value T) void
        + WriteAsync~T~(key string, value T) Task
        - ToPath(key string) string
        - CleanSegment(segment string) string
    }

    IStorage <|.. GodotFileStorage
    IDisposable <|.. GodotFileStorage
    IFileStorage <|.. FileStorage
    IDisposable <|.. FileStorage

    AsyncKeyLockManager ..|> IAsyncKeyLockManager

    GodotFileStorage --> ISerializer : uses
    GodotFileStorage --> IAsyncKeyLockManager : uses

    FileStorage --> ISerializer : uses
    FileStorage --> IAsyncKeyLockManager : uses
Loading

跨存储实现共享异步锁管理器的架构流程图

flowchart LR
    subgraph Core_Assembly[GFramework.Core]
        AsyncKeyLockManager
        IAsyncKeyLockManager
    end

    subgraph Godot_Assembly[GFramework.Godot]
        GodotFileStorage
    end

    subgraph Game_Assembly[GFramework.Game]
        FileStorage
    end

    subgraph Core_Serializer[GFramework.Core.Serializer]
        ISerializer
    end

    GodotFileStorage --> IAsyncKeyLockManager
    FileStorage --> IAsyncKeyLockManager
    AsyncKeyLockManager -.-> IAsyncKeyLockManager

    GodotFileStorage --> ISerializer
    FileStorage --> ISerializer

    GameCode[[Game code]] --> FileStorage
    GodotScene[[Godot scene scripts]] --> GodotFileStorage

    FileStorage -.uses filesystem APIs.-> FileSystem[(File system)]
    GodotFileStorage -.uses Godot FileAccess.-> GodotFileAccess[(Godot FileAccess)]
Loading

文件级变更

变更 详情 文件
用异步键锁管理器替换按键的 ConcurrentDictionary 锁,并让同步 API 与异步实现对齐。
  • 移除两个存储实现中按路径的 ConcurrentDictionary 锁以及 GetLock 辅助方法。
  • 引入 IAsyncKeyLockManager 依赖(默认实现为 AsyncKeyLockManager),并使用 AcquireLockAsync(path) 配合 await using 来保护所有文件操作。
  • 将同步方法(Delete、Exists、Read、Write)重构为对其异步对应方法的薄包装,使用 GetAwaiter().GetResult()。
GFramework.Godot/Storage/GodotFileStorage.cs
GFramework.Game/Storage/FileStorage.cs
添加 IDisposable 支持和已释放状态检查,以防止在存储实例被释放后继续使用。
  • 使 GodotFileStorage 和 FileStorage 都实现 IDisposable,并跟踪私有的 _disposed 标志位。
  • 在 Dispose() 中释放注入的或内部创建的 IAsyncKeyLockManager。
  • 在所有访问文件系统或序列化器的异步操作开头添加 ObjectDisposedException.ThrowIf(_disposed, this)。
GFramework.Godot/Storage/GodotFileStorage.cs
GFramework.Game/Storage/FileStorage.cs
将文件 I/O 转换为真正的异步操作,并在游戏存储实现中支持可配置缓冲区大小。
  • 在 GodotFileStorage 中,对非 Godot 路径用 File.ReadAllTextAsync/File.WriteAllTextAsync 替换同步的 File.ReadAllText/File.WriteAllText。
  • 在 FileStorage 中,为异步读取使用 useAsync: true 且缓冲大小为可配置 _bufferSize 的 FileStream,并使用 StreamReader.ReadToEndAsync 加载文本。
  • 为 FileStorage 添加带默认值(8192)的 bufferSize 构造函数参数,并将其存储为字段。
GFramework.Godot/Storage/GodotFileStorage.cs
GFramework.Game/Storage/FileStorage.cs
通过临时文件和最终移动/替换,为游戏文件存储实现原子写入。
  • 修改 FileStorage.WriteAsync,先使用异步 FileStream 和 StreamWriter 将序列化结果写入 tempPath(path + ".tmp"),然后以 overwrite:true 将其移动到目标路径。
  • 将写入/移动过程包裹在 try/catch 中,在失败时删除残留的临时文件后再重新抛出异常。
  • 更新 XML 文档注释,描述原子写语义和异步安全性。
GFramework.Game/Storage/FileStorage.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 摘要。你也可以在 pull request 中评论 @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

Refactors the Godot and game file storage implementations to use a shared async key-based lock manager, add disposal and object-disposed checks, introduce truly async I/O with configurable buffering, and implement atomic writes for game file storage via temporary files.

Sequence diagram for FileStorage.WriteAsync atomic write with async lock

sequenceDiagram
    actor GameSystem
    participant FileStorage
    participant IAsyncKeyLockManager
    participant AsyncKeyLockScope as IAsyncDisposable
    participant FileSystem

    GameSystem->>FileStorage: WriteAsync(key, value)
    FileStorage->>FileStorage: ObjectDisposedException.ThrowIf(_disposed, this)
    FileStorage->>FileStorage: path = ToPath(key)
    FileStorage->>FileStorage: tempPath = path + .tmp

    FileStorage->>IAsyncKeyLockManager: AcquireLockAsync(path)
    IAsyncKeyLockManager-->>FileStorage: AsyncKeyLockScope
    activate FileStorage
    FileStorage->>AsyncKeyLockScope: await using scope

    FileStorage->>FileStorage: content = _serializer.Serialize(value)

    FileStorage->>FileSystem: open FileStream(tempPath, Create, Write, None, _bufferSize, useAsync)
    FileStorage->>FileSystem: create StreamWriter and WriteAsync(content)
    FileStorage->>FileSystem: FlushAsync and close

    FileStorage->>FileSystem: File.Move(tempPath, path, overwrite true)

    AsyncKeyLockScope-->>IAsyncKeyLockManager: DisposeAsync
    deactivate FileStorage
    FileStorage-->>GameSystem: Task completed

    note over FileStorage,FileSystem: If any exception occurs, tempPath is deleted before rethrowing
Loading

Sequence diagram for GodotFileStorage.ExistsAsync with async key lock

sequenceDiagram
    actor GameCode
    participant GodotFileStorage
    participant IAsyncKeyLockManager
    participant AsyncKeyLockScope as IAsyncDisposable
    participant FileSystem
    participant GodotFS as Godot.FileAccess

    GameCode->>GodotFileStorage: ExistsAsync(key)
    GodotFileStorage->>GodotFileStorage: ObjectDisposedException.ThrowIf(_disposed, this)
    GodotFileStorage->>GodotFileStorage: path = ToAbsolutePath(key)

    GodotFileStorage->>IAsyncKeyLockManager: AcquireLockAsync(path)
    IAsyncKeyLockManager-->>GodotFileStorage: AsyncKeyLockScope
    GodotFileStorage->>AsyncKeyLockScope: await using scope

    alt path is normal file path
        GodotFileStorage->>FileSystem: File.Exists(path)
        FileSystem-->>GodotFileStorage: exists bool
    else path is Godot path
        GodotFileStorage->>GodotFS: FileAccess.Open(path, Read)
        GodotFS-->>GodotFileStorage: file handle or null
        GodotFileStorage->>GodotFileStorage: exists = file != null
    end

    AsyncKeyLockScope-->>IAsyncKeyLockManager: DisposeAsync
    GodotFileStorage-->>GameCode: exists bool
Loading

Class diagram for refactored async file storage and locking

classDiagram
    class IStorage
    class IFileStorage
    class ISerializer
    class IAsyncKeyLockManager {
        <<interface>>
        AcquireLockAsync(key string) Task~IAsyncDisposable~
        Dispose() void
    }

    class AsyncKeyLockManager {
        AsyncKeyLockManager()
        AcquireLockAsync(key string) Task~IAsyncDisposable~
        Dispose() void
    }

    class GodotFileStorage {
        <<sealed>>
        - _lockManager IAsyncKeyLockManager
        - _serializer ISerializer
        - _disposed bool
        + GodotFileStorage(serializer ISerializer, lockManager IAsyncKeyLockManager)
        + Dispose() void
        + Delete(key string) void
        + DeleteAsync(key string) Task
        + Exists(key string) bool
        + ExistsAsync(key string) Task~bool~
        + Read~T~(key string) T
        + Read~T~(key string, defaultValue T) T
        + ReadAsync~T~(key string) Task~T~
        + Write~T~(key string, value T) void
        + WriteAsync~T~(key string, value T) Task
        - ToAbsolutePath(key string) string
    }

    class FileStorage {
        <<sealed>>
        - _bufferSize int
        - _extension string
        - _lockManager IAsyncKeyLockManager
        - _rootPath string
        - _serializer ISerializer
        - _disposed bool
        + FileStorage(rootPath string, serializer ISerializer, extension string, bufferSize int, lockManager IAsyncKeyLockManager)
        + Dispose() void
        + Delete(key string) void
        + DeleteAsync(key string) Task
        + Exists(key string) bool
        + ExistsAsync(key string) Task~bool~
        + Read~T~(key string) T
        + Read~T~(key string, defaultValue T) T
        + ReadAsync~T~(key string) Task~T~
        + Write~T~(key string, value T) void
        + WriteAsync~T~(key string, value T) Task
        - ToPath(key string) string
        - CleanSegment(segment string) string
    }

    IStorage <|.. GodotFileStorage
    IDisposable <|.. GodotFileStorage
    IFileStorage <|.. FileStorage
    IDisposable <|.. FileStorage

    AsyncKeyLockManager ..|> IAsyncKeyLockManager

    GodotFileStorage --> ISerializer : uses
    GodotFileStorage --> IAsyncKeyLockManager : uses

    FileStorage --> ISerializer : uses
    FileStorage --> IAsyncKeyLockManager : uses
Loading

Architecture flow diagram for shared async lock manager across storage implementations

flowchart LR
    subgraph Core_Assembly[GFramework.Core]
        AsyncKeyLockManager
        IAsyncKeyLockManager
    end

    subgraph Godot_Assembly[GFramework.Godot]
        GodotFileStorage
    end

    subgraph Game_Assembly[GFramework.Game]
        FileStorage
    end

    subgraph Core_Serializer[GFramework.Core.Serializer]
        ISerializer
    end

    GodotFileStorage --> IAsyncKeyLockManager
    FileStorage --> IAsyncKeyLockManager
    AsyncKeyLockManager -.-> IAsyncKeyLockManager

    GodotFileStorage --> ISerializer
    FileStorage --> ISerializer

    GameCode[[Game code]] --> FileStorage
    GodotScene[[Godot scene scripts]] --> GodotFileStorage

    FileStorage -.uses filesystem APIs.-> FileSystem[(File system)]
    GodotFileStorage -.uses Godot FileAccess.-> GodotFileAccess[(Godot FileAccess)]
Loading

File-Level Changes

Change Details Files
Replace per-key ConcurrentDictionary locks with an async key lock manager and align sync APIs on async implementations.
  • Remove per-path ConcurrentDictionary locking and GetLock helpers in both storage implementations.
  • Introduce IAsyncKeyLockManager dependencies (with default AsyncKeyLockManager) and use AcquireLockAsync(path) with await using to guard all file operations.
  • Refactor synchronous methods (Delete, Exists, Read, Write) into thin wrappers over their async counterparts using GetAwaiter().GetResult().
GFramework.Godot/Storage/GodotFileStorage.cs
GFramework.Game/Storage/FileStorage.cs
Add IDisposable support and disposed-state checks to prevent use-after-dispose on storage instances.
  • Make both GodotFileStorage and FileStorage implement IDisposable and track a private _disposed flag.
  • Dispose the injected or internally created IAsyncKeyLockManager in Dispose().
  • Add ObjectDisposedException.ThrowIf(_disposed, this) at the start of async operations that touch the filesystem or serializer.
GFramework.Godot/Storage/GodotFileStorage.cs
GFramework.Game/Storage/FileStorage.cs
Convert file I/O to real async operations and support configurable buffer sizes in the game storage implementation.
  • Replace synchronous File.ReadAllText/File.WriteAllText with File.ReadAllTextAsync/File.WriteAllTextAsync in GodotFileStorage for non-Godot paths.
  • In FileStorage, use FileStream with useAsync: true and a configurable _bufferSize for async reads, and StreamReader.ReadToEndAsync for text loading.
  • Add a bufferSize constructor parameter with a default (8192) to FileStorage and store it as a field.
GFramework.Godot/Storage/GodotFileStorage.cs
GFramework.Game/Storage/FileStorage.cs
Implement atomic writes for game file storage using a temporary file and final move/replace.
  • Change FileStorage.WriteAsync to serialize to a tempPath (path + ".tmp") using an async FileStream and StreamWriter, then move it over the target path with overwrite:true.
  • Wrap the write/move in a try/catch that deletes any leftover temp file on failure before rethrowing.
  • Update XML documentation comments to describe atomic write semantics and async safety.
GFramework.Game/Storage/FileStorage.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

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Mar 11, 2026

DeepSource Code Review

We reviewed changes in 9c2e63f...820cdcf 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 ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

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

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了两个问题,并给出了一些高层次的反馈:

  • 同步方法(ReadWriteExistsDelete)现在通过调用它们对应的异步方法并使用 GetAwaiter().GetResult() 来实现,在某些环境下这会引入死锁风险或导致不必要的线程阻塞;建议要么保留一条完全同步的代码路径,要么在文档中明确说明这些 API 在有上下文绑定(比如 UI/主线程)的线程上使用是不安全的,并鼓励直接使用异步版本。
  • GodotFileStorageFileStorage 在任何情况下都会释放 IAsyncKeyLockManager,即便它是通过依赖注入提供的;建议跟踪锁管理器是否是在内部创建的,并只在内部创建的情况下才进行释放,以避免无意中释放一个共享的或外部拥有的实例。
面向 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- 同步方法(`Read``Write``Exists``Delete`)现在通过调用它们对应的异步方法并使用 `GetAwaiter().GetResult()` 来实现,在某些环境下这会引入死锁风险或导致不必要的线程阻塞;建议要么保留一条完全同步的代码路径,要么在文档中明确说明这些 API 在有上下文绑定(比如 UI/主线程)的线程上使用是不安全的,并鼓励直接使用异步版本。
- `GodotFileStorage``FileStorage` 在任何情况下都会释放 `IAsyncKeyLockManager`,即便它是通过依赖注入提供的;建议跟踪锁管理器是否是在内部创建的,并只在内部创建的情况下才进行释放,以避免无意中释放一个共享的或外部拥有的实例。

## Individual Comments

### Comment 1
<location path="GFramework.Godot/Storage/GodotFileStorage.cs" line_range="53-62" />
<code_context>
-            if (File.Exists(path))
-                File.Delete(path);
-        }
+        DeleteAsync(key).GetAwaiter().GetResult();
     }

</code_context>
<issue_to_address>
**issue (bug_risk):** 在有同步上下文的环境中,对异步方法使用 GetAwaiter().GetResult() 会带来死锁风险。

这里同步方法(Delete/Exists/Read/Write)只是对其异步版本的薄包装,通过 `GetAwaiter().GetResult()` 来调用。这种模式在这些 API 被 Godot 等框架或任何会捕获同步上下文的环境中使用时尤其危险。

为减轻这种风险,可以:
1. 在所有异步方法中的 await 调用后统一加上 `ConfigureAwait(false)`,这样它们就不会尝试在原始上下文中恢复,或者
2. 让同步方法真正同步(使用同步 I/O API),并将异步方法视为单独的异步实现,而不是在异步实现之上再叠加同步封装。

即便保留当前结构,仅仅是添加 `ConfigureAwait(false)` 也会显著降低风险。
</issue_to_address>

### Comment 2
<location path="GFramework.Godot/Storage/GodotFileStorage.cs" line_range="66-69" />
<code_context>
+        ObjectDisposedException.ThrowIf(_disposed, this);
+        var path = ToPath(key);
+
+        await using (await _lockManager.AcquireLockAsync(path))
+        {
+            if (File.Exists(path))
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 建议在该类库性质的存储代码中的所有 await 操作上统一使用 ConfigureAwait(false)。

由于这一存储层可能会被 UI/游戏线程或其它上下文消费,这些 await(`AcquireLockAsync``File.ReadAllTextAsync``File.WriteAllTextAsync` 等)当前会捕获调用方的同步上下文,这可能导致死锁或上下文依赖的行为。在类库代码中,通常的做法是在这类 await 调用上使用 `ConfigureAwait(false)`,例如:

```csharp
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
    content = await File.ReadAllTextAsync(path, Encoding.UTF8).ConfigureAwait(false);
}
```

`FileStorage` 中的其它异步方法也同样适用这个建议。
</issue_to_address>

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

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

  • The synchronous methods (Read, Write, Exists, Delete) now block on their async counterparts via GetAwaiter().GetResult(), which can introduce deadlocks or unnecessary thread blocking in some environments; consider either preserving a purely synchronous code path or documenting that these APIs are not safe for use on a context-bound thread (e.g., UI/main thread) and encouraging direct use of the async variants.
  • Both GodotFileStorage and FileStorage always dispose the IAsyncKeyLockManager, even when it is provided via dependency injection; consider tracking whether the lock manager was created internally and only disposing it in that case, to avoid unintentionally disposing a shared or externally owned instance.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The synchronous methods (`Read`, `Write`, `Exists`, `Delete`) now block on their async counterparts via `GetAwaiter().GetResult()`, which can introduce deadlocks or unnecessary thread blocking in some environments; consider either preserving a purely synchronous code path or documenting that these APIs are not safe for use on a context-bound thread (e.g., UI/main thread) and encouraging direct use of the async variants.
- Both `GodotFileStorage` and `FileStorage` always dispose the `IAsyncKeyLockManager`, even when it is provided via dependency injection; consider tracking whether the lock manager was created internally and only disposing it in that case, to avoid unintentionally disposing a shared or externally owned instance.

## Individual Comments

### Comment 1
<location path="GFramework.Godot/Storage/GodotFileStorage.cs" line_range="53-62" />
<code_context>
-            if (File.Exists(path))
-                File.Delete(path);
-        }
+        DeleteAsync(key).GetAwaiter().GetResult();
     }

</code_context>
<issue_to_address>
**issue (bug_risk):** Using GetAwaiter().GetResult() on async methods can introduce deadlock risk in environments with a synchronization context.

Here the sync methods (Delete/Exists/Read/Write) are thin wrappers over their async versions via `GetAwaiter().GetResult()`. That pattern is especially risky when these APIs are used from frameworks like Godot or any context with a captured synchronization context.

To mitigate this, either:
1. Add `ConfigureAwait(false)` to all awaits in the async methods so they don’t attempt to resume on the original context, or
2. Make the sync methods truly synchronous (using sync I/O APIs) and treat the async methods as separate async implementations, rather than layering sync on top of async.

Even if you keep the current structure, adding `ConfigureAwait(false)` would significantly reduce the risk.
</issue_to_address>

### Comment 2
<location path="GFramework.Godot/Storage/GodotFileStorage.cs" line_range="66-69" />
<code_context>
+        ObjectDisposedException.ThrowIf(_disposed, this);
+        var path = ToPath(key);
+
+        await using (await _lockManager.AcquireLockAsync(path))
+        {
+            if (File.Exists(path))
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider consistently using ConfigureAwait(false) on awaited operations in this library-type storage code.

Since this storage layer may be consumed from UI/game threads or other contexts, these awaits (`AcquireLockAsync`, `File.ReadAllTextAsync`, `File.WriteAllTextAsync`, etc.) currently capture the caller’s synchronization context, which can lead to deadlocks or context-dependent behavior. In library code, it’s standard to use `ConfigureAwait(false)` on such awaits, e.g.:

```csharp
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
    content = await File.ReadAllTextAsync(path, Encoding.UTF8).ConfigureAwait(false);
}
```

The same applies to the other async methods in `FileStorage`.
</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.Godot/Storage/GodotFileStorage.cs Outdated
Comment thread GFramework.Godot/Storage/GodotFileStorage.cs Outdated
GeWuYou added 2 commits March 11, 2026 22:26
- 添加了内部锁管理器所有权标识,防止外部传入的锁管理器被错误释放
- 在构造函数中正确初始化锁管理器的所有权状态
- 在Dispose方法中只释放内部创建的锁管理器,避免重复释放异常
- 为所有同步包装方法添加了ConfigureAwait(false)以避免死锁
- 更新了读取、写入、删除和检查存在的同步方法实现
- 为所有异步操作添加了适当的配置避免上下文切换问题
- 改进了Godot文件存储类的相同资源管理逻辑
- 为所有阻塞式同步方法添加了详细的XML注释警告说明
- 在 FileStorage 中添加 System.IO 和 System.Text 引用
- 在 GodotFileStorage 中整合所有必要的命名空间引用
- 统一并发和序列化接口的依赖注入方式
- 添加 Godot 特定的 FileAccess 类型别名
- 优化 Godot 扩展功能的引用结构
@GeWuYou GeWuYou merged commit d038b67 into main Mar 11, 2026
8 checks passed
@GeWuYou GeWuYou deleted the refactor/storage-async-safety branch March 11, 2026 15:08
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