✓=レビュー済 ○=未レビュー ⚠=Suspect(複数同時表示あり。IDクリックで詳細へ)
| グループ | REQ | ARCH | SPEC | TST | IMPL |
|---|---|---|---|---|---|
| CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC001 ✓ ## インターフェース ```python @spot.mark( save_blob: Optional[bool] = None, key... | TST001 ✓ ## 目的 `@spot.mark()` はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元... | IMPL001 ✓ ## 実装概要 `Spot.mark()` メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の `mark()` と、関数... |
| CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC002 ✓ ## インターフェース ```python @contextmanager def cached_run( self, *funcs: Any... | TST002 ✓ ## 目的 `cached_run` はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーテ... | IMPL002 ✓ ## 実装概要 `Spot.cached_run()` はコンテキストマネージャとして実装。 渡された関数群を内部で `mark()` と同等のラッピングを行... |
| CACHE | REQ001 ✓ 関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。 | ARCH001 ✓ ## コンポーネント構成 ```mermaid graph TD A[bs.Spot Factory] -->|DI/Composition| B[cor... | SPEC003 ✓ ## 概要 `@spot.mark()` デコレータは同期関数と非同期(`async def`)関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャ... | TST003 ✓ ## 目的 beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約(スレッドをまたぐコルーチン実... | IMPL003 ✓ ## 実装概要 `Spot._execute_sync()` と `Spot._execute_async()` が実行エンジンの中核。 両メソッドは同一のフ... |
| リンク方向 | カバー数 | カバー率 | 未カバー |
|---|---|---|---|
| ARCH → REQ | 1 / 1 | 100.0% | — |
| SPEC → ARCH | 1 / 1 | 100.0% | — |
| TST → SPEC | 3 / 3 | 100.0% | — |
| IMPL → SPEC | 3 / 3 | 100.0% | — |
関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。
親: —
子: ARCH001
graph TD
A[bs.Spot Factory] -->|DI/Composition| B[core.Spot]
B --> C[TaskDBBase]
B --> D[SerializerProtocol]
B --> E[BlobStorageBase]
B --> F[StoragePolicyProtocol]
B --> G[LimiterProtocol]
B --> H[LifecyclePolicy]
| コンポーネント | 責務 | インターフェース |
|---|---|---|
| core.Spot | キャッシュエンジン。キー生成→検索→実行→保存の制御 | mark(), cached_run() |
| TaskDBBase | メタデータ永続化。キャッシュキーによる検索と保存 | find_by_key(), insert() |
| SerializerProtocol | データのシリアライズ/デシリアライズ | pack(), unpack() |
| BlobStorageBase | 大規模データの外部ストレージ保存 | save(), load(), delete() |
| StoragePolicyProtocol | Blob保存の判定ポリシー | should_save_as_blob() |
sequenceDiagram
participant User as ユーザーコード
participant Spot as core.Spot
participant DB as TaskDB
participant Ser as Serializer
participant Blob as BlobStorage
User->>Spot: fn(args)
Spot->>DB: find_by_key(cache_key)
alt キャッシュヒット
DB-->>Spot: cached_data
Spot->>Ser: unpack(data)
else キャッシュミス
Spot->>Spot: fn(args) 実行
Spot->>Ser: pack(result)
Spot->>DB: insert(metadata)
opt Blob保存
Spot->>Blob: save(key, data)
end
end
Spot-->>User: result
| 技術領域 | 選定 | 理由 |
|---|---|---|
| メタデータDB | SQLite | ゼロ設定、組み込み可能、十分な性能 |
| シリアライズ | MessagePack | JSONより高速・コンパクト、バイナリ対応 |
| ストレージ | ローカルファイル / クラウド | 小規模はローカル、大規模は外部ストレージ等で拡張可能 |
親: REQ001
@spot.mark(
save_blob: Optional[bool] = None,
keygen: Optional[Union[Callable, KeyGenPolicy]] = None,
input_key_fn: Optional[Union[Callable, KeyGenPolicy]] = None,
version: str | None = None,
content_type: Optional[str | ContentType] = None,
serializer: Optional[SerializerProtocol] = None,
save_sync: Optional[bool] = None,
retention: RetentionSpec = None,
hooks: Optional[Sequence[HookBase]] = None,
)
def my_func(x, y): ...
inspect.iscoroutinefunction(fn) で判定する_execute_sync() に委譲する同期ラッパーを返す_execute_async() に委譲する非同期ラッパーを返す| パラメータ | デフォルト | 説明 |
|---|---|---|
save_blob |
None |
None: StoragePolicy に委譲、True: 強制Blob保存、False: DB内保存 |
keygen |
None |
キャッシュキー生成のカスタマイズ。引数やポリシーを指定可能 |
input_key_fn |
None |
(非推奨)keygen を使用すること |
version |
None |
バージョン文字列。変更するとキャッシュキーが変わり無効化される |
content_type |
None |
戻り値のMIMEタイプ |
serializer |
None |
None: Spotのデフォルトを使用。関数単位でオーバーライド可能 |
save_sync |
None |
保存処理を同期的に行うかどうか(Noneの場合はデフォルトに従う) |
retention |
None |
キャッシュの保持ポリシー("30d" などの文字列や期間オブジェクト等) |
hooks |
None |
関数単位のフックリスト(実行前後やキャッシュヒット時に発火) |
isgeneratorfunction または isasyncgenfunction)を渡した場合 → デコレーション時に ConfigurationError を送出するcached_run 利用時に引数として関数が一つも指定されなかった場合 → ValidationError を送出する__name__, __doc__ は functools.wraps によって保持される@mark (括弧なし) と @mark() (括弧あり) の両方の記法をサポートする親: ARCH001
@contextmanager
def cached_run(
self,
*funcs: Any,
**kwargs: Any
) -> Iterator[Any]: ...
Spot インスタンスからコンテキストマネージャとして呼び出される。funcs に渡された関数が0個の場合はエラーを送出する。Spot.mark(**kwargs) で生成したキャッシュデコレータを適用し、ラップされた関数を生成する。yield してコンテキストブロック内のユーザーコードに提供する。@spot.mark 適用済み関数と同様にキャッシュ機構を経由する。| パラメータ | 必須 | 説明 |
|---|---|---|
*funcs |
はい | キャッシュ機能を適用したい対象の関数。外部ライブラリの関数なども指定可能。複数指定可能。 |
**kwargs |
いいえ | Spot.mark デコレータに渡すオプション引数(save_blob, keygen, version, content_type, serializer, retention, save_sync, hooks 等)。 |
funcs が空(0個)の場合、beautyspot.exceptions.ValidationError を送出し、コンテキストに入らない。mark デコレータの制約に完全に依存する。KeyGen のスコープ規則に従う。親: ARCH001
@spot.mark() デコレータは同期関数と非同期(async def)関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャッシュ検索フローを透過的に切り替える。
inspect.iscoroutinefunction(func) を用いて判定する。_execute_sync() に委譲する。ThreadPoolExecutor) にタスクを投げて非同期化される(save_sync=False の場合)。_execute_async() に委譲する。inspect.isgeneratorfunction または inspect.isasyncgenfunction で真となるジェネレータ・非同期ジェネレータが渡された場合、サポート対象外として即座に ConfigurationError を送出する。on_background_error コールバックが設定されていればそこに処理が委譲される。親: ARCH001
@spot.mark() はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元の関数の挙動やメタデータが損なわれると、ユーザーコードのデバッグや型チェックに支障をきたすため、ラッパーの透過性を厳密に検証する。
save_blob=True 指定時にDB直接保存ではなくBlobストレージ経由で保存されること__name__, __doc__, inspect.signature が元の関数と一致することserializer= パラメータでデフォルトの MsgpackSerializer をカスタム実装に置換できることversion= パラメータを変更するとキャッシュキーが変わり、既存キャッシュがヒットしなくなることreferences: tests/integration/core/test_basic.py
親: SPEC001
子: —
cached_run はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーティ統合シナリオ全体に影響する。戻り値の型が関数の数で変わるスマートリターン仕様は、誤実装すると型安全性を損なうため重点的に検証する。
ValidationError が送出されることreferences: tests/integration/core/test_cached_run.py
親: SPEC002
子: —
beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約(スレッドをまたぐコルーチン実行、例外伝播のタイミング差異)により、同期版とは異なる不具合が発生しやすい。非同期パスの堅牢性を独立して検証する。
inspect.iscoroutinefunction() により同期/非同期が正しく識別され、適切なラッパーが適用されることawait が1回の実行結果を共有することsave_sync=False 時に _BackgroundLoop 経由でキャッシュ保存が非同期実行されることreferences: tests/integration/core/test_async_save.py
親: SPEC003
子: —
Spot.mark() メソッドが本仕様の中核。二段階デコレータファクトリとして、
オプションを受け取る外側の mark() と、関数をラップする内側の decorator() で構成される。
sync/async の判定は inspect.iscoroutinefunction() でデコレーション時に行い、
同期関数は _execute_sync()、非同期関数は _execute_async() に委譲する。
functools.wraps(fn) により元関数のメタデータ(__name__, __doc__, __module__, __qualname__)を保持する。
@mark() (括弧あり)と @mark(括弧なし)の両方をサポートするため、
引数の型で分岐するパターンを採用した。第一引数が callable なら括弧なし、
そうでなければ括弧ありと判定する。
デコレーション時に判定する方式を採用。呼び出し時に毎回 inspect する方式と比較し、
ランタイムオーバーヘッドがゼロになる利点がある。ただしデコレーション後に
関数の性質が動的に変わるケースには対応できない(実用上問題なし)。
inspect.isgeneratorfunction() と inspect.isasyncgenfunction() の
両方をチェックし、ConfigurationError を送出する。
ジェネレータの戻り値はイテレータであり、キャッシュの意味論と矛盾するため。
inspect.signature の保持は functools.wraps が設定する __wrapped__ 属性に依存する_resolve_settings() で Spot レベルのデフォルト → 関数レベルのオーバーライドの順で行われるkeygen パラメータが指定された場合、KeyGenPolicy.bind(fn) でシグネチャにバインドされたキー生成関数に変換される@mark を2回付ける)時の検出・警告が未実装references: src/beautyspot/core.py
親: SPEC001
子: —
Spot.cached_run() はコンテキストマネージャとして実装。
渡された関数群を内部で mark() と同等のラッピングを行い、
コンテキスト内で呼び出すとキャッシュが効く一時的なラッパーを返す。
単一関数の場合は結果を直接返し、複数関数の場合はタプルで返す。
これにより wrapped_fn = spot.cached_run(fn) のように自然に書ける。
0個の場合は ValidationError で早期失敗させる。
@contextmanager デコレータを使用。__enter__ でラップ済み関数を返し、
__exit__ で後処理(drain 等)は with spot: に委譲する設計。
@overload を使用しているfunctools.wraps が元関数に適用されるreferences: src/beautyspot/core.py
親: SPEC002
子: —
Spot._execute_sync() と Spot._execute_async() が実行エンジンの中核。
両メソッドは同一のフロー(キー生成→キャッシュ検索→Herd待機→関数実行→保存)を
それぞれ同期/非同期のセマンティクスで実装する。
mark() がデコレーション時に inspect.iscoroutinefunction() で判定し、
適切な方を選択する。
_execute_sync と _execute_async は処理フローが同一だが、
await の有無やロック機構(threading.Event vs asyncio.Future)が異なるため、
共通化せず並行して実装している。DRY 原則よりも明瞭性・デバッグ容易性を優先した。
Herd protection の結果ボックスへの結果格納は、DB/Blob 保存の前に行う。 これにより、保存が失敗しても待機中のスレッド/タスクは結果を受け取れる。 「保存の失敗で実行結果が失われる」ことを防ぐ意図的な設計。
save_sync=False 時の保存エラーは on_background_error コールバックに
通知し、ERROR ログを出力するが、例外を再送出しない。
関数の実行自体は成功しているため、ユーザーに例外を見せるのは不適切と判断した。
pre_execute, on_cache_hit, on_cache_miss)は
try/except で囲み、フック内の例外が実行結果に影響しないようにしている_eviction_guard_lock で非ブロッキングにガードし、
並行エビクションの重複実行を防止している_execute_async 内の保存処理は _BackgroundLoop.submit() でコルーチンとして投入されるreferences: src/beautyspot/core.py
親: SPEC003
子: —