Skip to content

Make offline replica mutations atomic - #71

Merged
rdlabo merged 2 commits into
mainfrom
agent/atomic-offline-replica
Aug 13, 2026
Merged

Make offline replica mutations atomic#71
rdlabo merged 2 commits into
mainfrom
agent/atomic-offline-replica

Conversation

@rdlabo

@rdlabo rdlabo commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • make native SQLite reads use real transactions and guard read/derive/write commits against other native connections with a data-version CAS
  • serialize failed-command persistence through the existing replica mutation lane instead of adding a second ownership abstraction
  • remove the redundant reconciliation-scope state source and restore pending pull work from durable awaiting_pull commands
  • keep legacy physical marker storage only for upgrade cleanup

Verification

  • full Kit suite: 44 files / 819 tests
  • real SQLite two-connection concurrency tests
  • offline ESLint and production package build
  • git diff check

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.

@netlify

netlify Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy Preview for rdlabo-ionic-angular-library ready!

Name Link
🔨 Latest commit d10ac6c
🔍 Latest deploy log https://app.netlify.com/projects/rdlabo-ionic-angular-library/deploys/6a7d92a07e9eaa000813a24a
😎 Deploy Preview https://deploy-preview-71--rdlabo-ionic-angular-library.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@rdlabo
rdlabo marked this pull request as ready for review August 13, 2026 09:49
@rdlabo
rdlabo merged commit 78171b2 into main Aug 13, 2026
12 checks passed
@rdlabo
rdlabo deleted the agent/atomic-offline-replica branch August 13, 2026 09:50

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines 794 to +796
#queueWrite(run: (databaseId: string) => Promise<void>): Promise<void> {
if (this.#atomicMutationRevision !== null) {
return this.#queueAtomicOperation(async () => this.#atomicTransaction(await this.#databaseConnection(), run));

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.

🔴 同期のまとまった処理の最中に別経路から出された保存が「処理中ではない」エラーで失敗する

まとまった処理の外側から出された保存が、その処理専用の待ち行列に入れられ(#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 を呼びますし、OfflineSessionServiceclearUser / 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 を所有者の操作経由の書き込みだけに限定する、といった案が考えられます。
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 307 to +311
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()));

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.

🟡 読み取り用スナップショットの中からもう一度データを読むと処理が永久に返らない

読み取り用スナップショットが書き込みの順番待ち列に登録されるようになったため(#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)、#transactionthis.#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 相当の実装に委譲する(キューに回さない)か、少なくとも自己待ちを検出して明確なエラーを投げてハングを避ける、といった案が考えられます。
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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