active: true
derived: false
groups:
- DB
header: |
  ライターキューによるSQLite書き込みの直列化
level: 1.42
links:
- ARCH003: hztRUPwAahIBkVhPgOSP_qIfRnh6FSkrUlxLmmzOQg8=
- REQ003: LqZhQpXoQqgtPpH_Y5OeHhk5km5jT5y5F4tO2Snt64g=
normative: true
ref: ''
reviewed: _3PMviN7t7WV5w6LCT8sV7dA6mKqPz3ugIeoCmgqJlw=
status: accepted
text: |
  # ライターキューによるSQLite書き込みの直列化

  ## コンテキストと課題

  `beautyspot` は `wait=False` のバックグラウンド保存や `async` 経路で、
  複数スレッドから同時に SQLite へ書き込みを行う可能性がある。
  WAL モードを有効化していても SQLite の書き込みは単一ライター制約があり、
  高並行時に `database is locked` が発生し得る。

  一方で、アプリケーションの関数実行自体は失敗させたくない。
  また、シャットダウン時には必ず書き込みをフラッシュしたい。

  ## 判断基準

  * **Reliability**: プロセス内の書き込み競合を回避し、安定して保存する。
  * **Non-blocking API**: キャッシュ保存の失敗で関数実行を失敗させない。
  * **Flush Guarantee**: `shutdown(wait=True)` で書き込みを確実にフラッシュする。
  * **Minimal Surface Change**: 公開 API の破壊的変更を避ける。

  ## 検討した選択肢

  * **Option 1**: 現状維持（都度接続 + timeout）。
  * **Option 2**: コネクションプールを導入。
  * **Option 3**: プロセス内ロックで `save/delete` を直列化（単一接続は持たない）。
  * **Option 4**: 専用 writer スレッド + キューで直列化（単一接続を保持）。
  * **Option 5**: 外部 DB 利用を推奨（SQLite を事実上やめる）。

  ## 決定事項

  採用した選択肢: **Option 4**.

  `SQLiteTaskDB` に writer スレッドとキューを実装し、
  書き込み系操作を単一接続で直列化する。
  シャットダウン時は `shutdown(wait=True)` によりキューを drain し、
  未処理の書き込みが残らないようにする。

  さらに、保存失敗は関数実行を失敗させず、
  ログと `on_background_error` で通知する。

  ### Implementation Sketch / 実装概要

  * `SQLiteTaskDB` 内部に `queue.Queue` と writer スレッドを保持。
  * 書き込み系メソッドは `_enqueue_write()` 経由でシリアライズ。
  * `TaskDBBase.shutdown()` を追加し、`SQLiteTaskDB.shutdown(wait=True)` でフラッシュ。
  * `Spot.shutdown(wait=True)` から `db.shutdown(wait=True)` を呼び出す。
  * `wait=True` 保存時の失敗も例外として外に出さず、ログとコールバックで通知。

  ## 影響・結果

  * **ポジティブ**:
    * プロセス内の書き込み競合が解消され、`database is locked` の発生率が下がる。
    * `shutdown(wait=True)` で書き込みを確実にフラッシュできる。
    * 保存失敗がアプリケーションの関数実行に影響しない。

  * **ネガティブ**:
    * 書き込みは直列化されるため、ピーク時の書き込み待ちが増える可能性がある。
    * writer スレッドがハングした場合、`shutdown(wait=True)` がブロックし続ける。
    * 他プロセスによる競合は依然として発生し得る。

  ## Notes / 補足

  本 ADR はプロセス内の直列化を対象とする。複数プロセスが同一 SQLite を共有する
  運用では、依然としてロック競合が起こり得るため注意する。
