局所トレーサビリティ — グループ: BGIO

# 局所トレーサビリティビュー グループ: BGIO

生成日時: 2026-03-13 23:59:50

### 対象アイテム

18

### REQ

1

### ARCH

1

### SPEC

2

### TST

2

### IMPL

2

### ADR

10

### レビュー済

18/18

### Suspect

0

### グループ

BGIO

## トレーサビリティマトリクス

✓=レビュー済 ○=未レビュー ⚠=Suspect（複数同時表示あり。IDクリックで詳細へ）

グループ | REQ | ARCH | SPEC | TST | IMPL | ADR
--- | --- | --- | --- | --- | --- | ---
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR005 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR005 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR024 ✓ # Non-blocking Cache Persistence and Task Tracking ## Context and Problem State...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR033 ✓ # 非同期保存処理におけるエラー可視化とコンテキストの導入 ## Context and Problem Statement / コンテキスト beauty...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR036 ✓ # バックグラウンドループのライフサイクル管理とGC時のデータロスト対策 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR037 ✓ # GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR040 ✓ # Background Loop Shutdown Strategy and Threading Model ## Context and Problem ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR047 ✓ # Background Loop Shutdown Strategy on Garbage Collection ## Context and Proble...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR049 ✓ # 実行中タスクの強参照セットによる追跡 ## Context and Problem Statement / コンテキスト `Spot` インスタンスは、...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR053 ✓ # Background Loop and Executor Shutdown Sequence ## Context and Problem Stateme...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC014 ✓ ## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Corouti... | TST014 ✓ ## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコル... | IMPL014 ✓ ## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_... | ADR001 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR024 ✓ # Non-blocking Cache Persistence and Task Tracking ## Context and Problem State...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR033 ✓ # 非同期保存処理におけるエラー可視化とコンテキストの導入 ## Context and Problem Statement / コンテキスト beauty...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR036 ✓ # バックグラウンドループのライフサイクル管理とGC時のデータロスト対策 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR037 ✓ # GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略 ## Context and Problem Statement / コンテキスト ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR040 ✓ # Background Loop Shutdown Strategy and Threading Model ## Context and Problem ...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR047 ✓ # Background Loop Shutdown Strategy on Garbage Collection ## Context and Proble...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR049 ✓ # 実行中タスクの強参照セットによる追跡 ## Context and Problem Statement / コンテキスト `Spot` インスタンスは、...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR053 ✓ # Background Loop and Executor Shutdown Sequence ## Context and Problem Stateme...
BGIO | REQ006 ✓ キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。 | ARCH006 ✓ ## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop... | SPEC015 ✓ ## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @sp... | TST015 ✓ ## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユ... | IMPL015 ✓ ## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの... | ADR001 ✓ # Manage Executor Lifecycle via Instance Ownership and Weak References ## Conte...

## カバレッジ（局所）

リンク方向 | カバー数 | カバー率 | 未カバー
--- | --- | --- | ---
ARCH → REQ | 1 / 1 | 100.0% | —
SPEC → ARCH | 1 / 1 | 100.0% | —
TST → SPEC | 2 / 2 | 100.0% | —
IMPL → SPEC | 2 / 2 | 100.0% | —
ADR → REQ | 1 / 1 | 100.0% | —

## アイテム詳細

### REQ006 REQ {h(g)} ✓ レビュー済

キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。

キャッシュの保存処理をバックグラウンドで非同期に実行し、関数の応答レイテンシに影響を与えないモードを提供すること。

親: —

子: ADR001, ADR005, ADR024, ADR033, ADR036, ADR037, ADR040, ADR047, ADR049, ADR053, ARCH006

### ARCH006 ARCH {h(g)} ✓ レビュー済

## コンポーネント構成

    graph TD
      A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop]
      B -->|Daemon Thread| C[asyncio.AbstractEventLoop]
      C -->|I/O| D[TaskDB / BlobStorage]
      A -->|ライフサイクル制御| E[atexit / ContextManager]

## コンポーネント責務

コンポーネント | 責務 | インターフェース
--- | --- | ---
_BackgroundLoop | デーモンスレッドでのイベントループ管理とタスク投入 | `submit()`, `run_forever()`
core.Spot | 非同期保存のトリガーとエラーハンドリング | `_save_metadata_async()`
ContextManager | 処理終了時のタスク完了待機（Flush） | `__exit__`, `flush()`

## データフロー

    sequenceDiagram
      participant Main as メインスレッド
      participant BGLoop as _BackgroundLoop
      participant Store as ストレージ

      Main->>BGLoop: submit(save_coro)
      Note over Main: 関数は即座に結果を返す (save_sync=False)
      BGLoop->>Store: 書き込み実行 (I/O)
      alt 成功
          Store-->>BGLoop: OK
      else 失敗
          BGLoop->>Main: on_background_error(error)
      end

## 技術選定

技術領域 | 選定 | 理由
--- | --- | ---
並行処理 | asyncio in Thread | GILを解放しつつ、多数のI/Oタスクを効率的に多重化
スレッド種類 | Daemon Thread | アプリケーション終了時にプロセスをブロックしない
終了制御 | atexit / drain | 強制終了時も、可能な限り保留中の書き込みを完了させる

## 非機能要件方針

- 透過性: 保存失敗時もユーザーコードの例外にはせず、コールバック経由で通知

- 一貫性: `flush()` により、クリティカルな処理の直後での保存完了を保証

- 安全性: シャットダウンシーケンス中に新規タスクを受け付けない排他制御を導入

## コンポーネント構成 ```mermaid graph TD A[core.Spot] -->|非同期タスク投入| B[_BackgroundLoop] B -->|Daemon Thread| C[asyncio.AbstractEventLoop] C -->|I/O| D[TaskDB / BlobStorage] A -->|ライフサイクル制御| E[atexit / ContextManager] ``` ## コンポーネント責務 | コンポーネント | 責務 | インターフェース | |---|---|---| | _BackgroundLoop | デーモンスレッドでのイベントループ管理とタスク投入 | `submit()`, `run_forever()` | | core.Spot | 非同期保存のトリガーとエラーハンドリング | `_save_metadata_async()` | | ContextManager | 処理終了時のタスク完了待機（Flush） | `__exit__`, `flush()` | ## データフロー ```mermaid sequenceDiagram participant Main as メインスレッド participant BGLoop as _BackgroundLoop participant Store as ストレージ Main->>BGLoop: submit(save_coro) Note over Main: 関数は即座に結果を返す (save_sync=False) BGLoop->>Store: 書き込み実行 (I/O) alt 成功 Store-->>BGLoop: OK else 失敗 BGLoop->>Main: on_background_error(error) end ``` ## 技術選定 | 技術領域 | 選定 | 理由 | |---|---|---| | 並行処理 | asyncio in Thread | GILを解放しつつ、多数のI/Oタスクを効率的に多重化 | | スレッド種類 | Daemon Thread | アプリケーション終了時にプロセスをブロックしない | | 終了制御 | atexit / drain | 強制終了時も、可能な限り保留中の書き込みを完了させる | ## 非機能要件方針 - **透過性**: 保存失敗時もユーザーコードの例外にはせず、コールバック経由で通知 - **一貫性**: `flush()` により、クリティカルな処理の直後での保存完了を保証 - **安全性**: シャットダウンシーケンス中に新規タスクを受け付けない排他制御を導入

親: REQ006

子: SPEC014, SPEC015

### SPEC014 SPEC {h(g)} ✓ レビュー済

## インターフェース

    class _BackgroundLoop:
        def submit(self, coro: Coroutine) -> None: ...
        def stop(self) -> None: ...
        def is_running(self) -> bool: ...

## 振る舞い

1. 起動: 初回の `submit` 時にデーモンスレッドを作成し、`asyncio.run()` を開始する
2. 投入: `run_coroutine_threadsafe` を使用して、メインスレッドからコルーチンをイベントループへ投入
3. 待機: 投入されたタスクの完了は待たず、制御を即座に呼び出し元へ返す

## パラメータ詳細

内部属性 | 型 | 説明
--- | --- | ---
`_loop` | `AbstractEventLoop` | スレッド内で稼働するループ本体
`_thread` | `Thread` | `daemon=True` 設定のスレッド

## エラーハンドリング

- 投入失敗: ループ停止中にタスクが投入された場合、エラーをログ出力し無視する
- タスク内エラー: コルーチン内で発生した例外は、`on_background_error` コールバックで処理される

## エッジケース

- 多重起動: すでにスレッドが実行中の場合は、既存のスレッドを再利用する

## インターフェース ```python class _BackgroundLoop: def submit(self, coro: Coroutine) -> None: ... def stop(self) -> None: ... def is_running(self) -> bool: ... ``` ## 振る舞い 1. **起動**: 初回の `submit` 時にデーモンスレッドを作成し、`asyncio.run()` を開始する 2. **投入**: `run_coroutine_threadsafe` を使用して、メインスレッドからコルーチンをイベントループへ投入 3. **待機**: 投入されたタスクの完了は待たず、制御を即座に呼び出し元へ返す ## パラメータ詳細 | 内部属性 | 型 | 説明 | |---|---|---| | `_loop` | `AbstractEventLoop` | スレッド内で稼働するループ本体 | | `_thread` | `Thread` | `daemon=True` 設定のスレッド | ## エラーハンドリング - **投入失敗**: ループ停止中にタスクが投入された場合、エラーをログ出力し無視する - **タスク内エラー**: コルーチン内で発生した例外は、`on_background_error` コールバックで処理される ## エッジケース - **多重起動**: すでにスレッドが実行中の場合は、既存のスレッドを再利用する

親: ARCH006

子: IMPL014, TST014

### SPEC015 SPEC {h(g)} ✓ レビュー済

## インターフェース

    def flush(timeout: float | None = None) -> bool: ...

    @spot.mark(save_sync=False)
    def my_func(): ...

## 振る舞い

### 同期保存 (`save_sync=True`)

- 関数終了後、DBおよびBlobへの保存が完了するまで呼び出し元をブロックする。

### 非同期保存 (`save_sync=False`)

- 保存処理を `BackgroundLoop` に委譲し、即座に関数の戻り値を返す。

### フラッシュ (`flush`)

- `_inflight` タスク（保存中）のリストを確認し、それら全てが完了するまで、またはタイムアウトまで待機する。

## パラメータ詳細

パラメータ | デフォルト | 説明
--- | --- | ---
`timeout` | `None` | `flush` の最大待機時間（秒）。`None` は無限待機
`on_background_error` | `logger.error` | 非同期保存失敗時のエラーハンドラ

## エラーハンドリング

- タイムアウト: `flush` がタイムアウトした場合、`False` を返し、一部のデータが未保存である可能性を通知する。

## エッジケース

- コンテキスト終了: `with spot:` 終了時に自動的に `flush` が呼ばれ、プロセス終了前のデータ保存を確実にする。

## インターフェース ```python def flush(timeout: float | None = None) -> bool: ... @spot.mark(save_sync=False) def my_func(): ... ``` ## 振る舞い ### 同期保存 (`save_sync=True`) - 関数終了後、DBおよびBlobへの保存が完了するまで呼び出し元をブロックする。 ### 非同期保存 (`save_sync=False`) - 保存処理を `BackgroundLoop` に委譲し、即座に関数の戻り値を返す。 ### フラッシュ (`flush`) - `_inflight` タスク（保存中）のリストを確認し、それら全てが完了するまで、またはタイムアウトまで待機する。 ## パラメータ詳細 | パラメータ | デフォルト | 説明 | |---|---|---| | `timeout` | `None` | `flush` の最大待機時間（秒）。`None` は無限待機 | | `on_background_error` | `logger.error` | 非同期保存失敗時のエラーハンドラ | ## エラーハンドリング - **タイムアウト**: `flush` がタイムアウトした場合、`False` を返し、一部のデータが未保存である可能性を通知する。 ## エッジケース - **コンテキスト終了**: `with spot:` 終了時に自動的に `flush` が呼ばれ、プロセス終了前のデータ保存を確実にする。

親: ARCH006

子: IMPL015, TST015

### TST014 TST {h(g)} ✓ レビュー済

## 目的

`_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコルーチン受け渡しは競合状態やデッドロックが発生しやすく、シャットダウン時のタスク消失はデータロスに直結する。

## 検証観点

- 保存の正確性: `submit(coro)` で投入したコルーチンが正しく実行され、結果がDBとストレージに反映されること
- 直列化保証: 同一キーへの保存が投入順に処理され、最終状態が最後に投入されたデータであること
- ゾンビスレッド防止: シャットダウン後にアクティブタスクが `drain_timeout` 秒以内に完了すること。タイムアウト後は強制終了されること
- atexit 安全網: プロセス終了時に atexit ハンドラが発火し、未完了タスクの drain が試行されること
- 新規受付停止: シャットダウン後の `submit()` 呼び出しが受け付けられないこと

## 目的 `_BackgroundLoop` はデーモンスレッドで asyncio イベントループを駆動し、非同期キャッシュ保存を処理する。スレッド間のコルーチン受け渡しは競合状態やデッドロックが発生しやすく、シャットダウン時のタスク消失はデータロスに直結する。 ## 検証観点 - **保存の正確性**: `submit(coro)` で投入したコルーチンが正しく実行され、結果がDBとストレージに反映されること - **直列化保証**: 同一キーへの保存が投入順に処理され、最終状態が最後に投入されたデータであること - **ゾンビスレッド防止**: シャットダウン後にアクティブタスクが `drain_timeout` 秒以内に完了すること。タイムアウト後は強制終了されること - **atexit 安全網**: プロセス終了時に atexit ハンドラが発火し、未完了タスクの drain が試行されること - **新規受付停止**: シャットダウン後の `submit()` 呼び出しが受け付けられないこと

references: tests/integration/core/test_background_loop.py

親: SPEC014

子: —

### TST015 TST {h(g)} ✓ レビュー済

## 目的

`save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユーザーが制御する手段である。`save_sync=False` 使用時にデータが失われないことと、コンテキストマネージャによる確実なフラッシュを保証する。

## 検証観点

- save_sync=True（デフォルト）: キャッシュ保存が呼び出しスレッドで同期的に完了し、関数リターン時点でデータが永続化されていること
- save_sync=False: キャッシュ保存がバックグラウンドに委譲され、関数がレイテンシなしでリターンすること。保存が最終的に完了すること
- flush(): `spot.flush(timeout)` 呼び出しで全ペンディング保存が完了するまで待機すること。タイムアウト時の挙動が明確であること
- コンテキスト exit での drain: `with spot:` ブロック終了時に未完了の保存が自動的に drain されること
- コンテキストマネージャの再利用: 同一 spot インスタンスが複数の `with` ブロックで再利用でき、各ブロック終了時に適切に drain されること
- バックグラウンド保存失敗: 非同期保存が例外を送出した場合に `on_background_error` コールバックが呼ばれ、メインスレッドには影響しないこと

## 目的 `save_sync` パラメータと `flush` / `drain` メカニズムは、キャッシュ保存のレイテンシとデータ安全性のトレードオフをユーザーが制御する手段である。`save_sync=False` 使用時にデータが失われないことと、コンテキストマネージャによる確実なフラッシュを保証する。 ## 検証観点 - **save_sync=True（デフォルト）**: キャッシュ保存が呼び出しスレッドで同期的に完了し、関数リターン時点でデータが永続化されていること - **save_sync=False**: キャッシュ保存がバックグラウンドに委譲され、関数がレイテンシなしでリターンすること。保存が最終的に完了すること - **flush()**: `spot.flush(timeout)` 呼び出しで全ペンディング保存が完了するまで待機すること。タイムアウト時の挙動が明確であること - **コンテキスト exit での drain**: `with spot:` ブロック終了時に未完了の保存が自動的に drain されること - **コンテキストマネージャの再利用**: 同一 spot インスタンスが複数の `with` ブロックで再利用でき、各ブロック終了時に適切に drain されること - **バックグラウンド保存失敗**: 非同期保存が例外を送出した場合に `on_background_error` コールバックが呼ばれ、メインスレッドには影響しないこと

references: tests/integration/core/test_exit_drain.py

親: SPEC015

子: —

### IMPL014 IMPL {h(g)} ✓ レビュー済

## 実装概要

`_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_loop()` で新しいイベントループを作成し、 `_thread`（デーモンスレッド）上で `loop.run_forever()` を実行する。 外部からは `submit()` を通じてコルーチンを投入でき、`run_coroutine_threadsafe` で スレッドセーフにループへ渡される。

## 設計判断

### デーモンスレッドと明示的なシャットダウン

スレッドは `daemon=True` とし、メインスレッド終了時にプロセスがブロックされないようにしている。 しかし、安全なリソース解放のために `drain()` メソッドを提供し、 投入された全タスクの完了を `drain_timeout` の範囲で待機する。

### 独立したイベントループ

メインスレッドのイベントループと競合しないよう、専用のイベントループを 作成してバックグラウンドスレッドで駆動する。これにより `save_sync=False` 時の 保存処理などが、呼び出し元の asyncio 環境から完全に隔離される。

## 実装メモ

- `submit()` は投入されたタスクを `_tasks` 集合に登録し、`add_done_callback` で 完了時に自身を削除することで、未完了タスクの追跡を可能にしている
- シャットダウン時の競合を防ぐため、`_lock` による排他制御を行っている
- `atexit` ハンドラとしてもシャットダウンが登録される

## 実装概要 `_BackgroundLoop` クラスが非同期タスクのバックグラウンド実行を管理する。 初期化時に `asyncio.new_event_loop()` で新しいイベントループを作成し、 `_thread`（デーモンスレッド）上で `loop.run_forever()` を実行する。 外部からは `submit()` を通じてコルーチンを投入でき、`run_coroutine_threadsafe` で スレッドセーフにループへ渡される。 ## 設計判断 ### デーモンスレッドと明示的なシャットダウン スレッドは `daemon=True` とし、メインスレッド終了時にプロセスがブロックされないようにしている。 しかし、安全なリソース解放のために `drain()` メソッドを提供し、 投入された全タスクの完了を `drain_timeout` の範囲で待機する。 ### 独立したイベントループ メインスレッドのイベントループと競合しないよう、専用のイベントループを 作成してバックグラウンドスレッドで駆動する。これにより `save_sync=False` 時の 保存処理などが、呼び出し元の asyncio 環境から完全に隔離される。 ## 実装メモ - `submit()` は投入されたタスクを `_tasks` 集合に登録し、`add_done_callback` で 完了時に自身を削除することで、未完了タスクの追跡を可能にしている - シャットダウン時の競合を防ぐため、`_lock` による排他制御を行っている - `atexit` ハンドラとしてもシャットダウンが登録される

references: src/beautyspot/core.py

親: SPEC014

子: —

### IMPL015 IMPL {h(g)} ✓ レビュー済

## 実装概要

`Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの保存処理は `_bg_loop.submit()` で バックグラウンドに投入される。これら未完了のタスクやDBキューを同期するため、 `flush()` およびコンテキストマネージャによる `drain` が実装されている。

## 設計判断

### flush と drain の使い分け

- `flush()`: DBライタースレッドのキュー (`self.db.flush()`) を空になるまで待機する
- `__exit__`: コンテキストマネージャ終了時に `flush()` を呼び出した上で、 さらに `_bg_loop.drain()` を呼び出し、非同期タスクの完了も待つ これにより、スクリプト終了時やバッチ処理の区切りで、データの完全な永続化を保証する。

### バックグラウンドエラーの隔離

バックグラウンド保存時のエラーは呼び出し元のメインフローを妨げないよう、 `on_background_error` コールバックに通知され、ログ出力のみ行う。 例外の再送出は行わない。

## 実装メモ

- `flush()` にはタイムアウトを設定でき、ハングアップを防止している
- `on_background_error` は `SaveErrorContext` を引数に取り、失敗時の キーや関数の詳細情報を提供する

## 実装概要 `Spot` クラスにおいて、バックグラウンド書き込みの同期と完了待機を制御する。 `save_sync=False` の場合、キャッシュへの保存処理は `_bg_loop.submit()` で バックグラウンドに投入される。これら未完了のタスクやDBキューを同期するため、 `flush()` およびコンテキストマネージャによる `drain` が実装されている。 ## 設計判断 ### flush と drain の使い分け - `flush()`: DBライタースレッドのキュー (`self.db.flush()`) を空になるまで待機する - `__exit__`: コンテキストマネージャ終了時に `flush()` を呼び出した上で、 さらに `_bg_loop.drain()` を呼び出し、非同期タスクの完了も待つ これにより、スクリプト終了時やバッチ処理の区切りで、データの完全な永続化を保証する。 ### バックグラウンドエラーの隔離 バックグラウンド保存時のエラーは呼び出し元のメインフローを妨げないよう、 `on_background_error` コールバックに通知され、ログ出力のみ行う。 例外の再送出は行わない。 ## 実装メモ - `flush()` にはタイムアウトを設定でき、ハングアップを防止している - `on_background_error` は `SaveErrorContext` を引数に取り、失敗時の キーや関数の詳細情報を提供する

references: src/beautyspot/core.py

親: SPEC015

子: —

### ADR005 ADR {h(g)} ✓ レビュー済

# Manage Executor Lifecycle via Instance Ownership and Weak References

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

`src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。

この実装には以下の課題がある：

1. リソース制御の欠如: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。
2. ゾンビプロセス: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。
3. テスト困難性: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。

代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。

## Decision Drivers / 要求

- Developer Experience (DX): ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。
- Configurability: スレッド数や Executor の実装をユーザーが制御できるようにしたい。
- Safety: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。
- Memory Safety: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。

## Considered Options / 検討

- Option 1: 現状維持（グローバル変数のまま）。
- Option 2: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。
- Option 3: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。
- Option 4: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。

## Decision Outcome / 決定

Chosen option: Option 4.

`Project` クラスの設計を以下のように変更する：

1. Executorのインスタンス化: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。
2. Dependency Injection (DI): コンストラクタで外部 `Executor` の注入を許可する。
3. 所有権と責任の分離:
   - 外部注入 (`executor` 引数あり): `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。
   - 内部生成 (`executor` 引数なし): `Project` がライフサイクルを管理する責任を持つ。
4. 自動クリーンアップ: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。

### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`?

単純な `atexit.register(self.shutdown)` を採用しなかった理由は、メモリリーク（循環参照）のリスク である。

- 問題点: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。
- 解決策: `weakref.finalize(self, func, *args)` を採用する。
  - `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。
  - プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。
  - 重要: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。

    # Implementation Sketch
    class Project:
        def __init__(self, ...):
            # ...
            self.executor = ThreadPoolExecutor(...)
            # self を参照しないよう、executor オブジェクトだけを渡す
            self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor)

        @staticmethod
        def _shutdown_executor(executor):
            executor.shutdown(wait=True)

## Consequences / 決定

- Positive:
  - ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。
  - `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。
  - DIにより、テスト時にモックExecutorを差し込むことが容易になる。
- Negative:
  - `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。
  - `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

# Manage Executor Lifecycle via Instance Ownership and Weak References ## Context and Problem Statement / コンテキスト `src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。 この実装には以下の課題がある： 1. **リソース制御の欠如**: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。 2. **ゾンビプロセス**: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。 3. **テスト困難性**: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。 代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。 ## Decision Drivers / 要求 * **Developer Experience (DX)**: ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。 * **Configurability**: スレッド数や Executor の実装をユーザーが制御できるようにしたい。 * **Safety**: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。 * **Memory Safety**: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。 ## Considered Options / 検討 * **Option 1**: 現状維持（グローバル変数のまま）。 * **Option 2**: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。 * **Option 3**: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。 * **Option 4**: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。 ## Decision Outcome / 決定 Chosen option: **Option 4**. `Project` クラスの設計を以下のように変更する： 1. **Executorのインスタンス化**: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。 2. **Dependency Injection (DI)**: コンストラクタで外部 `Executor` の注入を許可する。 3. **所有権と責任の分離**: * **外部注入 (`executor` 引数あり)**: `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。 * **内部生成 (`executor` 引数なし)**: `Project` がライフサイクルを管理する責任を持つ。 4. **自動クリーンアップ**: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。 ### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`? 単純な `atexit.register(self.shutdown)` を採用しなかった理由は、**メモリリーク（循環参照）のリスク** である。 * **問題点**: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。 * **解決策**: `weakref.finalize(self, func, *args)` を採用する。 * `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。 * プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。 * **重要**: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。 ```python # Implementation Sketch class Project: def __init__(self, ...): # ... self.executor = ThreadPoolExecutor(...) # self を参照しないよう、executor オブジェクトだけを渡す self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor) @staticmethod def _shutdown_executor(executor): executor.shutdown(wait=True) ``` ## Consequences / 決定 * **Positive**: * ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。 * `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。 * DIにより、テスト時にモックExecutorを差し込むことが容易になる。 * **Negative**: * `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。 * `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

親: REQ006

子: —

### ADR024 ADR {h(g)} ✓ レビュー済

# Non-blocking Cache Persistence and Task Tracking

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

大規模なデータのシリアライズや、S3 等のリモートストレージへの保存処理は、ユーザーのメインロジックの実行を妨げる（ブロッキング）要因となっていました。「インフラとしてのキャッシュ処理がユーザーを待たせない」という設計思想を強化する必要があります。

## Decision Drivers / 要求

- Latency Reduction: キャッシュの保存処理（特に入出力の重い処理）が、メインスレッドの実行速度を低下させないこと。
- Data Integrity: バックグラウンド保存であっても、プログラムの終了前には全ての保存が完了していることを保証し、データロストを防ぐこと。
- API Reusability: `Spot` インスタンスを何度も再利用でき、かつリソースの適切なクリーンアップ（フラッシュ）が行えること。

## Considered Options / 検討

- Option 1: 全ての保存処理をメインスレッドで同期的に行う。
- Option 2: 保存処理をバックグラウンド（Fire-and-Forget）で実行し、タスク追跡と `with` ブロックによる自動フラッシュの仕組みを導入する。

## Decision Outcome / 決定

Chosen option: Option 2.

キャッシュの保存処理をバックグラウンドで実行し、かつリソースの整合性を保つためのライフサイクル管理を導入します。

1. `wait=False` モードの導入: `@spot.mark` や `cached_run` に `wait` オプションを追加し、保存完了を待たずにメインロジックに結果を返すことを可能にします。
2. タスク追跡メカニズム: 実行中のバックグラウンドタスク（Future）を内部のセットで追跡します。
3. Flush (非破壊的待機) の導入: `with spot:` ブロックを抜ける際に、全てのバックグラウンドタスクの完了を待機する（Flush）処理を実行します。
4. 再利用性の確保: コンテキストを抜けても Executor は即座にシャットダウンせず、`Spot` インスタンスを繰り返し安全に利用可能にします。

## Consequences / 決定

- Positive:
  - レイテンシの改善: 保存処理がバックグラウンド化され、ユーザーの体感速度が向上する。
  - データ整合性の保証: `with` ブロックやプロセス終了時に未完了タスクを必ず待機するため、データの整合性が保たれる。
  - 直感的な API: グローバルに定義したオブジェクトを、何度も安全に「フラッシュ」しながら使い回せる。
- Negative:
  - 例外ハンドリングの複雑化: バックグラウンドで発生したエラーは即座に呼び出し元へ伝播しないため、ログによる通知に依存することになる。

# Non-blocking Cache Persistence and Task Tracking ## Context and Problem Statement / コンテキスト 大規模なデータのシリアライズや、S3 等のリモートストレージへの保存処理は、ユーザーのメインロジックの実行を妨げる（ブロッキング）要因となっていました。「インフラとしてのキャッシュ処理がユーザーを待たせない」という設計思想を強化する必要があります。 ## Decision Drivers / 要求 * **Latency Reduction**: キャッシュの保存処理（特に入出力の重い処理）が、メインスレッドの実行速度を低下させないこと。 * **Data Integrity**: バックグラウンド保存であっても、プログラムの終了前には全ての保存が完了していることを保証し、データロストを防ぐこと。 * **API Reusability**: `Spot` インスタンスを何度も再利用でき、かつリソースの適切なクリーンアップ（フラッシュ）が行えること。 ## Considered Options / 検討 * **Option 1**: 全ての保存処理をメインスレッドで同期的に行う。 * **Option 2**: 保存処理をバックグラウンド（Fire-and-Forget）で実行し、タスク追跡と `with` ブロックによる自動フラッシュの仕組みを導入する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. キャッシュの保存処理をバックグラウンドで実行し、かつリソースの整合性を保つためのライフサイクル管理を導入します。 1. **`wait=False` モードの導入**: `@spot.mark` や `cached_run` に `wait` オプションを追加し、保存完了を待たずにメインロジックに結果を返すことを可能にします。 2. **タスク追跡メカニズム**: 実行中のバックグラウンドタスク（Future）を内部のセットで追跡します。 3. **Flush (非破壊的待機) の導入**: `with spot:` ブロックを抜ける際に、全てのバックグラウンドタスクの完了を待機する（Flush）処理を実行します。 4. **再利用性の確保**: コンテキストを抜けても Executor は即座にシャットダウンせず、`Spot` インスタンスを繰り返し安全に利用可能にします。 ## Consequences / 決定 * **Positive**: * **レイテンシの改善**: 保存処理がバックグラウンド化され、ユーザーの体感速度が向上する。 * **データ整合性の保証**: `with` ブロックやプロセス終了時に未完了タスクを必ず待機するため、データの整合性が保たれる。 * **直感的な API**: グローバルに定義したオブジェクトを、何度も安全に「フラッシュ」しながら使い回せる。 * **Negative**: * **例外ハンドリングの複雑化**: バックグラウンドで発生したエラーは即座に呼び出し元へ伝播しないため、ログによる通知に依存することになる。

親: REQ006

子: —

### ADR033 ADR {h(g)} ✓ レビュー済

# 非同期保存処理におけるエラー可視化とコンテキストの導入

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

beautyspot では、関数の実行結果をキャッシュに保存する際、`wait=False` を指定することでバックグラウンドスレッドでの非同期保存を行うことができます。しかし、バックグラウンドでの保存処理中にエラーが発生した場合、これまではログが出力されるのみ（サイレントフェイル）でした。

メインスレッドをクラッシュさせないという点では安全ですが、ユーザーからすると「なぜかキャッシュが効かない」という原因究明が困難な状態に陥るリスクがありました。また、監視ツールへのエラー通知や、カスタムのリカバリ処理を行うためのフックが存在しませんでした。

## Decision Drivers / 要求

- Observability: バックグラウンドでの保存失敗を、ユーザーが確実に検知し、適切に（監視システムへの通知など）対処できること。
- Developer Experience (DX): エラー情報の取得において、型補完が効く安全なインターフェースを提供すること。
- Safety: エラーハンドリング自体がメインロジックの安定性やパフォーマンスを損なわないこと。

## Considered Options / 検討

- Option 1: 現状維持（ログ出力のみ）。可観測性が低い。
- Option 2: メインスレッドへ例外を伝播させる。アプリケーションの安定性を損なうため不採用。
- Option 3: コールバック関数 (`on_background_error`) の導入。エラー発生時に任意の処理を実行可能にする。

## Decision Outcome / 決定

Chosen option: Option 3.

以下の設計方針で実装します。

1. `on_background_error` 引数の追加: `Spot.__init__` にコールバックを受け付ける引数を追加します。
2. 型安全なコンテキストオブジェクト (`SaveErrorContext`) の導入: ハンドラーには、エラー時の詳細情報（対象の関数名、キャッシュキー、戻り値等）を含む専用のデータクラスを渡します。
3. `result` (評価済みオブジェクト) の包含: デバッグを容易にするため、キャッシュしようとした実際のオブジェクトを含めます。ただしメモリリークのリスクがあるため、Docstring にて警告を記載します。
4. コールバック内の例外保護: コールバック関数自体が失敗した場合でも、バックグラウンドスレッドをクラッシュさせないよう `try-except` で保護します。

## Consequences / 決定

- Positive:
  - ユーザーはバックグラウンド保存の失敗を確実に検知し、監視システムへの通知等が可能になる。
  - 型付きコンテキストにより、安全かつ直感的にエラー詳細へアクセスできる。
  - メインロジックのパフォーマンスや安定性を維持したまま、高い可観測性を実現できる。
- Negative:
  - `SaveErrorContext` で巨大なオブジェクトの参照を保持し続けた場合、メモリを圧迫する可能性がある。

# 非同期保存処理におけるエラー可視化とコンテキストの導入 ## Context and Problem Statement / コンテキスト beautyspot では、関数の実行結果をキャッシュに保存する際、`wait=False` を指定することでバックグラウンドスレッドでの非同期保存を行うことができます。しかし、バックグラウンドでの保存処理中にエラーが発生した場合、これまではログが出力されるのみ（サイレントフェイル）でした。 メインスレッドをクラッシュさせないという点では安全ですが、ユーザーからすると「なぜかキャッシュが効かない」という原因究明が困難な状態に陥るリスクがありました。また、監視ツールへのエラー通知や、カスタムのリカバリ処理を行うためのフックが存在しませんでした。 ## Decision Drivers / 要求 * **Observability**: バックグラウンドでの保存失敗を、ユーザーが確実に検知し、適切に（監視システムへの通知など）対処できること。 * **Developer Experience (DX)**: エラー情報の取得において、型補完が効く安全なインターフェースを提供すること。 * **Safety**: エラーハンドリング自体がメインロジックの安定性やパフォーマンスを損なわないこと。 ## Considered Options / 検討 * **Option 1**: 現状維持（ログ出力のみ）。可観測性が低い。 * **Option 2**: メインスレッドへ例外を伝播させる。アプリケーションの安定性を損なうため不採用。 * **Option 3**: コールバック関数 (`on_background_error`) の導入。エラー発生時に任意の処理を実行可能にする。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 以下の設計方針で実装します。 1. **`on_background_error` 引数の追加:** `Spot.__init__` にコールバックを受け付ける引数を追加します。 2. **型安全なコンテキストオブジェクト (`SaveErrorContext`) の導入:** ハンドラーには、エラー時の詳細情報（対象の関数名、キャッシュキー、戻り値等）を含む専用のデータクラスを渡します。 3. **`result` (評価済みオブジェクト) の包含:** デバッグを容易にするため、キャッシュしようとした実際のオブジェクトを含めます。ただしメモリリークのリスクがあるため、Docstring にて警告を記載します。 4. **コールバック内の例外保護:** コールバック関数自体が失敗した場合でも、バックグラウンドスレッドをクラッシュさせないよう `try-except` で保護します。 ## Consequences / 決定 * **Positive**: * ユーザーはバックグラウンド保存の失敗を確実に検知し、監視システムへの通知等が可能になる。 * 型付きコンテキストにより、安全かつ直感的にエラー詳細へアクセスできる。 * メインロジックのパフォーマンスや安定性を維持したまま、高い可観測性を実現できる。 * **Negative**: * `SaveErrorContext` で巨大なオブジェクトの参照を保持し続けた場合、メモリを圧迫する可能性がある。

親: REQ006

子: —

### ADR036 ADR {h(g)} ✓ レビュー済

# バックグラウンドループのライフサイクル管理とGC時のデータロスト対策

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

`Spot` クラスは非同期のキャッシュ保存を構造的に直列化するため、内部で専用の `asyncio` ループを実行するスレッド (`_BackgroundLoop`) を保持しています。インスタンスがガベージコレクション (GC) によって破棄される際、未保存のキャッシュデータを厳密に待機してしまうと、メインスレッドを予測不能なタイミングでフリーズさせたり、デッドロックを引き起こす危険性があります。

そのため、これまでは安全を優先し、GC 時にはタスクをキャンセルしてリソースを解放する設計となっていました。しかし、この設計では一時的なスコープで `Spot` を使用した場合、GC のタイミングによってキャッシュデータが失われるという課題がありました。

## Decision Drivers / 要求

- Safety: GC 時の予期せぬブロッキングやデッドロックを回避し、アプリケーションの安定性を損なわないこと。
- Data Integrity: バックグラウンドに投入された保存タスクを、可能な限り完了させること。
- Predictability: ユーザーがライフサイクルを制御できる手段を提供し、挙動を明確にすること。

## Considered Options / 検討

- Option 1: GC 時に常に保存完了を待機する。フリーズやデッドロックのリスクが高い。
- Option 2: GC 時に即座にタスクをキャンセルする。データロストのリスクがある。
- Option 3: `atexit` によるグローバルな安全網を導入し、プロセス終了時に未完了タスクをドレインする。あわせて明示的な管理を推奨する。

## Decision Outcome / 決定

Chosen option: Option 3.

1. `atexit` によるグローバルな安全網の追加: `weakref.WeakSet` を用いて起動中のバックグラウンドループを追跡し、プロセス終了時 (`atexit`) に `wait=True` でドレイン（残存タスクの処理）を行うフックを実装します。プロセス終了時であれば、メインスレッドをブロックしてもデッドロックの危険性が低いためです。
2. 明示的なリソース管理の推奨: `Spot` インスタンスは長寿命なオブジェクトとして扱うか、コンテキストマネージャ (`with`) または `shutdown(wait=True)` による明示的な管理を行うべきであることを周知します。

## Consequences / 決定

- Positive:
  - 一時的なスコープで `Spot` を使った場合でも、アプリ終了時まではデータが救済される可能性が高まる。
  - メインスレッドの安全性（GC 時のフリーズ回避）と、キャッシュ保存の確実性のバランスが取れる。
- Negative:
  - `atexit` 時に大量の未完了タスクがあると、プロセス終了に時間がかかる場合がある。

# バックグラウンドループのライフサイクル管理とGC時のデータロスト対策 ## Context and Problem Statement / コンテキスト `Spot` クラスは非同期のキャッシュ保存を構造的に直列化するため、内部で専用の `asyncio` ループを実行するスレッド (`_BackgroundLoop`) を保持しています。インスタンスがガベージコレクション (GC) によって破棄される際、未保存のキャッシュデータを厳密に待機してしまうと、メインスレッドを予測不能なタイミングでフリーズさせたり、デッドロックを引き起こす危険性があります。 そのため、これまでは安全を優先し、GC 時にはタスクをキャンセルしてリソースを解放する設計となっていました。しかし、この設計では一時的なスコープで `Spot` を使用した場合、GC のタイミングによってキャッシュデータが失われるという課題がありました。 ## Decision Drivers / 要求 * **Safety**: GC 時の予期せぬブロッキングやデッドロックを回避し、アプリケーションの安定性を損なわないこと。 * **Data Integrity**: バックグラウンドに投入された保存タスクを、可能な限り完了させること。 * **Predictability**: ユーザーがライフサイクルを制御できる手段を提供し、挙動を明確にすること。 ## Considered Options / 検討 * **Option 1**: GC 時に常に保存完了を待機する。フリーズやデッドロックのリスクが高い。 * **Option 2**: GC 時に即座にタスクをキャンセルする。データロストのリスクがある。 * **Option 3**: `atexit` によるグローバルな安全網を導入し、プロセス終了時に未完了タスクをドレインする。あわせて明示的な管理を推奨する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 1. **`atexit` によるグローバルな安全網の追加**: `weakref.WeakSet` を用いて起動中のバックグラウンドループを追跡し、プロセス終了時 (`atexit`) に `wait=True` でドレイン（残存タスクの処理）を行うフックを実装します。プロセス終了時であれば、メインスレッドをブロックしてもデッドロックの危険性が低いためです。 2. **明示的なリソース管理の推奨**: `Spot` インスタンスは長寿命なオブジェクトとして扱うか、コンテキストマネージャ (`with`) または `shutdown(wait=True)` による明示的な管理を行うべきであることを周知します。 ## Consequences / 決定 * **Positive**: * 一時的なスコープで `Spot` を使った場合でも、アプリ終了時まではデータが救済される可能性が高まる。 * メインスレッドの安全性（GC 時のフリーズ回避）と、キャッシュ保存の確実性のバランスが取れる。 * **Negative**: * `atexit` 時に大量の未完了タスクがあると、プロセス終了に時間がかかる場合がある。

親: REQ006

子: —

### ADR037 ADR {h(g)} ✓ レビュー済

# GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略

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

`beautyspot` の `Spot` クラスは、非同期でのキャッシュ保存タスクを構造的に直列化するため、内部で専用の `asyncio` ループを実行するバックグラウンドスレッド (`_BackgroundLoop`) を保持しています。インスタンスのライフサイクル終了時、これらのバックグラウンドタスクを安全に終了させる必要がありますが、シャットダウンのトリガー（明示的呼び出し、アプリ終了、GC）によって、メインスレッドへの影響とプロセスの振る舞いが大きく異なるという課題がありました。

特に、GC は予測不能なタイミングで発生します。ここでタスク完了を待機（スレッドを `join`）してしまうと、メインスレッドがフリーズしたりデッドロックを引き起こす危険性があります。一方で、待機せずにタスクを強制キャンセルすると、保存待ちだったキャッシュデータが失われてしまいます。

## Decision Drivers / 要求

- Stability: GC 発生時にメイン処理をブロックせず、アプリの安定性とパフォーマンスを維持すること。
- Data Reliability: インスタンスがスコープを抜けた後でも、投入された保存タスクを可能な限り完了させること。
- Safety: プロセス終了時には確実にデータをドレイン（排出）し、不完全な書き込みを防ぐこと。

## Considered Options / 検討

- Option 1: 常に同期的にシャットダウンする（GC 時のフリーズリスク）。
- Option 2: 常に非同期でシャットダウンし、タスクをキャンセルする（データロストリスク）。
- Option 3: シャットダウン方式を状況（明示的、GC、終了時）に応じて使い分ける多段的な戦略を導入する。

## Decision Outcome / 決定

Chosen option: Option 3.

以下の3つの方式を実装し、状況に応じて使い分けます。

1. `stop(wait=True)` (完全同期シャットダウン)
   - 用途: 明示的な `shutdown()`、`with` ブロックの終了、`atexit`。
   - 理由: データの確定を保証するため。プロセス終了時はメインスレッドをブロックしないと OS に強制終了されるため。
2. `stop_gracefully_no_wait()` (非同期ドレイン / スレッド切り離し)
   - 用途: `weakref.finalize` による GC 時。
   - 理由: メインスレッドのフリーズを回避しつつ、既存タスクを完了させてから自発的に終了させるため。
3. `stop(wait=False)` (強制停止)
   - 用途: エラー発生時の緊急停止など。

## Consequences / 決定

- Positive:
  - 堅牢性の向上: `Spot` が一時的なスコープでインスタンス化され GC された場合でも、データロストのリスクがほぼ解消された。
  - 安全性の担保: GC 発生時にメイン処理がブロックされない。
- Negative:
  - 実装が複雑になり、バックグラウンドスレッドの状態管理（状態遷移）が重要になる。

# GC時のデータロスト防止とバックグラウンドループのシャットダウン戦略 ## Context and Problem Statement / コンテキスト `beautyspot` の `Spot` クラスは、非同期でのキャッシュ保存タスクを構造的に直列化するため、内部で専用の `asyncio` ループを実行するバックグラウンドスレッド (`_BackgroundLoop`) を保持しています。インスタンスのライフサイクル終了時、これらのバックグラウンドタスクを安全に終了させる必要がありますが、シャットダウンのトリガー（明示的呼び出し、アプリ終了、GC）によって、メインスレッドへの影響とプロセスの振る舞いが大きく異なるという課題がありました。 特に、GC は予測不能なタイミングで発生します。ここでタスク完了を待機（スレッドを `join`）してしまうと、メインスレッドがフリーズしたりデッドロックを引き起こす危険性があります。一方で、待機せずにタスクを強制キャンセルすると、保存待ちだったキャッシュデータが失われてしまいます。 ## Decision Drivers / 要求 * **Stability**: GC 発生時にメイン処理をブロックせず、アプリの安定性とパフォーマンスを維持すること。 * **Data Reliability**: インスタンスがスコープを抜けた後でも、投入された保存タスクを可能な限り完了させること。 * **Safety**: プロセス終了時には確実にデータをドレイン（排出）し、不完全な書き込みを防ぐこと。 ## Considered Options / 検討 * **Option 1**: 常に同期的にシャットダウンする（GC 時のフリーズリスク）。 * **Option 2**: 常に非同期でシャットダウンし、タスクをキャンセルする（データロストリスク）。 * **Option 3**: シャットダウン方式を状況（明示的、GC、終了時）に応じて使い分ける多段的な戦略を導入する。 ## Decision Outcome / 決定 Chosen option: **Option 3**. 以下の3つの方式を実装し、状況に応じて使い分けます。 1. **`stop(wait=True)`** (完全同期シャットダウン) * **用途:** 明示的な `shutdown()`、`with` ブロックの終了、`atexit`。 * **理由:** データの確定を保証するため。プロセス終了時はメインスレッドをブロックしないと OS に強制終了されるため。 2. **`stop_gracefully_no_wait()`** (非同期ドレイン / スレッド切り離し) * **用途:** `weakref.finalize` による GC 時。 * **理由:** メインスレッドのフリーズを回避しつつ、既存タスクを完了させてから自発的に終了させるため。 3. **`stop(wait=False)`** (強制停止) * **用途:** エラー発生時の緊急停止など。 ## Consequences / 決定 * **Positive**: * **堅牢性の向上**: `Spot` が一時的なスコープでインスタンス化され GC された場合でも、データロストのリスクがほぼ解消された。 * **安全性の担保**: GC 発生時にメイン処理がブロックされない。 * **Negative**: * 実装が複雑になり、バックグラウンドスレッドの状態管理（状態遷移）が重要になる。

親: REQ006

子: —

### ADR040 ADR {h(g)} ✓ レビュー済

# Background Loop Shutdown Strategy and Threading Model

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

バックグラウンドでの非同期 IO タスクを処理するイベントループにおいて、単純に `daemon=True` スレッドを使用すると、メインスレッド終了時にプロセスが即座に強制終了され、データ破損やキャッシュのロストが発生する危険がありました。一方で、`daemon=False` にして `atexit` で待機を試みるアプローチでは、タスクがハングした場合にプロセス全体が永遠に終了しなくなるデッドロックの罠が存在していました。

## Decision Drivers / 要求

- Data Durability: プロセス終了時に、インフライト（実行中）の IO タスクが確実に完了し、ディスクへ永続化されること。
- Reliability: タスクのスケジュールとシャットダウンの間の競合状態 (Race Condition) を排除すること。
- Liveness: ネットワーク障害等でタスクがスタックしても、一定時間後にプロセスが確実に終了し、ゾンビ化を防ぐこと。

## Considered Options / 検討

- Option 1: `daemon=False` スレッドを使用し、`atexit` で待機する。デッドロックやハングのリスクが高い。
- Option 2: `daemon=True` スレッドを維持しつつ、明示的なタスク追跡と猶予期間（Grace Period）付きの `atexit` ハンドラを実装する。

## Decision Outcome / 決定

Chosen option: Option 2.

真の耐障害性を持つ Graceful Shutdown を実現するため、以下のアーキテクチャを採用します。

1. デーモンスレッドの維持: スレッドは `daemon=True` で起動し、プロセス終了時の無限ハングを OS の力で防ぎます。
2. 明示的なタスク追跡とロック: ロックとアクティブタスクカウンタを用いたステートマシンを導入し、`submit` 時と `atexit` 時の競合状態を完全に排除します。
3. Grace Period（猶予期間）の導入: `atexit` フック内でメインスレッドをブロックし、タイムアウト付き（デフォルト5秒）で `_thread.join()` を実行することで、IO タスクが完了するための猶予を与えます。

## Consequences / 決定

- Positive:
  - ユーザーが明示的な終了処理を意識せずとも、プロセス終了時に安全にデータが永続化される。
  - タスクが無限にスタックしても、タイムアウト後にプロセスが終了するため、アプリがハングアップしない。
- Negative:
  - プロセス終了時に、バックグラウンド処理が終わるまで最大 5 秒のブロックが発生する。

# Background Loop Shutdown Strategy and Threading Model ## Context and Problem Statement / コンテキスト バックグラウンドでの非同期 IO タスクを処理するイベントループにおいて、単純に `daemon=True` スレッドを使用すると、メインスレッド終了時にプロセスが即座に強制終了され、データ破損やキャッシュのロストが発生する危険がありました。一方で、`daemon=False` にして `atexit` で待機を試みるアプローチでは、タスクがハングした場合にプロセス全体が永遠に終了しなくなるデッドロックの罠が存在していました。 ## Decision Drivers / 要求 * **Data Durability**: プロセス終了時に、インフライト（実行中）の IO タスクが確実に完了し、ディスクへ永続化されること。 * **Reliability**: タスクのスケジュールとシャットダウンの間の競合状態 (Race Condition) を排除すること。 * **Liveness**: ネットワーク障害等でタスクがスタックしても、一定時間後にプロセスが確実に終了し、ゾンビ化を防ぐこと。 ## Considered Options / 検討 * **Option 1**: `daemon=False` スレッドを使用し、`atexit` で待機する。デッドロックやハングのリスクが高い。 * **Option 2**: `daemon=True` スレッドを維持しつつ、明示的なタスク追跡と猶予期間（Grace Period）付きの `atexit` ハンドラを実装する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. 真の耐障害性を持つ Graceful Shutdown を実現するため、以下のアーキテクチャを採用します。 1. **デーモンスレッドの維持**: スレッドは `daemon=True` で起動し、プロセス終了時の無限ハングを OS の力で防ぎます。 2. **明示的なタスク追跡とロック**: ロックとアクティブタスクカウンタを用いたステートマシンを導入し、`submit` 時と `atexit` 時の競合状態を完全に排除します。 3. **Grace Period（猶予期間）の導入**: `atexit` フック内でメインスレッドをブロックし、タイムアウト付き（デフォルト5秒）で `_thread.join()` を実行することで、IO タスクが完了するための猶予を与えます。 ## Consequences / 決定 * **Positive**: * ユーザーが明示的な終了処理を意識せずとも、プロセス終了時に安全にデータが永続化される。 * タスクが無限にスタックしても、タイムアウト後にプロセスが終了するため、アプリがハングアップしない。 * **Negative**: * プロセス終了時に、バックグラウンド処理が終わるまで最大 5 秒のブロックが発生する。

親: REQ006

子: —

### ADR047 ADR {h(g)} ✓ レビュー済

# Background Loop Shutdown Strategy on Garbage Collection

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

`beautyspot` はキャッシュの永続化をバックグラウンドスレッドにオフロードしています。コンテキストマネージャを使用した場合は安全にタスクがドレインされますが、インスタンスをグローバルに生成して使い捨てた場合など、ガベージコレクション (GC) によってインスタンスが破棄される際の振る舞いが課題となっていました。GC のタイミングで未完了の I/O タスクを待機すべきか、それとも破棄すべきかを決定する必要があります。

## Decision Drivers / 要求

- Responsiveness: GC は任意のタイミングで発生するため、メインスレッドを予期せずフリーズさせないこと。
- Predictability: リソース管理の責務を明確にし、マジックに頼りすぎない予測可能なライフサイクルを提供すること。
- Observability: 適切でない管理によってデータが破棄された場合に、開発者がそれを検知できるようにすること。

## Considered Options / 検討

- Option 1: GC 時にもタスクの完了を待機する。メインスレッドがフリーズするリスクがある。
- Option 2: 未完了タスクを即座に破棄（キャンセル）し、警告を発行する。メインスレッドの安定性を最優先する。

## Decision Outcome / 決定

Chosen option: Option 2.

ガベージコレクション (`weakref.finalize`) によるインスタンス破棄時には、未完了のタスクを待機せず、即座に破棄する（Fail-fast & Non-blocking） ことを決定しました。

同時に、タスクが破棄された場合は `logger.warning` および `ResourceWarning` を発行し、ユーザーに対してコンテキストマネージャや明示的な `shutdown()` の使用を強く促します。

## Consequences / 決定

- Positive:
  - `beautyspot` が原因でアプリケーションの GC やシャットダウンがハングアップしないことが保証される。
  - ライフサイクル管理の重要性をユーザーに明示的に伝えることができる。
- Negative:
  - 不適切な使い方をした場合、キャッシュが永続化されない（データロスト）事象が発生する。
- Mitigation:
  - ドキュメントにてコンテキストマネージャの利用をベストプラクティスとして周知し、安全な管理方法を強調する。

# Background Loop Shutdown Strategy on Garbage Collection ## Context and Problem Statement / コンテキスト `beautyspot` はキャッシュの永続化をバックグラウンドスレッドにオフロードしています。コンテキストマネージャを使用した場合は安全にタスクがドレインされますが、インスタンスをグローバルに生成して使い捨てた場合など、ガベージコレクション (GC) によってインスタンスが破棄される際の振る舞いが課題となっていました。GC のタイミングで未完了の I/O タスクを待機すべきか、それとも破棄すべきかを決定する必要があります。 ## Decision Drivers / 要求 * **Responsiveness**: GC は任意のタイミングで発生するため、メインスレッドを予期せずフリーズさせないこと。 * **Predictability**: リソース管理の責務を明確にし、マジックに頼りすぎない予測可能なライフサイクルを提供すること。 * **Observability**: 適切でない管理によってデータが破棄された場合に、開発者がそれを検知できるようにすること。 ## Considered Options / 検討 * **Option 1**: GC 時にもタスクの完了を待機する。メインスレッドがフリーズするリスクがある。 * **Option 2**: 未完了タスクを即座に破棄（キャンセル）し、警告を発行する。メインスレッドの安定性を最優先する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. ガベージコレクション (`weakref.finalize`) によるインスタンス破棄時には、**未完了のタスクを待機せず、即座に破棄する（Fail-fast & Non-blocking）** ことを決定しました。 同時に、タスクが破棄された場合は `logger.warning` および `ResourceWarning` を発行し、ユーザーに対してコンテキストマネージャや明示的な `shutdown()` の使用を強く促します。 ## Consequences / 決定 * **Positive**: * `beautyspot` が原因でアプリケーションの GC やシャットダウンがハングアップしないことが保証される。 * ライフサイクル管理の重要性をユーザーに明示的に伝えることができる。 * **Negative**: * 不適切な使い方をした場合、キャッシュが永続化されない（データロスト）事象が発生する。 * **Mitigation**: * ドキュメントにてコンテキストマネージャの利用をベストプラクティスとして周知し、安全な管理方法を強調する。

親: REQ006

子: —

### ADR049 ADR {h(g)} ✓ レビュー済

# 実行中タスクの強参照セットによる追跡

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

`Spot` インスタンスは、バックグラウンドで実行中の保存タスク（`Future`）を内部のセットで管理しています。これはシャットダウン時や `flush()` 呼び出し時に、すべてのタスクが完了したかを確認するために必要です。この追跡において、強参照を使用すべきか、それとも GC を妨げない弱参照を使用すべきかを決定する必要があります。

## Decision Drivers / 要求

- Durability: ユーザーコードが `Future` オブジェクトへの参照を即座に破棄した場合でも、タスクが途中で GC されることなく確実に完了まで実行されること。
- Waitability: `flush()` 呼び出し時に、全ての未完了タスクを漏れなく検知し、待機できること。
- Determinism: タスクの追跡ライフサイクルが、GC という非決定的なイベントではなく、タスクの完了という明確なイベントに同期していること。

## Considered Options / 検討

- Option 1: `weakref.WeakSet` を使用する。参照がなくなると自動的に削除されるが、待機すべきタスクが GC のタイミングで勝手に消えてしまうリスクがある。
- Option 2: 通常の `set`（強参照）を使用し、タスク完了のコールバックによって明示的にセットから削除する。

## Decision Outcome / 決定

Chosen option: Option 2.

タスクの追跡には通常の `set` を使用し、強参照によってタスクの生存を保証します。

1. 生存保証: `Spot` が強参照を保持することで、タスクが確実に完了まで実行されることを保証します。
2. 待機可能性: `WeakSet` で発生しうる「待機すべきタスクが勝手に消える」という不透明な挙動を排除します。
3. 決定論的な挙動: タスク削除のタイミングが完了コールバックに紐付くため、挙動が予測可能になり、デバッグやテストが容易になります。

## Consequences / 決定

- Positive:
  - タスクの完了が保証され、データの整合性が高まる。
  - `flush()` の挙動が正確になり、確実なドレインが可能になる。
- Negative:
  - コールバックが呼ばれないような致命的なバグが発生した場合に、メモリを占有し続けるリスクがある。ただし、`atexit` 等のシャットダウン機構によってプロセスレベルでの救済策が講じられているため、許容可能である。

# 実行中タスクの強参照セットによる追跡 ## Context and Problem Statement / コンテキスト `Spot` インスタンスは、バックグラウンドで実行中の保存タスク（`Future`）を内部のセットで管理しています。これはシャットダウン時や `flush()` 呼び出し時に、すべてのタスクが完了したかを確認するために必要です。この追跡において、強参照を使用すべきか、それとも GC を妨げない弱参照を使用すべきかを決定する必要があります。 ## Decision Drivers / 要求 * **Durability**: ユーザーコードが `Future` オブジェクトへの参照を即座に破棄した場合でも、タスクが途中で GC されることなく確実に完了まで実行されること。 * **Waitability**: `flush()` 呼び出し時に、全ての未完了タスクを漏れなく検知し、待機できること。 * **Determinism**: タスクの追跡ライフサイクルが、GC という非決定的なイベントではなく、タスクの完了という明確なイベントに同期していること。 ## Considered Options / 検討 * **Option 1**: `weakref.WeakSet` を使用する。参照がなくなると自動的に削除されるが、待機すべきタスクが GC のタイミングで勝手に消えてしまうリスクがある。 * **Option 2**: 通常の `set`（強参照）を使用し、タスク完了のコールバックによって明示的にセットから削除する。 ## Decision Outcome / 決定 Chosen option: **Option 2**. タスクの追跡には通常の `set` を使用し、強参照によってタスクの生存を保証します。 1. **生存保証**: `Spot` が強参照を保持することで、タスクが確実に完了まで実行されることを保証します。 2. **待機可能性**: `WeakSet` で発生しうる「待機すべきタスクが勝手に消える」という不透明な挙動を排除します。 3. **決定論的な挙動**: タスク削除のタイミングが完了コールバックに紐付くため、挙動が予測可能になり、デバッグやテストが容易になります。 ## Consequences / 決定 * **Positive**: * タスクの完了が保証され、データの整合性が高まる。 * `flush()` の挙動が正確になり、確実なドレインが可能になる。 * **Negative**: * コールバックが呼ばれないような致命的なバグが発生した場合に、メモリを占有し続けるリスクがある。ただし、`atexit` 等のシャットダウン機構によってプロセスレベルでの救済策が講じられているため、許容可能である。

親: REQ006

子: —

### ADR053 ADR {h(g)} ✓ レビュー済

# Background Loop and Executor Shutdown Sequence

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

`beautyspot` では、バックグラウンドでのI/Oタスク処理に専用のスレッド（`_BackgroundLoop`）と、ブロッキング操作を委譲する `ThreadPoolExecutor` の2つを組み合わせて使用しています。 `Spot.shutdown(save_sync=False)` またはプロセスのシャットダウン時に、これら2つのリソースを破棄する順序やタイミングの不整合が問題となりました。

具体的には、バックグラウンドループ内の非同期タスクが `run_in_executor` に依存している状態（I/O待ち）で、先に `Executor` が `cancel_futures=True` で強制終了されると、タスク側で予期せぬ `CancelledError` や `RuntimeError` が発生し、クラッシュやログのノイズ、正常な後処理（一時ファイルの削除やDB切断など）の阻害を引き起こすリスクがありました。

## Decision Drivers / 要求

- Graceful Degradation: 強制終了時（`save_sync=False`）であっても、システムがクラッシュせず、残存タスクが安全に破棄されること。
- Correct Sequencing: リソースの依存関係（Loop -> Executor）に基づいた適切なシャットダウン手順の確立。
- Observability: 意図的なキャンセルによるタスクのドロップが、ユーザーに適切に通知されること（エラーの握りつぶしを防ぐ）。

## Considered Options / 検討

- Option 1: Executorのシャットダウンを常にLoopの完全な終了（タイムアウトなし）まで遅延させる。 -> シャットダウンがブロックし続けるリスクがあり、フェイルファストの要件を満たせない。
- Option 2: LoopとExecutorのシャットダウン順序は維持しつつ、`run_in_executor` を呼び出すバックグラウンドタスク内で `CancelledError` および `RuntimeError` を明示的に捕捉・ハンドリングする。

## Decision Outcome / 決定

Chosen option: Option 2.

シャットダウン手順の順序自体（Loopの停止 -> Executorの停止）は論理的に正しいものの、強制停止（`save_sync=False` または Loop のドレインタイムアウト）時には、Executor内のタスクがキャンセルされることを前提とした防御的プログラミングを採用します。

具体的には、`_save_result_async` 内で `run_in_executor` を呼び出す際に `try...except (asyncio.CancelledError, RuntimeError)` ブロックを設け、シャットダウンによる強制キャンセルを安全に捕捉します。捕捉した場合は、`on_background_error` コールバックを呼び出すか、適切に警告を記録することで、クラッシュを防ぎつつ通知を行います。

## Consequences / 決定

- Positive:
- 強制シャットダウン時にエラーによるクラッシュや未処理例外のログ洪水が発生しなくなる。
- バックグラウンドタスクが安全に中断され、リソースリークを防ぐことができる。
- Negative:
- 強制キャンセルされた保存タスクは破棄されるため、データロストの事実がログ（Warning）にのみ残る。これは `save_sync=False` による明示的な要求動作として許容される。

# Background Loop and Executor Shutdown Sequence ## Context and Problem Statement / コンテキスト `beautyspot` では、バックグラウンドでのI/Oタスク処理に専用のスレッド（`_BackgroundLoop`）と、ブロッキング操作を委譲する `ThreadPoolExecutor` の2つを組み合わせて使用しています。 `Spot.shutdown(save_sync=False)` またはプロセスのシャットダウン時に、これら2つのリソースを破棄する順序やタイミングの不整合が問題となりました。 具体的には、バックグラウンドループ内の非同期タスクが `run_in_executor` に依存している状態（I/O待ち）で、先に `Executor` が `cancel_futures=True` で強制終了されると、タスク側で予期せぬ `CancelledError` や `RuntimeError` が発生し、クラッシュやログのノイズ、正常な後処理（一時ファイルの削除やDB切断など）の阻害を引き起こすリスクがありました。 ## Decision Drivers / 要求 * **Graceful Degradation**: 強制終了時（`save_sync=False`）であっても、システムがクラッシュせず、残存タスクが安全に破棄されること。 * **Correct Sequencing**: リソースの依存関係（Loop -> Executor）に基づいた適切なシャットダウン手順の確立。 * **Observability**: 意図的なキャンセルによるタスクのドロップが、ユーザーに適切に通知されること（エラーの握りつぶしを防ぐ）。 ## Considered Options / 検討 * **Option 1**: Executorのシャットダウンを常にLoopの完全な終了（タイムアウトなし）まで遅延させる。 -> シャットダウンがブロックし続けるリスクがあり、フェイルファストの要件を満たせない。 * **Option 2**: LoopとExecutorのシャットダウン順序は維持しつつ、`run_in_executor` を呼び出すバックグラウンドタスク内で `CancelledError` および `RuntimeError` を明示的に捕捉・ハンドリングする。 ## Decision Outcome / 決定 Chosen option: **Option 2**. シャットダウン手順の順序自体（Loopの停止 -> Executorの停止）は論理的に正しいものの、強制停止（`save_sync=False` または Loop のドレインタイムアウト）時には、Executor内のタスクがキャンセルされることを前提とした防御的プログラミングを採用します。 具体的には、`_save_result_async` 内で `run_in_executor` を呼び出す際に `try...except (asyncio.CancelledError, RuntimeError)` ブロックを設け、シャットダウンによる強制キャンセルを安全に捕捉します。捕捉した場合は、`on_background_error` コールバックを呼び出すか、適切に警告を記録することで、クラッシュを防ぎつつ通知を行います。 ## Consequences / 決定 * **Positive**: * 強制シャットダウン時にエラーによるクラッシュや未処理例外のログ洪水が発生しなくなる。 * バックグラウンドタスクが安全に中断され、リソースリークを防ぐことができる。 * **Negative**: * 強制キャンセルされた保存タスクは破棄されるため、データロストの事実がログ（Warning）にのみ残る。これは `save_sync=False` による明示的な要求動作として許容される。

親: REQ006

子: —

### ADR001 ADR {h(g)} ✓ レビュー済

# Manage Executor Lifecycle via Instance Ownership and Weak References

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

`src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。

この実装には以下の課題がある：

1. リソース制御の欠如: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。
2. ゾンビプロセス: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。
3. テスト困難性: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。

代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。

## Decision Drivers / 要求

- Developer Experience (DX): ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。
- Configurability: スレッド数や Executor の実装をユーザーが制御できるようにしたい。
- Safety: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。
- Memory Safety: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。

## Considered Options / 検討

- Option 1: 現状維持（グローバル変数のまま）。
- Option 2: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。
- Option 3: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。
- Option 4: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。

## Decision Outcome / 決定

Chosen option: Option 4.

`Project` クラスの設計を以下のように変更する：

1. Executorのインスタンス化: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。
2. Dependency Injection (DI): コンストラクタで外部 `Executor` の注入を許可する。
3. 所有権と責任の分離:
   - 外部注入 (`executor` 引数あり): `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。
   - 内部生成 (`executor` 引数なし): `Project` がライフサイクルを管理する責任を持つ。
4. 自動クリーンアップ: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。

### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`?

単純な `atexit.register(self.shutdown)` を採用しなかった理由は、メモリリーク（循環参照）のリスク である。

- 問題点: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。
- 解決策: `weakref.finalize(self, func, *args)` を採用する。
  - `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。
  - プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。
  - 重要: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。

    # Implementation Sketch
    class Project:
        def __init__(self, ...):
            # ...
            self.executor = ThreadPoolExecutor(...)
            # self を参照しないよう、executor オブジェクトだけを渡す
            self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor)

        @staticmethod
        def _shutdown_executor(executor):
            executor.shutdown(wait=True)

## Consequences / 決定

- Positive:
  - ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。
  - `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。
  - DIにより、テスト時にモックExecutorを差し込むことが容易になる。
- Negative:
  - `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。
  - `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

# Manage Executor Lifecycle via Instance Ownership and Weak References ## Context and Problem Statement / コンテキスト `src/beautyspot/core.py` において、非同期タスクのIOオフロード用に `ThreadPoolExecutor` がグローバル変数 `_io_executor` として定義されている。 この実装には以下の課題がある： 1. **リソース制御の欠如**: スレッド数（`max_workers=4`）がハードコードされており、ユーザーが実行環境（AWS Lambda、強力なサーバー等）に合わせて調整できない。 2. **ゾンビプロセス**: プロセス終了時に明示的なシャットダウンが行われないため、環境によってはゾンビプロセス化するリスクがある。 3. **テスト困難性**: グローバル変数はモック化が難しく、ユニットテストの分離を妨げる。 代替案として `with` 文（Context Manager）による管理も検討されたが、`beautyspot` の「デコレータを付与するだけで動作する」という簡易な利用体験（DX）を損なうため、採用には至らなかった。ユーザーコードを変更させずに、安全にリソースをクリーンアップする仕組みが必要である。 ## Decision Drivers / 要求 * **Developer Experience (DX)**: ユーザーに `shutdown()` の呼び出しや `with` 文を強制したくない。 * **Configurability**: スレッド数や Executor の実装をユーザーが制御できるようにしたい。 * **Safety**: プロセス終了時やインスタンス破棄時に、確実にスレッドプールを閉じたい。 * **Memory Safety**: クリーンアップ処理の登録によって、メモリリーク（循環参照）を引き起こしてはならない。 ## Considered Options / 検討 * **Option 1**: 現状維持（グローバル変数のまま）。 * **Option 2**: `Project` を Context Manager 化し、`__exit__` で `shutdown()` を呼ぶ。 * **Option 3**: `atexit` モジュールを使い、終了時に `shutdown()` を呼ぶ。 * **Option 4**: インスタンス管理に変更し、`weakref.finalize` で自動クリーンアップを行う。 ## Decision Outcome / 決定 Chosen option: **Option 4**. `Project` クラスの設計を以下のように変更する： 1. **Executorのインスタンス化**: グローバル変数を廃止し、`Project` インスタンスごとに `ThreadPoolExecutor` を保持する。 2. **Dependency Injection (DI)**: コンストラクタで外部 `Executor` の注入を許可する。 3. **所有権と責任の分離**: * **外部注入 (`executor` 引数あり)**: `Project` はそれを借用するのみ。シャットダウンの責任はユーザー（呼び出し元）にあるため、自動クリーンアップは行わない。 * **内部生成 (`executor` 引数なし)**: `Project` がライフサイクルを管理する責任を持つ。 4. **自動クリーンアップ**: 内部生成した場合に限り、`weakref.finalize` を使用してシャットダウンを自動化する。 ### Technical Details / 技術詳細: Why `weakref.finalize` instead of `atexit`? 単純な `atexit.register(self.shutdown)` を採用しなかった理由は、**メモリリーク（循環参照）のリスク** である。 * **問題点**: `atexit` レジストリにインスタンスメソッド（`self.shutdown`）を登録すると、`atexit` が `self` への強参照（Strong Reference）を保持し続ける。これにより、アプリケーション実行中に `Project` インスタンスが不要になってもガベージコレクション（GC）されず、メモリリークを引き起こす。これを防ぐには `atexit.unregister` が必要になるが、管理が複雑になる。 * **解決策**: `weakref.finalize(self, func, *args)` を採用する。 * `self` がGCされた時点で、即座に `func`（クリーンアップ処理）が実行される。 * プログラム終了時まで `self` が生存していた場合も、`atexit` 相当のタイミングで `func` が実行される。 * **重要**: クリーンアップ関数 `_shutdown_executor` は `staticmethod` とし、引数には `self` ではなく `self.executor` のみを渡す。これにより、ファイナライザが `self` を参照し続けて寿命を延ばしてしまう事故を防ぐ。 ```python # Implementation Sketch class Project: def __init__(self, ...): # ... self.executor = ThreadPoolExecutor(...) # self を参照しないよう、executor オブジェクトだけを渡す self._finalizer = weakref.finalize(self, self._shutdown_executor, self.executor) @staticmethod def _shutdown_executor(executor): executor.shutdown(wait=True) ``` ## Consequences / 決定 * **Positive**: * ユーザーはリソース管理を意識する必要がなくなり、APIもシンプルに保たれる。 * `Project` インスタンスがGCされたタイミングでスレッドプールも回収されるため、リソース効率が良い。 * DIにより、テスト時にモックExecutorを差し込むことが容易になる。 * **Negative**: * `core.py` の実装において、GCの挙動と `weakref` の仕様を理解したコーディングが必要になり、内部的な複雑性が若干増す。 * `Project` インスタンスを大量に生成・破棄するような特殊な使い方をした場合、スレッドプールの生成コストがオーバーヘッドになる可能性がある（ドキュメントでシングルトン的な利用を推奨することで緩和する）。

親: REQ006

子: —