Skip to content

fix: make IndexedDB cancellation callback-safe - #70

Merged
kochounoyume merged 5 commits into
mainfrom
feature/fix-indexeddb-cancellation-lifetime
Jul 25, 2026
Merged

fix: make IndexedDB cancellation callback-safe#70
kochounoyume merged 5 commits into
mainfrom
feature/fix-indexeddb-cancellation-lifetime

Conversation

@kochounoyume

Copy link
Copy Markdown
Member

概要

WebGL の IndexedDB 操作をキャンセルした後、遅れて到着する JavaScript コールバックが解放済み/再利用済みの GCHandle を参照し得る問題を修正します。

C# 側のコールバック寿命管理と JavaScript 側の exactly-once 完了保証は片方だけでは安全性を満たせないため、同じPRにまとめています。公開 API は変更しません。

発生していた問題

IDBValueTaskSource は pool され、JavaScript へ GCHandle のポインターを渡します。従来はキャンセルにより managed の ValueTask が完了すると、consumer の GetResult 時点で GCHandle を解放し、source を reset して pool へ返していました。

しかし、ブラウザー側の IndexedDB request 自体はキャンセルできず、後から success / error callback が到着します。そのため、遅延 callback が解放済みハンドルへ触れたり、pool から別操作へ再利用された source を誤って完了させる可能性がありました。

逆に C# 側だけで native callback を待つ設計へ変えても、JavaScript 側が terminal callback を必ず返さなければハンドルを解放できず、リークします。従来の jslib には同期例外、open blocked、transaction abort など、完了通知が欠ける経路があり、複数エラー経路から重複 callback が起きる可能性もありました。

修正内容

C# 側

  • managed 完了、native 終端、consumer 完了を別々の状態として追跡
  • GCHandle は native terminal callback 到着時に解放
  • source は native と consumer の双方が完了した後にだけ pool へ返却
  • 遅延/重複 callback を無視し、1操作を一度だけ完了
  • キャンセル直後に次の IndexedDB 操作を行う回帰テストを追加
  • 待機には Unity の Awaitable.WaitForSecondsAsync を使用

JavaScript 側

  • 各操作へ completed guard と共通完了処理を導入
  • success / error の callback を exactly once に制限
  • 同期例外、onblocked、transaction の onabort / onerror を terminal error として通知
  • 全終了経路で database connection を close
  • エラー文字列用メモリを callback 後に解放
  • 読み出しデータは HEAPU8.slice で独立コピーして寿命を明確化

影響範囲

  • WebGL IndexedDB の内部寿命管理のみで、公開 API の変更なし
  • managed await は従来通りキャンセルとして完了
  • browser request の terminal callback までは内部 state を保持し、use-after-free と pool 誤再利用を防止
  • JavaScript 側で terminal callback を保証し、保持期間が無期限になるリークも防止

検証

  • dotnet format を変更 C# ファイルへ実行
  • Rider の対象ファイル検査: problem 0
  • Unity コンパイル成功
  • WebGL Player テスト画面で All test(s) succeeded を確認

テスト結果受け渡しについて

ブラウザー上のテスト自体は全件成功しました。一方、Unity Test Framework から Editor への結果送信時に接続が切れることがあり、Editor 側では結果取得に失敗する場合があります。今回もブラウザー画面では成功を確認しており、既知の接続不安定性とテスト失敗は区別しています。

@kochounoyume kochounoyume self-assigned this Jul 18, 2026
@kochounoyume
kochounoyume marked this pull request as ready for review July 18, 2026 19:21
Copilot AI review requested due to automatic review settings July 18, 2026 19:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the WebGL IndexedDB bridge against cancellation races by ensuring delayed JavaScript callbacks cannot observe freed/reused GCHandles, and by adding tests to cover cancellation → subsequent operation correctness.

Changes:

  • Refactors IDBValueTaskSource to track managed/native/consumer completion separately and only return pooled sources after both native terminal callback + consumer completion.
  • Adds CompletionState flags/helpers to support the new lifecycle model.
  • Updates the WebGL .jslib to enforce exactly-once completion and to close DB / free temporary buffers across terminal paths; adds regression tests for pooling and cancellation.

Reviewed changes

Copilot reviewed 4 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Runtime/IDBValueTaskSource.cs Separates managed/native/consumer completion and delays pooling until both are done.
src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Runtime/CompletionState.cs Introduces completion flags + small helpers to coordinate lifecycle safely.
src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Runtime/CompletionState.cs.meta Unity meta for the new runtime file.
src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Plugins/WebGL/native_idb.jslib Adds a completion guard, broader terminal error handling, and tighter memory/DB cleanup.
src/LocalPrefs.Unity/Assets/Tests/IDBUtilsTest.cs Adds regression tests for pooling behavior and cancel-followed-by-next-operation correctness.
src/LocalPrefs.Unity/Assets/Tests/IDBUtilsTest.cs.meta Unity meta for the new test file.
Files not reviewed (2)
  • src/LocalPrefs.Unity/Assets/Tests/IDBUtilsTest.cs.meta: Generated file
  • src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Runtime/CompletionState.cs.meta: Generated file
Comments suppressed due to low confidence (3)

src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Plugins/WebGL/native_idb.jslib:45

  • onupgradeneeded can throw (e.g., createObjectStore failures). If that happens, no terminal callback is sent to C#, leaving the GCHandle allocated and the operation hanging/leaking. Wrap the handler in try/catch and route failures through fail(...) so exactly-once completion still holds.
        request.onupgradeneeded = function (event) {
            const db = event.target.result;
            if (!db.objectStoreNames.contains(Config.STORE_NAME)) {
                db.createObjectStore(Config.STORE_NAME);
            }

src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Plugins/WebGL/native_idb.jslib:126

  • Same as above: an exception in onupgradeneeded would skip notifying C# and leak the pending operation/handle. Use try/catch and call fail(exception, db) to guarantee a terminal callback.
        request.onupgradeneeded = function (event) {
            const db = event.target.result;
            if (!db.objectStoreNames.contains(Config.STORE_NAME)) {
                db.createObjectStore(Config.STORE_NAME);
            }

src/LocalPrefs.Unity/Packages/jp.andantetribe.localprefs/Plugins/WebGL/native_idb.jslib:207

  • Same risk here: if onupgradeneeded throws, completed is never set and neither success nor error is invoked, so the managed await can hang and the GCHandle won’t be released. Catch exceptions and forward them to fail(...).
        request.onupgradeneeded = function (event) {
            const db = event.target.result;
            if (!db.objectStoreNames.contains(Config.STORE_NAME)) {
                db.createObjectStore(Config.STORE_NAME);
            }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kochounoyume
kochounoyume merged commit f4b7436 into main Jul 25, 2026
8 checks passed
@kochounoyume
kochounoyume deleted the feature/fix-indexeddb-cancellation-lifetime branch July 25, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants