Make offline replica mutations atomic - #71
Conversation
✅ Deploy Preview for rdlabo-ionic-angular-library ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| #queueWrite(run: (databaseId: string) => Promise<void>): Promise<void> { | ||
| if (this.#atomicMutationRevision !== null) { | ||
| return this.#queueAtomicOperation(async () => this.#atomicTransaction(await this.#databaseConnection(), run)); |
There was a problem hiding this comment.
🔴 同期のまとまった処理の最中に別経路から出された保存が「処理中ではない」エラーで失敗する
まとまった処理の外側から出された保存が、その処理専用の待ち行列に入れられ(#queueWrite の分岐 projects/kit/offline/src/lib/sqlite-offline-repository.ts:794-796)、まとまった処理が終わった後に実行されると必ず失敗するため、その保存内容が失われます。
Impact: 同期中に行われた設定やログイン情報、同期状態の保存が原因不明のエラーで捨てられ、同期が余計に失敗したりやり直しになったりします。
アトミック実行中に外部書き込みが取り込まれる仕組みと競合
#atomicMutationRevision はリポジトリ全体の状態で、これが非nullの間は呼び出し元がどこであっても #queueWrite / #transaction が通常の書き込みキューではなく #queueAtomicOperation に流されます(projects/kit/offline/src/lib/sqlite-offline-repository.ts:794-796, 806-808)。
キューされたクロージャは実際に走る時点で this.#atomicMutationRevision を読みます(projects/kit/offline/src/lib/sqlite-offline-repository.ts:822-824)。[OFFLINE_REPOSITORY_ATOMIC_MUTATION] は await this.#atomicOperations(sqlite-offline-repository.ts:327)でスナップショット時点のチェーンしか待たず、その後に追加された外部書き込みや、operation() が例外を投げた場合に残っている外部書き込みは、finally で #atomicMutationRevision = null(sqlite-offline-repository.ts:333)になった後に走り、Offline replica atomic mutation is not active. を投げます。
この経路は製品コードだけでなくKit自身からも到達します。OfflineSyncService の #markScopeReconciled(projects/kit/offline/src/lib/offline-sync.service.ts:1728-1735)と #persistFatalPullAttentions(offline-sync.service.ts:1079)は mutation lane の外で transactReplica を呼びますし、OfflineSessionService の clearUser / setLastUserId / putSessionManifest(projects/kit/offline/src/lib/offline-session.service.ts:75-95)も lane 外の書き込みです。
併せて、これら lane 外の書き込みは #atomicTransaction の既定 marksCommit = true によって #atomicMutationCommitted を立ててしまうため(sqlite-offline-repository.ts:836)、本来の read/derive/write 側が書き込みを行っていなくても最終のCAS検証(sqlite-offline-repository.ts:328-330)がスキップされます。
Prompt for agents
SqliteOfflineRepository のアトミック変更(OFFLINE_REPOSITORY_ATOMIC_MUTATION)は、リポジトリ全体のフラグ #atomicMutationRevision に依存しているため、mutation lane の外から呼ばれた書き込み(OfflineSyncService の #markScopeReconciled / #persistFatalPullAttentions、OfflineSessionService の clearUser / setLastUserId / putSessionManifest など)まで #atomicOperations キューに取り込まれます。キューされたクロージャは実行時に #atomicMutationRevision を読むため、アトミック処理が終了(または operation() が失敗)した後に走ると 'Offline replica atomic mutation is not active.' で必ず失敗します。また、これら外部書き込みが #atomicMutationCommitted を立ててしまい、本来の read/derive/write に対する最終CAS検証がスキップされる副作用もあります。対処方針としては、(a) アトミック処理の所有者を識別できるようにして、所有者以外の書き込みは通常の #writes キュー(アトミック処理の完了待ち)に回す、(b) キューされたクロージャが自分の開始時点のリビジョンを捕捉して後から null になっても失敗しないようにする、(c) #atomicMutationCommitted を所有者の操作経由の書き込みだけに限定する、といった案が考えられます。
Was this helpful? React with 👍 or 👎 to provide feedback.
| async runReadSnapshot<T>(read: (reader: OfflineRepositoryReader) => Promise<T>): Promise<T> { | ||
| return this.#withCommittedRead(() => read(this.#reader())); | ||
| if (this.#atomicMutationRevision !== null) { | ||
| return this.#queueAtomicOperation(async () => this.#nativeTransaction(await this.#databaseConnection(), () => read(this.#reader()))); | ||
| } | ||
| return this.#transaction(() => read(this.#reader())); |
There was a problem hiding this comment.
🟡 読み取り用スナップショットの中からもう一度データを読むと処理が永久に返らない
読み取り用スナップショットが書き込みの順番待ち列に登録されるようになったため(#transaction 経由 projects/kit/offline/src/lib/sqlite-offline-repository.ts:307-311)、その中からもう一度データを読むと自分自身の完了を待ち続け、処理が永久に返らなくなります。
Impact: 該当する読み取りを行う画面や同期処理がフリーズし、以降のローカル保存もすべて止まります。
デッドロックの発生経路
変更前 runReadSnapshot は #withCommittedRead を使い、reader lease を取るだけだったので、コールバック内から repository.getReplicaRow() などを呼んでも await this.#writes は既に解決済みで問題ありませんでした。
変更後は非アトミック経路が #transaction(...) になり(sqlite-offline-repository.ts:311)、#transaction は this.#writes = transaction...(sqlite-offline-repository.ts:815-818)と自分自身を書き込みチェーンに繋ぎます。コールバック内で #withCommittedRead 系のAPI(getCommands / getReplicaRow など)を呼ぶと await this.#writes(sqlite-offline-repository.ts:759)が実行中の自分のスナップショットを待つため解決しません。
アトミック変更中の経路(sqlite-offline-repository.ts:308-310)でも同様で、スナップショット自体が #atomicOperations に載っているため、内部からのリポジトリ呼び出しは #queueAtomicOperation で自分の後ろに並び解決しません。
Web側の IonicOfflineRepository.runReadSnapshot は従来通り #withCommittedRead のままなので(projects/kit/offline/src/lib/offline-repository.ts:417-419)、プラットフォーム間で挙動が乖離します。契約上は reader 経由での合成が推奨されていますが、従来動いていた呼び出しがハングに変わる点は明示的な検討が必要です。
Prompt for agents
SqliteOfflineRepository.runReadSnapshot が #withCommittedRead から #transaction ベースに変わったことで、スナップショット自体が書き込みチェーン(#writes)またはアトミック操作チェーン(#atomicOperations)を占有します。その結果、read コールバック内から reader ではなくリポジトリ本体の読み取りAPI(getCommands / getReplicaRow など)を呼ぶと、自分自身の完了を待つ形になり永久にハングします。Web の IonicOfflineRepository は従来どおりで挙動が乖離します。対処方針としては、スナップショット実行中であることを示す内部状態を持ち、その間に来た同一フローの読み取りAPIを直接 reader 相当の実装に委譲する(キューに回さない)か、少なくとも自己待ちを検出して明確なエラーを投げてハングを避ける、といった案が考えられます。
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Verification
Design note
The repository-global atomic guard is safe only because every production read/derive/write and command-state write now enters through the single OfflineReplicaMutationCoordinator lane. This deliberately avoids an owner-facade/async-context subsystem.