---
title: Serialize SQLite Writes with a Writer Queue
status: accepted
date: 2026-02-24
context: Internal
---

# Serialize SQLite Writes with a Writer Queue

## Context and Problem Statement / コンテキスト

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

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

## Decision Drivers / 要求

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

## Considered Options / 検討

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

## Decision Outcome / 決定

Chosen option: **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` 保存時の失敗も例外として外に出さず、ログとコールバックで通知。

## Consequences / 決定

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

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

## Notes / 補足

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