Skip to content

Fixed the issue about 'Object was deleted while awaiting a callback.'#50

Open
LingkongSky wants to merge 3 commits into
BAKAOLC:mainfrom
LingkongSky:main
Open

Fixed the issue about 'Object was deleted while awaiting a callback.'#50
LingkongSky wants to merge 3 commits into
BAKAOLC:mainfrom
LingkongSky:main

Conversation

@LingkongSky
Copy link
Copy Markdown
Contributor

Summary / 概要

  • 抑制了 RitsuLib 模组设置子菜单后台初始化任务在 Godot UI 节点于延迟帧等待期间被释放时产生OperationCanceledException 刷屏问题

Why / 背景与动机

  • 在首次进游戏、首次选择模组角色、首次开始新局等流程中,复用的 RitsuModSettingsSubmenu 可能会在被创建后迅速销毁。当拥有者节点被删除时,相关的取消异常会被同步抛出。由于这些任务此前没有统一纳入观察,导致正常的生命周期取消被直接暴露为 Godot Mono 日志里的 Object was deleted while awaiting a callback. 刷屏。

What changed / 变更点

  • 将子菜单在延迟帧等待期间被关闭/释放导致的 OperationCanceledException 视为预期行为
  • 将两个原本未观察的后台启动路径统一接入该观察器:
  • _Ready 中的初始 UI 就绪预热
  • OnSubmenuOpened 中的内容就绪预热

Test plan / 测试计划

  • Build passes
  • Basic runtime smoke test (if applicable)

Notes / 备注

@ritsukage-local-analyzer
Copy link
Copy Markdown

Warning

此分析由 AI 自动生成,仅供开发者参考,最终判断请以人工确认为准。如有疑问建议结合本地环境进一步复现确认。

摘要

本 PR 在 RitsuModSettingsSubmenu 中新增 ObserveBackgroundUiTask / ObserveBackgroundUiTaskAsync 辅助方法,并将 _Ready()OnSubmenuOpened() 中两处原本未被观察的后台异步任务启动路径接入该观察器,从而将节点生命周期释放触发的 OperationCanceledException 静默处理,消除 Godot Mono 日志中的刷屏问题。变更范围精准(+23/-2,单文件),逻辑影响面极小,建议直接合并。

重点结论

  • 变更简介:新增统一后台任务观察器,将两条 fire-and-forget 异步链路(_Ready 初始 UI 预热、OnSubmenuOpened 内容预热)产生的预期生命周期取消异常转为静默,其余异常降级为 Warn 日志。
  • 主要风险:未见明显阻断风险;CancellationToken.None 贯穿所有 AwaitRitsuProcessFrame 调用,唯一取消来源是 RitsuGodotAwaitSafety.ThrowIfInvalid() 的节点有效性检查,此处静默处理语义完全正确。
  • 建议处理:可直接合并,修复目标明确、改动最小。
展开详细审阅、证据与验证

变更概要

字段
base ← head mainmain(跨 fork PR)
draft
changed_files 1
+/- +23 / -2
目标文件 Settings/ModSettingsUi/Core/RitsuModSettingsSubmenu.cs

变更属纯粹防御性修复,不引入新状态、不改变 UI 构建逻辑,仅在两个后台任务调用点外包一层异常观察器。

PR 说明与代码对照

PR body 列出的三项变更点:

说明条目 diff 覆盖情况
OperationCanceledException 视为预期行为 已覆盖:ObserveBackgroundUiTaskAsynccatch (OperationCanceledException) { }
_Ready 初始 UI 就绪预热接入观察器 已覆盖:ObserveBackgroundUiTask(WaitForInitialUiReadyAsync(), "initial_ui_ready")
OnSubmenuOpened 内容就绪预热接入观察器 已覆盖:ObserveBackgroundUiTask(EnsureOpenContentReadyAsync(), "open_content_ready")

详细代码审阅

根本原因链路

RitsuGodotAwaitSafety.ThrowIfInvalid()
  → throw new OperationCanceledException("Godot owner was deleted while awaiting a callback.", ct)

该异常在 EnsureUiUpToDateDeferred / EnsureOpenContentReadyAsync 等异步方法内逐层冒泡,在原有的两处裸 fire-and-forget 路径中无人捕获,最终由 Godot Mono 运行时直接打印到日志。

修复实现(新增,约 19 行)

private void ObserveBackgroundUiTask(Task task, string operation)
{
    _ = ObserveBackgroundUiTaskAsync(task, operation);
}

private static async Task ObserveBackgroundUiTaskAsync(Task task, string operation)
{
    try { await task; }
    catch (OperationCanceledException)
    {
        // Normal when the submenu is closed or freed between deferred frame waits.
    }
    catch (Exception ex)
    {
        RitsuLibFramework.Logger.Warn($"[Settings] Background UI task '{operation}' failed: {ex.Message}");
    }
}
  • OperationCanceledException 静默处理——符合节点生命周期语义。
  • 其余异常保留为 Warn 日志,不会丢失真正的错误信号。
  • _initialUiTaskCancelPendingUiWork() 中被重置为 null,节点若重新加入场景树会触发新一轮预热,逻辑一致。

修改的调用点

位置 修改前(推测) 修改后
_Ready() _ = WaitForInitialUiReadyAsync() ObserveBackgroundUiTask(WaitForInitialUiReadyAsync(), "initial_ui_ready")
OnSubmenuOpened() _ = EnsureOpenContentReadyAsync() ObserveBackgroundUiTask(EnsureOpenContentReadyAsync(), "open_content_ready")

详细验证建议

  • 首次进游戏时打开 Mod 设置界面后立即切换场景,确认不再出现 Object was deleted while awaiting a callback. 刷屏
  • 首次选择 Mod 角色流程中复现原场景,观察 Godot Mono 日志无该异常
  • 确认正常打开/关闭设置界面时 UI 构建仍正常完成(IsInitialUiReady 为 true)
  • 构造一个 UI 构建中真正抛出非取消异常的路径,验证 Warn 日志如预期输出

合并与回滚风险

  • 无数据迁移、无 feature flag,回滚只需 revert 该单文件。
  • 所有既有的非取消异常路径行为不变(转 Warn 而非静默)。

English translation

Summary

This PR adds ObserveBackgroundUiTask / ObserveBackgroundUiTaskAsync helper methods to RitsuModSettingsSubmenu and routes two previously unobserved fire-and-forget async task launch sites (_Ready() and OnSubmenuOpened()) through the new observer. The result: OperationCanceledException caused by Godot node destruction during deferred-frame awaits is now silently suppressed, eliminating the log spam. The change is tightly scoped (+23/-2, single file) and carries minimal risk; merge is recommended.

Key Takeaways

  • Change summary: Adds a unified background task observer; routes the initial-UI warmup (_Ready) and content warmup (OnSubmenuOpened) paths through it, silencing expected lifecycle-cancellation exceptions while still logging all other failures as warnings.
  • Primary risk: No meaningful blocking risk. CancellationToken.None is used throughout all AwaitRitsuProcessFrame calls; the only cancellation source is the node-validity guard in RitsuGodotAwaitSafety.ThrowIfInvalid(), so the silent catch is semantically correct.
  • Recommendation: Merge as-is — the fix is targeted, minimal, and correctly handles the cancellation semantics.
Expand detailed analysis, evidence, and verification

Change overview

Field Value
base ← head mainmain (cross-fork PR)
Draft No
Changed files 1
+/- +23 / -2
Target file Settings/ModSettingsUi/Core/RitsuModSettingsSubmenu.cs

The change is purely defensive: no new state, no UI build logic changes, only an exception observer wrapper added at two task call sites.

PR description vs. diff

PR claim Covered in diff?
Treat OperationCanceledException as expected behavior Yes — catch (OperationCanceledException) { } in ObserveBackgroundUiTaskAsync
Wire _Ready initial UI warmup through observer Yes — ObserveBackgroundUiTask(WaitForInitialUiReadyAsync(), "initial_ui_ready")
Wire OnSubmenuOpened content warmup through observer Yes — ObserveBackgroundUiTask(EnsureOpenContentReadyAsync(), "open_content_ready")

Detailed code review

Root cause chain

RitsuGodotAwaitSafety.ThrowIfInvalid()
  → throw new OperationCanceledException("Godot owner was deleted while awaiting a callback.", ct)

This exception bubbles up through EnsureUiUpToDateDeferred / EnsureOpenContentReadyAsync and, with the old bare fire-and-forget pattern, was caught by Godot Mono's unhandled-exception printer, producing log spam.

Fix implementation (new, ~19 lines)

private void ObserveBackgroundUiTask(Task task, string operation)
{
    _ = ObserveBackgroundUiTaskAsync(task, operation);
}

private static async Task ObserveBackgroundUiTaskAsync(Task task, string operation)
{
    try { await task; }
    catch (OperationCanceledException)
    {
        // Normal when the submenu is closed or freed between deferred frame waits.
    }
    catch (Exception ex)
    {
        RitsuLibFramework.Logger.Warn($"[Settings] Background UI task '{operation}' failed: {ex.Message}");
    }
}
  • OperationCanceledException is silently swallowed — correct for Godot node lifecycle.
  • All other exceptions are preserved as Warn log entries; no error signal is lost.
  • _initialUiTask is reset to null in CancelPendingUiWork(), so a node re-entering the scene tree would trigger a fresh warmup cycle — consistent behavior.

Modified call sites

Location Before (inferred) After
_Ready() bare _ = WaitForInitialUiReadyAsync() ObserveBackgroundUiTask(WaitForInitialUiReadyAsync(), "initial_ui_ready")
OnSubmenuOpened() bare _ = EnsureOpenContentReadyAsync() ObserveBackgroundUiTask(EnsureOpenContentReadyAsync(), "open_content_ready")

Detailed verification checklist

  • Open mod settings on first game entry, then immediately switch scenes — confirm no Object was deleted while awaiting a callback. in Godot Mono log
  • Reproduce original scenario (first mod character selection) and verify log is clean
  • Normal open/close cycle: confirm IsInitialUiReady becomes true and UI builds correctly
  • Induce a genuine non-cancellation exception in the UI build path; verify Warn log appears as expected

Merge and rollback risk

  • No data migration, no feature flags; rollback is a single-file revert.
  • All non-cancellation exception paths remain observable via Warn logging.

工具侧记录
  • BAKAOLC/STS2-RitsuLib#50 · 退出码 0 · 进程 126.33s
  • claude-sonnet-4-6 · API 124795ms · 9 轮 · ↓8 ↑6919 · 缓存读205393 写58263 · $0.3839
  • MCP available: management tools exposed to Claude.

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