Doorstop Specifications

Generated at 2026-03-09 23:27:17

REQ Specification

1.0 序文 REQ014

本ドキュメントは、Python関数キャッシュおよびレート制限ライブラリ『beautyspot』の要件を定義する。

2.0 背景・動機 REQ015

データパイプラインや機械学習の実験、外部API呼び出し等において、同一入力での不要な再実行を防ぐことで、実行時間の短縮とリソースの節約を実現する。開発者が最小限のコード変更で強力なキャッシュ・レート制限機能を導入できることを目指す。

3.0 用語定義 REQ016

4.0 機能要件 REQ017

本章ではシステムが提供すべき具体的な機能要件について定義する。

4.1 関数キャッシュ REQ001
CACHE

関数の実行結果をキャッシュし、同一入力に対して再実行せずにキャッシュから結果を返せること。同期関数・非同期関数の両方をサポートすること。

4.2 キャッシュキー生成 REQ002
KEY

関数の引数から安定かつ一意なキャッシュキーを生成できること。引数の型・構造に応じた正規化を行い、辞書のキー順序やコレクション型の違いを正しく区別できること。

4.3 メタデータDB REQ003
DB

キャッシュのメタデータ(関数名、入力ID、バージョン、有効期限等)をデータベースに永続化できること。

抽象インターフェースにより、DBやストレージなどのバックエンド実装を差し替え可能であること。

4.4 Blobストレージ REQ004
STORE

大きなキャッシュデータを外部ストレージ(ローカルファイルシステムやS3等)に保存できること。

DB直接保存とBlob保存を自動判定できること。

4.5 シリアライゼーション REQ005
SERIAL

関数の戻り値を安全にシリアライズ・デシリアライズできること。ユーザー定義型のカスタムエンコーダ・デコーダを登録可能であること。

4.6 バックグラウンドIO REQ006
BGIO

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

4.7 ライフサイクル管理 REQ007
LIFE

キャッシュの有効期限を関数名パターンに基づくルールで設定できること。個別の関数に対して保持期間を指定でき、期限切れのキャッシュを自動的に無効化できること。

4.8 レート制限 REQ008
LIMIT

関数の実行頻度をトークンバケット方式で制限し、外部APIの呼び出しレートを制御できること。同期・非同期の両方に対応すること。

4.9 フック機能 REQ009
HOOK

関数の実行前、キャッシュヒット時、キャッシュミス時にカスタムコールバック(フック)を実行できること。スレッドセーフなフック実装を提供すること。

4.10 メンテナンス REQ010
MAINT

期限切れキャッシュの削除、孤立Blobファイルのガベージコレクション、時間ベースの一括削除(prune)等のメンテナンス操作ができること。

4.11 CLI REQ011
CLI

コマンドラインインターフェースからキャッシュの一覧表示、詳細確認、統計表示、削除、GC等の管理操作ができること。

4.12 依存注入設計 REQ012
DI

全コンポーネント(DB、ストレージ、シリアライザ、リミッター等)が Protocol/ABC に基づく依存注入により差し替え可能であること。ファクトリ関数がデフォルトの組み立てを提供すること。

4.13 Thundering Herd対策 REQ013
HERD

同一キャッシュキーに対する並行リクエストを直列化し、1つのリクエストのみが関数を実行し、他のリクエストはその結果を共有することで重複実行を防止できること。

ARCH Specification

1.0 序文 ARCH014

本ドキュメントは、『beautyspot』のアーキテクチャ設計を定義する。

2.0 構成要素 ARCH015

システムを構成する主要コンポーネントとその責務、およびコンポーネント間の相互作用を定義する。

2.1 関数キャッシュアーキテクチャ ARCH001
CACHE

コンポーネント構成

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より高速・コンパクト、バイナリ対応
ストレージ ローカルファイル / クラウド 小規模はローカル、大規模は外部ストレージ等で拡張可能

非機能要件方針

2.2 キャッシュキー生成アーキテクチャ ARCH002
KEY

コンポーネント構成

graph TD
  A[core.Spot] -->|キャッシュキー生成要求| B[KeyGenPolicy]
  B -->|バインド| C[bound_keygen]
  C -->|引数別戦略| D[Strategy]
  C -->|正規化| E[canonicalize]
  C -->|ハッシュ計算| F[KeyGen.hash_items]
  E --> F

コンポーネント責務

コンポーネント 責務 インターフェース
KeyGenPolicy 関数の引数に対するハッシュ化戦略(無視、ファイル内容等)の宣言的定義と保持。 bind(func)
Strategy 引数ごとの処理方法(DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT)の定義。 -
canonicalize Pythonオブジェクトを再帰的に正規化し、Msgpackで安定してシリアライズ可能な状態に変換。 @singledispatch canonicalize(obj)
KeyGen 正規化されたリストからSHA-256ハッシュを生成。レガシーデフォルトのハッシュ生成も担う。 hash_items(items), from_file_content(path)

データフロー

sequenceDiagram
  participant Spot as core.Spot
  participant KP as KeyGenPolicy
  participant Sig as inspect.signature
  participant Can as canonicalize
  participant KG as KeyGen

  Spot->>KP: bind(func)
  KP->>Sig: signature(func)
  Sig-->>KP: bound_keygen 生成
  KP-->>Spot: bound_keygen
  Spot->>Spot: bound_keygen(*args, **kwargs)
  loop 各引数
    alt Strategy.IGNORE
      Spot->>Spot: スキップ
    else Strategy.FILE_CONTENT
      Spot->>KG: from_file_content(path)
    else Strategy.PATH_STAT
      Spot->>KG: from_path_stat(path)
    else Strategy.DEFAULT
      Spot->>Can: canonicalize(val)
      Can-->>Spot: 正規化済みデータ
    end
  end
  Spot->>KG: hash_items(items_to_hash)
  KG-->>Spot: SHA-256 ハッシュ文字列 (キャッシュキー)

技術選定

技術領域 選定 理由
引数のバインド inspect.signature argsとkwargsを定義順に正確にマッピングし、デフォルト値も適用するため
正規化 functools.singledispatch 型ごとの正規化ロジックを拡張可能かつクリーンに実装するため
シリアライズ msgpack 高速でバイナリセーフであり、一貫したバイト列表現を得るため
ハッシュ関数 SHA-256 衝突確率が極めて低く、暗号学的に安全かつ広くサポートされているため

非機能要件方針

2.3 メタデータDBアーキテクチャ ARCH003
DB

コンポーネント構成

classDiagram
  class TaskDBCore {
    <<Protocol>>
    +init_schema() void
    +get(cache_key) TaskRecord
    +save(cache_key, ...) void
    +delete(cache_key) bool
  }
  class Maintenable {
    <<Protocol>>
    +delete_expired() int
    +prune(older_than) int
    +delete_all() int
  }
  class TaskDBMaintenable {
    <<Protocol>>
  }
  TaskDBCore <|-- TaskDBMaintenable
  Maintenable <|-- TaskDBMaintenable

  class SQLiteTaskDB {
    -db_path: Path
    -_write_queue: Queue
    +init_schema() void
    +get(cache_key) TaskRecord
    +save(cache_key, ...) void
  }
  TaskDBMaintenable <|-- SQLiteTaskDB

コンポーネント責務

コンポーネント 責務 インターフェース
TaskDBCore キャッシュ実行時に必要な最小限のメタデータDBアクセス init_schema(), get(), save(), delete()
Maintenable GCやCLI等、運用・保守に必要な拡張操作 delete_expired(), prune(), delete_all()
TaskDBMaintenable 実行用と保守用の両方を備えた上位Protocol TaskDBCore + Maintenable
SQLiteTaskDB SQLiteを用いたデフォルト実装。非同期書き込みキューを内包 TaskDBMaintenable 実装

データフロー

sequenceDiagram
  participant Spot as core.Spot
  participant DB as SQLiteTaskDB
  participant Q as WriteQueue
  participant Thread as WriterThread
  participant SQLite as SQLiteDB

  Spot->>DB: get(cache_key)
  DB->>SQLite: SELECT
  SQLite-->>DB: TaskRecord
  DB-->>Spot: TaskRecord

  Spot->>DB: save(metadata)
  DB->>Q: put(WriteTask)
  DB-->>Spot: void (Non-blocking)

  Thread->>Q: get()
  Thread->>SQLite: INSERT / UPDATE

技術選定

技術領域 選定 理由
デフォルトメタデータDB SQLite ゼロ設定、組み込み可能、ローカルキャッシュとして十分な性能
バックグラウンド書き込み キュー + 専用スレッド SQLiteの排他制御によるメインスレッドのブロックを防ぐため
抽象化 ProtocolベースのDI TaskDBCoreを満たせばPostgreSQL等に容易に差し替え可能

非機能要件方針

2.4 Blobストレージ構成 ARCH004
STORE

コンポーネント構成

graph TD
  A[core.Spot] -->|判定| B[StoragePolicyProtocol];
  A -->|保存/読込| C[BlobStorageBase];
  D[LocalStorage] --> C;
  E[S3Storage] --> C;
  A -->|メタデータ| F[TaskDBBase];

コンポーネント責務

コンポーネント 責務 インターフェース
StoragePolicyProtocol データのサイズ等に基づき、DB保存かBlob保存かを判定する should_save_as_blob()
BlobStorageBase 大規模データの外部ストレージ保存を抽象化する save(), load(), delete()
LocalStorage ローカルファイルシステムへのアトミックな保存と検証 base_dir, os.replace
S3Storage AWS S3互換オブジェクトストレージへの保存 s3:// URI, boto3

データフロー

sequenceDiagram
  participant Spot as core.Spot
  participant Policy as StoragePolicy
  participant DB as TaskDB
  participant Blob as BlobStorage

  Spot->>Policy: should_save_as_blob(data)
  alt Policy: True (Blob保存)
    Spot->>Blob: save(key, data)
    Blob-->>Spot: location (path/URI)
    Spot->>DB: insert(metadata, storage_type=FILE, blob_key=location)
  else Policy: False (Inline保存)
    Spot->>DB: insert(metadata, storage_type=DIRECT_BLOB, result_data=data)
  end

技術選定

技術領域 選定 理由
保存判定 閾値ベース (Threshold) DBの肥大化を防ぎつつ、小容量データのI/Oを最適化
アトミック性 一時ファイル + Rename 保存中のクラッシュや並行書き込みによる破損を完全に防止
クラウド対応 Boto3 (S3) 業界標準のプロトコルによる高い互換性とスケーラビリティ

非機能要件方針

2.5 シリアライゼーション構成 ARCH005
SERIAL

コンポーネント構成

graph TD
  A[MsgpackSerializer] -->|Registry| B[TypeRegistryProtocol]
  A -->|LRU Cache| C[Thread-Local Storage]
  A -->|Core| D[msgpack-python]

コンポーネント責務

コンポーネント 責務 インターフェース
MsgpackSerializer MessagePackベースの高速・コンパクトなバイナリ変換 dumps(), loads()
TypeRegistryProtocol ユーザー定義型(ExtType)のエンコーダ・デコーダ登録 register()
Thread-Local Cache free-threading環境でのロック競合を回避するMRO解決キャッシュ OrderedDict (LRU)

データフロー

sequenceDiagram
  participant User as ユーザーコード
  participant Ser as MsgpackSerializer
  participant Reg as TypeRegistry
  participant MP as msgpack

  User->>Ser: dumps(obj)
  Ser->>Ser: 型チェック & MRO解決
  opt カスタム型
    Ser->>Reg: エンコーダ取得
    Reg-->>Ser: encoder_fn
    Ser->>MP: ExtType(code, encoder_fn(obj))
  end
  MP-->>Ser: binary
  Ser-->>User: bytes

技術選定

技術領域 選定 理由
フォーマット MessagePack JSONより高速・コンパクト。バイナリをネイティブにサポート
スレッド安全 Copy-on-Write (CoW) 読み取りパスからロックを排除し、マルチスレッド性能を最大化
型拡張 ExtType (0-127) カスタム型を1バイトのオーバーヘッドで表現可能

非機能要件方針

2.6 バックグラウンドIO構成 ARCH006
BGIO

コンポーネント構成

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 強制終了時も、可能な限り保留中の書き込みを完了させる

非機能要件方針

2.7 ライフサイクル管理構成 ARCH007
LIFE

コンポーネント構成

graph TD
  A[core.Spot] -->|マッチング要求| B[LifecyclePolicy]
  B -->|ルールリスト| C[Rule]
  C -->|パターン照合| D[fnmatch]
  B -->|パース| E[Retention]

コンポーネント責務

コンポーネント 責務 インターフェース
LifecyclePolicy 複数のルールを管理し、関数名に最適な有効期限を決定する resolve_with_fallback()
Rule 1つのパターンとそれに対応する保持期間のペア match(func_name)
Retention 多様な時間表現(文字列/数値)を秒数に正規化する parse_retention()

データフロー

sequenceDiagram
  participant Spot as core.Spot
  participant Policy as LifecyclePolicy
  participant DB as TaskDB

  Spot->>Policy: resolve_with_fallback("module.func")
  Policy->>Policy: ルール順次照合 (fnmatch)
  Policy-->>Spot: expires_at (timestamp)
  Spot->>DB: save(..., expires_at)
  Note over DB: get() 時に現在時刻と比較判定

技術選定

技術領域 選定 理由
パターンマッチ fnmatch (Glob) 正規表現よりも直感的で、開発者にとって馴染みのある指定方法
保持期間指定 文字列パーサー ("30d"等) 設定ファイルやデコレータでの可読性を向上
判定タイミング 遅延判定 (Lazy) 書き込み・読み込み時のオーバーヘッドを最小化

非機能要件方針

2.8 レート制限構成 ARCH008
LIMIT

コンポーネント構成

graph TD
  A["@spot.consume"] -->|宣言的適用| B[LimiterProtocol]
  C[core.Spot] -->|DI| B
  D[TokenBucket] --> B
  D -->|アルゴリズム| E[GCRA]
  D -->|時刻同期| F[monotonic clock]

コンポーネント責務

コンポーネント 責務 インターフェース
TokenBucket GCRAアルゴリズムによるスムースなトラフィック制御 consume(), consume_async()
@spot.consume 関数実行前のトークン消費を透過的に行うデコレータ cost (int or callable)
LimiterProtocol レートリミッターの差し替えを可能にするインターフェース LimiterProtocol

データフロー

sequenceDiagram
  participant User as ユーザーコード
  participant Dec as @spot.consume
  participant Bucket as TokenBucket

  User->>Dec: call func()
  Dec->>Bucket: consume(cost)
  Bucket->>Bucket: 次回実行許可時刻の計算 (GCRA)
  alt 許可
      Bucket-->>Dec: OK
      Dec->>User: execute func()
  else 拒否 (Over rate)
      Bucket-->>Dec: raise ValueError/RateLimitError
  end

技術選定

技術領域 選定 理由
アルゴリズム GCRA (Generic Cell Rate Algorithm) 固定ウィンドウと違い「スムースな」流量制御が可能
待機制御 asyncio.sleep / time.sleep 同期・非同期の両方のコンテキストで正確なスロットリングを実現
コスト計算 ダイナミックコスト 引数に応じて消費トークン数を動的に変更可能

非機能要件方針

2.9 フック機能構成 ARCH009
HOOK

コンポーネント構成

graph TD
  A[core.Spot] -->|イベント通知| B[HookBase]
  C[ThreadSafeHookBase] --> B
  B -->|コンテキスト| D[Context Objects]
  E[PreExecuteContext] --> D
  F[CacheHitContext] --> D
  G[CacheMissContext] --> D

コンポーネント責務

コンポーネント 責務 インターフェース
HookBase 実行ライフサイクルの各段階で呼び出される抽象基底クラス pre_execute, on_cache_hit, on_cache_miss
ThreadSafeHookBase RLockを用いて各フックメソッドの実行を直列化する __init_subclass__ による自動ラップ
Context Objects フックに渡される実行時の引数、結果、例外、メタデータのコンテナ ctx.args, ctx.result

データフロー

sequenceDiagram
  participant Spot as core.Spot
  participant Hook as HookBase
  participant Fn as Target Function

  Spot->>Hook: pre_execute(ctx)
  alt キャッシュヒット
      Spot->>Hook: on_cache_hit(ctx)
  else キャッシュミス
      Spot->>Fn: execute()
      Fn-->>Spot: result
      Spot->>Hook: on_cache_miss(ctx)
  end

技術選定

技術領域 選定 理由
パターン オブザーバー コアロジックを汚さずに、ログ出力や統計取得等の横断的関心を分離
スレッド安全 自動装飾 (Decorator) ユーザーが意識せずに、サブクラス化するだけで安全なフックを実装可能
実行制御 準同期実行 フック内の例外をキャッチしログ出力に留めることで、主処理を保護

非機能要件方針

2.10 メンテナンス構成 ARCH010
MAINT

コンポーネント構成

graph TD
  A[MaintenanceService] -->|操作集約| B[TaskDBBase]
  A -->|操作集約| C[BlobStorageBase]
  A -->|自動実行| D[Probabilistic Eviction]

コンポーネント責務

コンポーネント 責務 インターフェース
MaintenanceService DBとストレージを跨ぐクリーンアップ操作のファサード clean_garbage(), prune()
TaskDBBase 参照カウントや期限切れレコードの提供 get_blob_refs(), delete_expired()
BlobStorageBase 物理ファイルの列挙と削除 list_keys(), delete()

データフロー (GCプロセス)

sequenceDiagram
  participant Service as MaintenanceService
  participant DB as TaskDB
  participant Storage as BlobStorage

  Service->>DB: get_blob_refs()
  DB-->>Service: 有効な参照リスト (Whitelist)
  Service->>Storage: list_keys()
  Storage-->>Service: 全ファイルリスト
  Service->>Service: 差分抽出 (孤立ファイルの特定)
  Service->>Storage: delete(orphan_keys)
  Note over Service: 猶予期間 (grace_period) を考慮して削除

技術選定

技術領域 選定 理由
削除ポリシー ホワイトリスト方式 DBに記録がないBlobを削除対象とすることで、データの整合性を担保
競合防止 更新時刻確認 (mtime) 保存中のファイルを誤って消さないよう、一定時間経過した孤立のみ削除
実行頻度 確率的オートエビクション 書き込み時に低確率でGCをキックし、手動メンテなしでの健康度を維持

非機能要件方針

2.11 CLI構成 ARCH011
CLI

コンポーネント構成

graph TD
  A[beautyspot CLI] -->|Command| B[Typer App]
  B -->|Business Logic| C[MaintenanceService]
  B -->|Output| D[Rich Console]
  B -->|Dashboard| E[Dashboard App]

コンポーネント責務

コンポーネント 責務 インターフェース
Typer App コマンドライン引数のパースとサブコマンドのディスパッチ main(), gc(), list()
MaintenanceService 実際のキャッシュ管理ロジックの実行 clean_garbage(), clear()
Rich Console ターミナルでの視覚的に分かりやすいテーブルやパネルの描画 Console, Table
Dashboard App キャッシュ状態をインタラクティブに閲覧するためのTUI dashboard.py

データフロー

sequenceDiagram
  participant User as ユーザー
  participant CLI as CLI App
  participant Service as MaintenanceService
  participant Output as Rich Console

  User->>CLI: beautyspot gc --name my-project
  CLI->>Service: clean_garbage()
  Service-->>CLI: result (removed count, etc.)
  CLI->>Output: print summary table
  Output-->>User: formatted output

技術選定

技術領域 選定 理由
フレームワーク Typer 型ヒントに基づいた堅牢なコマンドラインインターフェースを迅速に構築
出力装飾 Rich プログレスバーやステータス表示により、長時間処理の進捗を可視化
連携 Factory DI カレントディレクトリや設定から自動的に Spot インスタンスを組み立て

非機能要件方針

2.12 依存注入構成 ARCH012
DI

コンポーネント構成

graph TD
  A[bs.Spot Factory] -->|構築| B[core.Spot]
  A -->|DI| C[TaskDBBase]
  A -->|DI| D[BlobStorageBase]
  A -->|DI| E[SerializerProtocol]
  A -->|DI| F[StoragePolicyProtocol]
  A -->|DI| G[LimiterProtocol]

コンポーネント責務

コンポーネント 責務 インターフェース
bs.Spot (Factory) 環境(名前、パス等)に応じたデフォルトコンポーネントの選定と組み立て bs.Spot()
core.Spot 注入された依存関係を使用して、キャッシュロジックをオーケストレーションする mark(), cached_run()
Protocols / ABCs 各コンポーネントが満たすべき契約(インターフェース)の定義 SerializerProtocol

技術選定

技術領域 選定 理由
パターン Constructor Injection 依存関係を明示的に渡し、テスト時のMock差し替えを容易にする
インターフェース Protocol (Duck Typing) 厳密な継承を強制せず、構造的部分型によりサードパーティ実装を受け入れ
デフォルト設定 規約より構成 (CoC) .beautyspot/ ディレクトリを基点とした標準パスを自動生成

非機能要件方針

2.13 Thundering Herd対策構成 ARCH013
HERD

コンポーネント構成

graph TD
  A[core.Spot] -->|管理要求| B[CacheManager]
  B -->|In-flight追跡| C[_inflight Dict]
  C -->|同期待機| D[threading.Event]
  C -->|非同期待機| E[asyncio.Future]

コンポーネント責務

コンポーネント責務インターフェースCacheManagerキャッシュキーごとの実行状態管理と実行のシリアライズget_or_create_inflight()_inflight実行中のタスクを保持し、後続リクエストをイベントで待機させるExecutionStatecore.Spot待機結果の受け取りとエラーの伝播cached_run()

データフロー

sequenceDiagram
  participant T1 as Thread 1 (First)
  participant T2 as Thread 2 (Second)
  participant CM as CacheManager
  participant Fn as Target Function

  T1->>CM: get_or_create(key)
  Note over CM: 新規作成 (Executor)
  T2->>CM: get_or_create(key)
  Note over CM: 既存あり (Waiter)

  T1->>Fn: execute()
  Fn-->>T1: result
  T1->>CM: set_result(key, result)
  Note over CM: Eventをセット
  CM-->>T2: notify result
  T1-->>T1: return result
  T2-->>T2: return result

技術選定

技術領域選定理由待機機構Event / FutureOS/ランタイムレベルの待機を使用し、CPU負荷(ビジーループ)を回避スコープキャッシュキー単位異なる関数の実行は妨げず、同一入力のみを直列化安全策タイムアウト & リトライ実行者がハングした場合に、待機側がデッドロックしないよう保護

非機能要件方針

SPEC Specification

1.0 序文 SPEC025

本ドキュメントは、『beautyspot』の各機能における詳細な仕様、インターフェース、およびアルゴリズムを定義する。

2.0 詳細仕様 SPEC026

各コンポーネントが提供するAPI、内部状態遷移、および例外ハンドリングの詳細を定義する。

2.1 mark デコレータ SPEC001
CACHE

インターフェース

@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): ...

振る舞い

基本フロー

  1. デコレータが対象関数をラップする
  2. 呼び出し時にキャッシュキーを生成する
  3. DBからキャッシュを検索する
  4. ヒット時: デシリアライズして結果を返す(関数は実行しない)
  5. ミス時: 関数を実行し、結果をシリアライズしてDB/Blobに保存する

sync/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 関数単位のフックリスト(実行前後やキャッシュヒット時に発火)

エラーハンドリング

エッジケース

2.2 cached_run コンテキストマネージャ SPEC002
CACHE

インターフェース

@contextmanager
def cached_run(
    self,
    *funcs: Any,
    **kwargs: Any
) -> Iterator[Any]: ...

振る舞い

基本フロー

  1. Spot インスタンスからコンテキストマネージャとして呼び出される。
  2. 引数 funcs に渡された関数が0個の場合はエラーを送出する。
  3. 各関数に対して、Spot.mark(**kwargs) で生成したキャッシュデコレータを適用し、ラップされた関数を生成する。
  4. ラップされた関数を yield してコンテキストブロック内のユーザーコードに提供する。
  5. ユーザーコード内での実行は、通常の @spot.mark 適用済み関数と同様にキャッシュ機構を経由する。
  6. コンテキストブロック終了時のクリーンアップ処理は特に行わない(一時的なラッパーはスコープを抜けると破棄される)。

パラメータ詳細

パラメータ 必須 説明
*funcs はい キャッシュ機能を適用したい対象の関数。外部ライブラリの関数なども指定可能。複数指定可能。
**kwargs いいえ Spot.mark デコレータに渡すオプション引数(save_blob, keygen, version, content_type, serializer, retention, save_sync, hooks 等)。

戻り値(Yields)の仕様

エラーハンドリング

エッジケース

2.3 同期・非同期関数サポート SPEC003
CACHE

概要

@spot.mark() デコレータは同期関数と非同期(async def)関数の両方をサポートし、関数定義に応じて適切な実行・保存・キャッシュ検索フローを透過的に切り替える。

振る舞い

sync/async の自動判定

実行フロー

エラーハンドリング

エッジケース

2.4 SHA-256キャッシュキー SPEC004
KEY

インターフェース

class KeyGen:
    @staticmethod
    def _default(args: tuple, kwargs: dict) -> str: ...

    @staticmethod
    def hash_items(items: list) -> str: ...

振る舞い

基本フロー (_default)

  1. 正規化: argskwargscanonicalize() により再帰的に正規化する
  2. パック: 正規化された構造 [args, kwargs] を MessagePack でバイナリにシリアライズする
  3. ハッシュ: バイナリデータに対して SHA-256 ハッシュを計算し、16進数文字列を返す

リストハッシュ (hash_items)

パラメータ詳細

パラメータ 説明
args tuple 関数の位置引数
kwargs dict 関数のキーワード引数
items list バインド済み引数の正規化値リスト

エラーハンドリング

エッジケース

2.5 引数の再帰的正規化 SPEC005
KEY

インターフェース

@singledispatch
def canonicalize(obj: Any) -> Any: ...

振る舞い

singledispatch を使用し、型に応じた最適な正規化形式へ再帰的に変換する。

主要な型別の処理

パラメータ詳細

オブジェクト型 正規化後の形式 備考
numpy.ndarray ("__numpy__", shape, dtype, bytes) 高速かつ正確なハッシュを保証
OrderedDict ("__ordered_dict__", items) 挿入順序を保持したまま正規化
Pydantic Model ("__pydantic_v2__", schema) スキーマ情報をベースに判定

エラーハンドリング

エッジケース

2.6 KeyGenPolicy 戦略 SPEC006
KEY

インターフェース

class Strategy(Enum):
    DEFAULT = auto()      # 再帰的正規化
    IGNORE = auto()       # キー計算から除外
    FILE_CONTENT = auto() # ファイルの内容をハッシュ
    PATH_STAT = auto()    # パス+サイズ+更新時刻をハッシュ

class KeyGenPolicy:
    def bind(self, func: Callable) -> Callable[..., str]: ...

振る舞い

  1. バインド: bind(func) 時に inspect.signature を解析し、引数名とデフォルト値を把握する
  2. 適用: 呼び出し時に引数を実値にバインドし、デフォルト値を補填する
  3. 戦略実行: 引数名ごとに指定された Strategy を適用し、正規化された値のリストを作成する
  4. 集約: 全ての値を KeyGen.hash_items() で一つのハッシュにまとめる

パラメータ詳細

戦略 内容 用途
IGNORE キャッシュキーに含めない verboselogger 等、結果に影響しない引数
FILE_CONTENT ファイルを全読込してハッシュ化 入力ファイルの厳密な変更検知
PATH_STAT os.stat 情報を使用 大容量ファイルの高速な変更検知

エラーハンドリング

エッジケース

2.7 SQLiteTaskDB SPEC007
DB

インターフェース

class SQLiteTaskDB(TaskDBBase):
    def __init__(self, db_path: str | Path, timeout: float = 30.0): ...
    def save(self, task: TaskRecord) -> None: ...
    def get(self, input_key: str) -> TaskRecord | None: ...

振る舞い

ライタースレッド方式

読み取り並行性

スキーマ管理

パラメータ詳細

設定 デフォルト 説明
journal_mode WAL 書き込みと読み取りの並行性を高める設定
synchronous NORMAL パフォーマンスと耐久性のバランス設定
timeout 30.0 データベースロック待機時間の閾値

エラーハンドリング

エッジケース

2.8 DB プロトコル階層 SPEC008
DB

インターフェース

@runtime_checkable
class TaskDBCore(Protocol):
    def get(self, input_key: str) -> TaskRecord | None: ...
    def save(self, record: TaskRecord) -> None: ...

class TaskDBBase(ABC):
    # TaskDBCore を満たしつつ、メンテナンスのデフォルト実装を提供

振る舞い

階層構造

  1. TaskDBCore: キャッシュの実行に必要な最小限のメソッド定義
  2. Maintenable: GCや統計取得に必要な管理用メソッドの定義
  3. TaskDBBase: 共通のバリデーションや例外処理を実装する基底クラス

パラメータ詳細 (主要メソッド)

メソッド 役割
get_blob_refs() 全レコードの blob_key を列挙する(GC用)
delete_expired() expires_at を経過したレコードを一括削除する
get_history() 実行履歴(統計)を取得する

エラーハンドリング

エッジケース

2.9 LocalStorage SPEC009
STORE

インターフェース

class LocalStorage(BlobStorageMaintenable):
    def __init__(self, base_dir: str | Path): ...
    def save(self, key: str, data: bytes) -> str: ...
    def load(self, location: str) -> bytes: ...

振る舞い

アトミック書き込み (save)

  1. 指定ディレクトリ内に tempfile.mkstemp で一時ファイルを作成
  2. データを書き込み、flush() および os.fsync() でディスク到達を保証
  3. os.replace で最終的なパスへリネーム(アトミックな置換)

パストラバーサル対策 (load / delete)

パラメータ詳細

パラメータ 説明 制限
key 保存ファイル名のベース ../ を含む場合は ValidationError
location save が返した識別子 相対パス形式を推奨

エラーハンドリング

エッジケース

2.10 S3Storage SPEC010
STORE

インターフェース

class S3Storage(BlobStorageMaintenable):
    def __init__(self, s3_uri: str, s3_opts: dict | None = None): ...

振る舞い

URIベースの管理

大容量対応 (save)

高速なメタデータ取得 (get_mtime)

パラメータ詳細

設定 内容
s3_uri s3://my-bucket/my-prefix 形式
s3_opts boto3.client('s3', **s3_opts) に渡される設定

エラーハンドリング

エッジケース

2.11 ストレージポリシー SPEC011
STORE

インターフェース

class StoragePolicyProtocol(Protocol):
    def should_save_as_blob(self, data: bytes) -> bool: ...

振る舞い

判定ロジック

パラメータ詳細

ポリシー型 パラメータ デフォルト 説明
Threshold threshold なし バイト単位の閾値。10MB等を推奨
Warning warning_threshold なし 警告を出すサイズ閾値

エラーハンドリング

エッジケース

2.12 MsgpackSerializer SPEC012
SERIAL

インターフェース

class MsgpackSerializer(SerializerProtocol, TypeRegistryProtocol):
    def dumps(self, obj: Any) -> bytes: ...
    def loads(self, data: bytes) -> Any: ...

振る舞い

シリアライズ (dumps)

  1. オブジェクトの型を type(obj) で取得
  2. 登録済み型またはそのサブクラス(MRO順)を探索
  3. ヒットした場合、カスタムエンコーダを実行し ExtType としてパック
  4. ヒットしない場合、MessagePack のデフォルト処理(または SerializationError

デシリアライズ (loads)

  1. MessagePack ストリームをパース
  2. ExtType を発見した場合、コードに対応するカスタムデコーダを呼び出す

パラメータ詳細

設定 デフォルト 説明
max_cache_size 1024 スレッドごとの型解決LRUキャッシュの最大サイズ

エラーハンドリング

エッジケース

2.13 カスタム型登録 SPEC013
SERIAL

インターフェース

def register(
    code: int,
    encoder: Callable[[Any], Any],
    decoder: Callable[[Any], Any],
) -> Callable: ... # デコレータ形式

def register_type(
    type_class: Type,
    code: int,
    encoder: Callable[[Any], Any],
    decoder: Callable[[Any], Any],
) -> None: ...

振る舞い

  1. 重複チェック: 同一コードまたは同一クラスが既に登録されていないか確認
  2. CoW更新: 現在のレジストリ辞書をコピーし、新しいエントリを追加した後に参照を差し替える
  3. 世代更新: _registry_generation をインクリメントし、全スレッドのLRUキャッシュを無効化対象とする

パラメータ詳細

パラメータ 制限 説明
code 0127 MessagePack ExtType のユーザー定義領域コード
encoder 引数1、戻り値シリアライズ可能 オブジェクトをプリミティブ構造へ変換する関数
decoder 引数1、戻り値オブジェクト プリミティブ構造からオブジェクトを復元する関数

エラーハンドリング

エッジケース

2.14 BackgroundLoop SPEC014
BGIO

インターフェース

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 設定のスレッド

エラーハンドリング

エッジケース

2.15 save_sync / flush / drain SPEC015
BGIO

インターフェース

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

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

振る舞い

同期保存 (save_sync=True)

非同期保存 (save_sync=False)

フラッシュ (flush)

パラメータ詳細

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

エラーハンドリング

エッジケース

2.16 LifecyclePolicy SPEC016
LIFE

インターフェース

class LifecyclePolicy:
    def add_rule(self, pattern: str, retention: Any) -> None: ...
    def resolve_with_fallback(self, func_name: str) -> float | None: ...

振る舞い

  1. 順次照合: 登録された順にルールを走査する

  2. パターンマッチ: fnmatch.fnmatch を使用し、関数名がパターンに合致するか判定

  3. 二段階解決:

    1. 最初に「完全修飾名 (module.qualname)」で照合

    2. マッチしなければ「短縮名 (qualname)」で照合

  4. 結果返却: 最初にマッチしたルールの保持期間を秒数に変換して返す

パラメータ詳細

パラメータ例説明pattern"my_mod.*", "*_test"Glob形式のパターンretention"30d", 3600保持期間の定義

デフォルト保持期間

エラーハンドリング

エッジケース

2.17 Retention 仕様 SPEC017
LIFE

インターフェース

def parse_retention(val: Any) -> float | None: ...

振る舞い

文字列パース

数値・オブジェクト

パラメータ詳細 (単位)

単位 倍率 (秒) 備考
d 86400 日数
h 3600 時間
m 60
s 1

エラーハンドリング

エッジケース

2.18 TokenBucket (GCRA) SPEC018
LIMIT

インターフェース

class TokenBucket(LimiterProtocol):
    def consume(self, cost: float = 1.0) -> None: ...
    async def consume_async(self, cost: float = 1.0) -> None: ...

振る舞い

GCRAアルゴリズム

同期 vs 非同期

パラメータ詳細

設定 デフォルト 説明
tokens_per_minute 必須 1分間に許可する平均リクエスト数(コスト合計)
max_burst 1.0 許容されるバースト数(トークン単位)

エラーハンドリング

エッジケース

2.19 HookBase / ThreadSafeHookBase SPEC019
HOOK

インターフェース

class HookBase:
    def pre_execute(self, ctx: PreExecuteContext) -> None: ...
    def on_cache_hit(self, ctx: CacheHitContext) -> None: ...
    def on_cache_miss(self, ctx: CacheMissContext) -> None: ...

振る舞い

  1. フック呼出: キャッシュエンジンの各ステート(実行前、ヒット、ミス)で登録されたフックを順番に実行する
  2. スレッド安全化: ThreadSafeHookBase を継承した場合、メタクラスが自動的に各メソッドを with self._lock: で包む

パラメータ詳細 (Context)

コンテキスト 保持データ
PreExecuteContext func, args, kwargs, cache_key
CacheHitContext cache_key, result, metadata
CacheMissContext cache_key, result, execution_time

エラーハンドリング

エッジケース

2.20 MaintenanceService SPEC020
MAINT

インターフェース

class MaintenanceService:
    def clean_garbage(self, grace_period: int = 3600) -> GarbageStats: ...
    def prune(self, days: int, func_name: str | None = None) -> int: ...
    def clear(self, func_name: str | None = None) -> int: ...

振る舞い

ガベージコレクション (clean_garbage)

  1. DB Flush: 保存中のタスクをDBへ反映させる
  2. 期限切れ削除: delete_expired() でDBから古いタスクを除去
  3. 孤立Blobスキャン: DB内の全 blob_key とストレージ内の全ファイルを比較
  4. 物理削除: 参照のないBlobのうち、作成から grace_period 以上経過したものを削除
  5. 構造清掃: ストレージ内の空ディレクトリを削除

パラメータ詳細

パラメータ デフォルト 説明
grace_period 3600 孤立Blobを削除するまでの猶予期間(秒)。並行実行中の保存を保護する
days なし prune で削除対象とする経過日数

エラーハンドリング

エッジケース

2.21 CLIコマンド SPEC021
CLI

インターフェース

beautyspot list [--db DB_PATH]
beautyspot gc [--name PROJECT_NAME] [--force]
beautyspot stats
beautyspot ui

振る舞い

  1. コンテキスト解決: --db 指定がない場合、デフォルトの .beautyspot/ ディレクトリを探索する
  2. コマンド実行: MaintenanceService または Spot インスタンスを生成し、対応するメソッドを呼び出す
  3. フォーマット出力: Rich ライブラリを使用して、結果をテーブルやプログレスバーで表示する

サブコマンド詳細

コマンド 説明
list 保存されているキャッシュキーと関数名の一覧を表示
gc 期限切れタスクと孤立Blobのクリーンアップを実行
stats キャッシュヒット率やストレージ使用量の統計を表示
ui ターミナルベースのインタラクティブダッシュボードを起動

エラーハンドリング

エッジケース

2.22 ファクトリ関数と Protocol SPEC022
DI

インターフェース

def Spot(
    name: str = "default",
    storage_path: str | Path | None = None,
    serializer: SerializerProtocol | None = None,
    limiter: LimiterProtocol | None = None,
    # ...その他の依存
) -> core.Spot: ...

振る舞い

  1. パス解決: storage_path が未指定の場合、.beautyspot/ 以下のプロジェクト名ディレクトリを使用する
  2. DB初期化: SQLiteTaskDB を生成し、マイグレーションを実行
  3. ストレージ構成: URI(s3://等)を判定し、適切な BlobStorage 実装を生成
  4. インスタンス化: 全てのコンポーネントを core.Spot のコンストラクタに注入して返す

パラメータ詳細

パラメータ デフォルト値の導出
db .beautyspot/{name}.db
blobs .beautyspot/blobs/{name}/
serializer MsgpackSerializer()
policy WarningOnlyPolicy (閾値10MB)

エラーハンドリング

エッジケース

2.23 Thundering Herd Protection SPEC023
HERD

インターフェース

class CacheManager:
    def get_or_create_inflight(
        self, key: str, is_async: bool
    ) -> ExecutionState: ...

振る舞い

同時実行制御

  1. キャッシュキーをキーとした _inflight 辞書をチェックする
  2. 存在しない場合: ExecutionState を新規作成し、自分が「実行者 (Executor)」となる
  3. 存在する場合: 既存の ExecutionState を返し、自分は「待機者 (Waiter)」となる
  4. 完了通知: 実行者が処理を終えたら、ExecutionState の Event (または Future) をセットし、結果を共有する

強参照によるインフライト状態の保持

目的

実行中のタスク状態(イベント・結果・Future)が GC によって消失しないよう、 _inflight 辞書を通じて強参照で保持する。WeakRef は使用しない。

データ構造

_inflight 辞書はキャッシュキーをキーとし、以下の3要素タプルを値として保持する:

_inflight: dict[str, tuple[threading.Event, list[asyncio.Future], list]]
要素 役割
event threading.Event 同期待機者への完了通知シグナル
futures list[asyncio.Future] 非同期待機者への結果配信チャネル
result_box list 結果の共有ボックス。[(success: bool, value: Any)] 形式

ライフサイクル

  1. 作成: wait_herd_sync / wait_herd_async_inflight にキーが存在しない場合、 _inflight_lock を保持した状態で新規タプルを挿入する
  2. 保持: 実行者が処理を完了するまで、_inflight 辞書がタプルへの唯一の管理参照を保持する。 待機者はロック取得時にタプル要素への参照を取得するが、_inflight エントリが正規の所有者である
  3. 解放: notify_and_cleanup_inflight で以下の順序で解放する: a. _inflight_lock を取得し、エントリの同一性を event の identity (is) で確認する b. 辞書からエントリを削除する(del _inflight[cache_key]) c. ロックを解放した後、event.set() で同期待機者に通知する d. futures リスト内の各 asyncio.Future に結果またはエラーを伝播する

不変条件

GC安全性

_inflight 辞書は CacheManager インスタンスの属性であり、CacheManager が 生存している限り、全てのインフライトエントリは GC の対象にならない。 CacheManager 自体は Spot インスタンスが所有するため、with spot: ブロック内での 安全性が保証される。

パラメータ詳細

内部定数 説明
HERD_TIMEOUT 300.0 待機者が実行者の完了を待つ最大秒数
HERD_MAX_RETRIES 3 実行者が失敗またはタイムアウトした際のリトライ回数

エラーハンドリング

エッジケース

2.24 Blobストレージ抽象インターフェース SPEC024
STORE

インターフェース

class BlobStorageBase(ABC):
    @abstractmethod
    def save(self, key: str, data: ReadableBuffer) -> str: ...
    @abstractmethod
    def load(self, location: str) -> bytes: ...
    @abstractmethod
    def delete(self, location: str) -> None: ...
    @abstractmethod
    def list_keys(self) -> Iterator[str]: ...
    @abstractmethod
    def get_mtime(self, location: str) -> float: ...

振る舞い

パラメータ詳細

パラメータ 説明
key str キャッシュキー(通常はハッシュ値)
data bytes / memoryview シリアライズ済みバイナリ
location str 実体へのポインタ(ファイルパス、S3 URI等)

エラーハンドリング

エッジケース

TST Specification

1.0 序文 TST025

本ドキュメントは、『beautyspot』のテスト計画および検証内容を定義する。

2.0 詳細仕様 TST026

各機能が仕様通りに動作することを確認するための検証観点およびテストシナリオを定義する。

2.1 mark デコレータテスト TST001
CACHE

目的

@spot.mark() はユーザーが最も頻繁に使う公開APIであり、キャッシュの正確性・透過性・拡張性の基盤となる。デコレーションによって元の関数の挙動やメタデータが損なわれると、ユーザーコードのデバッグや型チェックに支障をきたすため、ラッパーの透過性を厳密に検証する。

検証観点

2.2 cached_run テスト TST002
CACHE

目的

cached_run はデコレータを直接付与できない外部ライブラリ関数にキャッシュを適用する唯一の手段であり、このAPIの不具合はサードパーティ統合シナリオ全体に影響する。戻り値の型が関数の数で変わるスマートリターン仕様は、誤実装すると型安全性を損なうため重点的に検証する。

検証観点

2.3 同期・非同期サポートテスト TST003
CACHE

目的

beautyspot は同期・非同期の両方の関数を透過的にキャッシュするが、asyncio 固有のイベントループ制約(スレッドをまたぐコルーチン実行、例外伝播のタイミング差異)により、同期版とは異なる不具合が発生しやすい。非同期パスの堅牢性を独立して検証する。

検証観点

2.4 SHA-256 キャッシュキーテスト TST004
KEY

目的

キャッシュキーはキャッシュの同一性判定の基盤であり、ハッシュが不安定だとキャッシュミスの頻発(性能劣化)や誤ヒット(データ不整合)を引き起こす。Python バージョンやプラットフォームに依存しないキー生成の安定性を保証する。

検証観点

2.5 再帰的正規化テスト TST005
KEY

目的

canonicalize() は引数の型ごとに正規化ルールを適用し、論理的に同値な入力が同じキャッシュキーを生成することを保証する。正規化の不備は「同じ入力なのにキャッシュミス」または「異なる入力なのに誤ヒット」という致命的なバグに直結する。

検証観点

2.6 KeyGenPolicy テスト TST006
KEY

目的

KeyGenPolicy は引数ごとのキー生成戦略をカスタマイズする機構であり、ログレベルやデバッグフラグのような非決定的引数をキーから除外したり、ファイルパスの代わりにファイル内容でキーを生成するユースケースを支える。戦略の誤適用はキャッシュの正確性を根本から損なう。

検証観点

2.7 SQLiteTaskDB テスト TST007
DB

目的

SQLiteTaskDB はキャッシュメタデータの永続化を担う中核コンポーネントであり、DB の不整合はキャッシュの消失や重複実行を引き起こす。WALモード・専用ライタースレッドという独自アーキテクチャの信頼性を保証する。

検証観点

2.8 DB プロトコル階層テスト TST008
DB

目的

TaskDBCore / TaskDBBase のプロトコル階層は、サードパーティによるカスタムDB実装(Redis、PostgreSQL 等)の差し替えを可能にする拡張ポイントである。インターフェース契約が不明確だと、カスタム実装が実行時に予期しないエラーを起こす。

検証観点

2.9 LocalStorage テスト TST009
STORE

目的

LocalStorage はBlobデータのファイルシステム永続化を担い、並行書き込み時のデータ破損やパストラバーサルによるセキュリティ脆弱性が発生しうる。アトミック書き込みとセキュリティバリデーションの正確性を保証する。

検証観点

2.10 S3Storage テスト TST010
STORE

目的

S3Storage はクラウド環境での大規模Blob保存を担い、ネットワーク障害・認証エラー・バケット不在などオンプレミスにはない障害モードが存在する。boto3 オプショナル依存のガード処理も含め、クラウドストレージ統合の信頼性を検証する。

検証観点

2.11 ストレージポリシーテスト TST011
STORE

目的

ストレージポリシーは「データをDB直接保存するか、Blobストレージに分離するか」を決定する戦略レイヤーである。閾値判定の誤りはDBの肥大化(性能劣化)や不要なBlob分離(オーバーヘッド増加)を招く。3種のポリシー実装がそれぞれの契約を正しく満たすことを検証する。

検証観点

2.12 MsgpackSerializer テスト TST012
SERIAL

目的

MsgpackSerializer はキャッシュデータの永続化形式を決定する。シリアライズの不具合はデータ消失(デシリアライズ不能)やサイレントなデータ劣化(精度低下等)に直結する。スレッドセーフ性の欠如はマルチスレッド環境での競合状態を引き起こす。

検証観点

2.13 カスタム型登録テスト TST013
SERIAL

目的

カスタム型登録はユーザー定義クラスのキャッシュを可能にする拡張ポイントである。登録の不備は SerializationError によるキャッシュ保存失敗を引き起こし、ユーザーの既存コードとの統合を阻害する。デコレータ方式と命令方式の両方の登録パスを検証する。

検証観点

2.14 BackgroundLoop テスト TST014
BGIO

目的

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

検証観点

2.15 save_sync / flush / drain テスト TST015
BGIO

目的

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

検証観点

2.16 LifecyclePolicy テスト TST016
LIFE

目的

LifecyclePolicy はキャッシュデータの保持期間を関数名パターンで制御する。パターンマッチングの不備は、重要なキャッシュの意図しない削除(データロス)や不要なキャッシュの蓄積(ディスク圧迫)を引き起こす。

検証観点

2.17 Retention テスト TST017
LIFE

目的

Retention はキャッシュの有効期間をユーザーフレンドリーな形式で指定する値オブジェクトである。パースの不備は意図しない保持期間(例: "7d" を 7秒と誤解釈)を招き、重要なキャッシュの早期削除やディスクの際限ない肥大化に繋がる。

検証観点

2.18 TokenBucket テスト TST018
LIMIT

目的

TokenBucket はAPI呼び出しやリソースアクセスのレート制限を実現する。レートリミッターの不具合はAPIの過負荷(制限が甘い場合)やスループットの不必要な低下(制限が厳しすぎる場合)を招く。GCRA アルゴリズムのスムーズなレート制御を検証する。

検証観点

2.19 Hook テスト TST019
HOOK

目的

Hook システムはキャッシュライフサイクルへのユーザー拡張ポイント(ロギング、メトリクス収集、監査等)であり、フック内の不具合がメインの関数実行に影響を与えないことが最重要の契約である。また ThreadSafeHookBase の自動ロック機構の正確性を保証する。

検証観点

2.20 MaintenanceService テスト TST020
MAINT

目的

MaintenanceService は CLI やダッシュボードからのシステム管理操作を仲介するサービス層である。タスク詳細の表示ミスは運用者の誤判断を招き、メンテナンス操作の不備はデータの意図しない削除に繋がる。

検証観点

2.21 CLI テスト TST021
CLI

目的

CLI はユーザーがキャッシュの状況確認・管理を行う主要インターフェースであり、コマンドの不具合はユーザー体験と運用効率に直接影響する。CliRunner によるコマンド実行と出力の正確性を E2E で検証する。

検証観点

2.22 ファクトリ関数・DI テスト TST022
DI

目的

bs.Spot() ファクトリ関数は全コンポーネントの DI 配線を担う唯一のパブリックエントリポイントであり、デフォルト構成の正確性とカスタム実装の差し替え可能性がシステム全体の柔軟性と正確性を決定する。

検証観点

2.23 Thundering Herd テスト TST023
HERD

目的

Thundering Herd Protection は、同一キーへの並行リクエストが一斉に関数を実行する「Thundering Herd」問題を防ぐ。この保護が不完全だと、重い計算やAPI呼び出しが不要に多重実行され、リソース浪費やレートリミット超過を招く。並行性バグはテスト困難であるため、複数の観点から網羅的に検証する。

検証観点

2.24 BlobStorageBase インターフェース契約テスト TST024
STORE

目的

BlobStorageBase は全ストレージバックエンド(LocalStorage, S3Storage, サードパーティ)が 準拠すべき抽象契約を定義する。この契約が正しく機能しないと、キャッシュデータの 保存・復元・削除・メンテナンス(GC)の全てが破綻する。 LocalStorage を具象実装として使い、インターフェース契約を網羅的に検証する。

検証観点

Ref: tests/integration/storage/test_blob_storage_base.py

IMPL Specification

1.0 序文 IMPL025

本ドキュメントは、『beautyspot』の実装記録(ジャーナル)を保持する。

2.0 詳細仕様 IMPL026

ソースコードの実装構造、設計判断の根拠、および技術的負債について記録する。

2.1 mark デコレータ実装 IMPL001
CACHE

実装概要

Spot.mark() メソッドが本仕様の中核。二段階デコレータファクトリとして、 オプションを受け取る外側の mark() と、関数をラップする内側の decorator() で構成される。 sync/async の判定は inspect.iscoroutinefunction()デコレーション時に行い、 同期関数は _execute_sync()、非同期関数は _execute_async() に委譲する。 functools.wraps(fn) により元関数のメタデータ(__name__, __doc__, __module__, __qualname__)を保持する。

設計判断

二段階デコレータファクトリの採用

@mark() (括弧あり)と @mark(括弧なし)の両方をサポートするため、 引数の型で分岐するパターンを採用した。第一引数が callable なら括弧なし、 そうでなければ括弧ありと判定する。

sync/async 分岐のタイミング

デコレーション時に判定する方式を採用。呼び出し時に毎回 inspect する方式と比較し、 ランタイムオーバーヘッドがゼロになる利点がある。ただしデコレーション後に 関数の性質が動的に変わるケースには対応できない(実用上問題なし)。

ジェネレータ関数の拒否

inspect.isgeneratorfunction()inspect.isasyncgenfunction() の 両方をチェックし、ConfigurationError を送出する。 ジェネレータの戻り値はイテレータであり、キャッシュの意味論と矛盾するため。

実装メモ

TODO / 技術的負債

Ref: src/beautyspot/core.py
2.2 cached_run 実装 IMPL002
CACHE

実装概要

Spot.cached_run() はコンテキストマネージャとして実装。 渡された関数群を内部で mark() と同等のラッピングを行い、 コンテキスト内で呼び出すとキャッシュが効く一時的なラッパーを返す。

設計判断

スマートリターンの型分岐

単一関数の場合は結果を直接返し、複数関数の場合はタプルで返す。 これにより wrapped_fn = spot.cached_run(fn) のように自然に書ける。 0個の場合は ValidationError で早期失敗させる。

コンテキストマネージャパターン

@contextmanager デコレータを使用。__enter__ でラップ済み関数を返し、 __exit__ で後処理(drain 等)は with spot: に委譲する設計。

実装メモ

Ref: src/beautyspot/core.py
2.3 同期・非同期実行エンジン IMPL003
CACHE

実装概要

Spot._execute_sync()Spot._execute_async() が実行エンジンの中核。 両メソッドは同一のフロー(キー生成→キャッシュ検索→Herd待機→関数実行→保存)を それぞれ同期/非同期のセマンティクスで実装する。 mark() がデコレーション時に inspect.iscoroutinefunction() で判定し、 適切な方を選択する。

設計判断

同期・非同期の並行実装

_execute_sync_execute_async は処理フローが同一だが、 await の有無やロック機構(threading.Event vs asyncio.Future)が異なるため、 共通化せず並行して実装している。DRY 原則よりも明瞭性・デバッグ容易性を優先した。

Herd 結果の保存前共有

Herd protection の結果ボックスへの結果格納は、DB/Blob 保存のに行う。 これにより、保存が失敗しても待機中のスレッド/タスクは結果を受け取れる。 「保存の失敗で実行結果が失われる」ことを防ぐ意図的な設計。

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

save_sync=False 時の保存エラーは on_background_error コールバックに 通知し、ERROR ログを出力するが、例外を再送出しない。 関数の実行自体は成功しているため、ユーザーに例外を見せるのは不適切と判断した。

実装メモ

Ref: src/beautyspot/core.py
2.4 SHA-256 キャッシュキー実装 IMPL004
KEY

実装概要

KeyGen クラスの静的メソッド群がキャッシュキー生成を担う。 _default(args, kwargs) が主要なエントリポイントで、引数を canonicalize() で正規化 → msgpack でシリアライズ → SHA-256 でハッシュする。 最終キーは func_identifier:input_id[:version] 形式の文字列を SHA-256 した値。

設計判断

SHA-256 の採用

MD5 より衝突耐性が高く、Python 標準ライブラリの hashlib で利用可能。 キャッシュキーとして64文字の hex 文字列は十分にコンパクトで、 DB のインデックスとしても効率的。

msgpack 経由のシリアライズ

正規化結果を直接 str() でハッシュする方式と比較し、 msgpack はバイナリ表現が安定しており、Python バージョン間での repr() の差異に影響されない。

ファイルベースの戦略

from_file_content() はファイルを 65KB チャンクで読み取り、 拡張子をハッシュに含める。大きなファイルでもメモリ効率が良い。 from_path_stat() は mtime + size のみでハッシュし、 ファイル内容の読み取りを避ける高速版。ファイル不在時は "MISSING_{filepath}" を返し、不在自体をキーに反映する。

実装メモ

Ref: src/beautyspot/cachekey.py
2.5 再帰的正規化 (canonicalize) IMPL005
KEY

実装概要

canonicalize()functools.singledispatch で実装された再帰的正規化関数。 型ごとにディスパッチハンドラを登録し、ネストされたデータ構造を再帰的に正規化する。 各ハンドラは型タグ付きのタプルを返し、異なるコレクション型が同じ内容でも 区別されるようにする。

設計判断

singledispatch の採用

if/elif チェーンと比較して、新しい型のサポート追加が局所的で、 既存コードへの影響がない。numpy や Pydantic のようなオプショナル依存の 型ハンドラも、条件付きで登録できる。

型タグによるコレクション区別

[1, 2](list)と (1, 2)(tuple)を区別するため、 正規化結果に ("__list__", ...), ("__tuple__", ...) のように 型タグを含める。これにより、構造的に同一でも型が異なるデータが 異なるキャッシュキーを生成する。

bool vs int の区別

Python では True == 1 かつ isinstance(True, int) だが、 キャッシュキーとしては区別が必要。singledispatch は MRO 順でマッチするため、 boolint より先にチェックするインライン処理を base handler に配置した。

実装メモ

Ref: src/beautyspot/cachekey.py
2.6 KeyGenPolicy 実装 IMPL006
KEY

実装概要

Strategy enum(DEFAULT, IGNORE, FILE_CONTENT, PATH_STAT の4種)と KeyGenPolicy クラスで構成。KeyGenPolicy は引数名→戦略のマッピングを保持し、 bind(func)inspect.signature() を使って関数シグネチャにバインドされた キー生成関数を返す。ファクトリメソッド KeyGen.ignore(), KeyGen.map(), KeyGen.file_content(), KeyGen.path_stat() で宣言的にポリシーを構築できる。

設計判断

bind() によるシグネチャバインディング

デコレーション時に inspect.signature(func) を呼び、 位置引数・キーワード引数・デフォルト値を解決する。 これにより、呼び出し時にはシグネチャ解析のオーバーヘッドがなくなる。

Strategy enum による宣言的指定

引数ごとの戦略を enum で宣言する方式を採用。 関数を渡す方式(keygen=lambda ...)と比較して、 シリアライズ可能で、デバッグ時の可読性が高い。

実装メモ

Ref: src/beautyspot/cachekey.py
2.7 SQLiteTaskDB 実装 IMPL007
DB

実装概要

SQLiteTaskDB クラスが中核。Writer Thread + Reader Threads パターンで実装。 単一の _writer_thread(デーモン)が _write_queue から書き込みタスクを取り出して 直列処理し、読み取りは threading.local() のスレッドローカル接続で並行実行する。 WAL モードにより読み取りと書き込みが並行可能。

設計判断

専用ライタースレッドの採用

SQLite は単一書き込み接続しかサポートしないため、全書き込みを1つのスレッドに 集約する Producer-Consumer パターンを採用。キュー経由でタスクを受け渡し、 ライタースレッドがコミット/ロールバックを管理する。 マルチスレッドからの並行書き込みによるロック競合を根本的に回避する。

スレッドローカル読み取り接続

threading.local()_ReadConnWrapper を格納し、スレッドごとに 読み取り専用接続(PRAGMA query_only = ON)を保持する。 接続エラー時は自動的に再作成し、リーク防止のため WeakSet で全接続を追跡する。

_WriteTask 状態マシン

書き込みタスクを PENDING → RUNNING → DONE(または CANCELLED)の 状態マシンで管理。タイムアウト時の try_cancel() により、 キュー内で待機中のタスクを安全にキャンセルできる。 既に RUNNING 状態のタスクはキャンセルせず完了を待つ。

実装メモ

Ref: src/beautyspot/db.py
2.8 DB プロトコル階層実装 IMPL008
DB

実装概要

DB 層のプロトコル階層を定義。TaskDBCore(ランタイム必須の CRUD)、 Flushable(書き込み同期)、Shutdownable(安全な停止)、 Maintenable(GC・統計等の管理操作)の4つのプロトコルと、 それらを統合した TaskDBMaintenable を提供する。 TaskDBBase は ABC として Maintenable のデフォルト実装(安全な no-op)を提供する。

設計判断

プロトコルの細分化

単一の巨大インターフェースではなく、責務ごとにプロトコルを分割した。 これにより、カスタム DB 実装は TaskDBCore のみ実装すればランタイムで動作し、 メンテナンス機能は段階的にオプトインできる。

デフォルト実装の安全性

TaskDBBase のメンテナンスメソッド(delete_expired(), prune() 等)は デフォルトで空リスト返却や no-op を行い、サブクラスが未実装でも安全に動作する。 CLI の gc コマンドが未対応の DB バックエンドでもエラーにならない設計。

実装メモ

Ref: src/beautyspot/db.py
2.9 LocalStorage 実装 IMPL009
STORE

実装概要

設計判断

アトミック書き込みパターン

一時ファイルに書き込み → fsync でディスクに確実に反映 → os.replace で アトミックにリネーム。この3段階により、書き込み中のクラッシュや 並行アクセスでファイルが半壊状態になることを防ぐ。 一時ファイルには .spot_tmp 接尾辞を使用し、残存時のクリーンアップを容易にした。

パストラバーサル防止の二重ガード

実装メモ

Ref: src/beautyspot/storage.py
2.10 S3Storage 実装 IMPL010
STORE

実装概要

S3Storage クラスが BlobStorageBase を実装。 s3://bucket/prefix/ 形式の URI を解析し、boto3 の S3 クライアントを使用。 ファイルは {prefix}/{key}.bin のキーで S3 に保存される。

設計判断

オプショナル依存のガード

boto3 は import 時にガードし、未インストール時は クラス定義は成功するがインスタンス化で明確なエラーメッセージを表示する。 beautyspot 全体が boto3 に依存しないよう、遅延インポートパターンを採用。

URI ベースのバックエンド選択

s3://bucket/prefix 形式の URI から bucket と prefix を自動解析する _parse_s3_uri() ヘルパーにより、ファクトリ関数がスキームに基づいて LocalStorage と S3Storage を透過的に切り替えられる。

実装メモ

Ref: src/beautyspot/storage.py
2.11 ストレージポリシー実装 IMPL011
STORE

実装概要

StoragePolicyProtocolshould_save_as_blob(data: bytes) -> bool を定義。 3つの実装を提供: - ThresholdStoragePolicy: len(data) > threshold で判定 - WarningOnlyPolicy: 常に False を返し、閾値超過時に WARNING ログを出力 - AlwaysBlobPolicy: 常に True を返す

設計判断

Strategy パターンによるポリシー分離

保存先判定ロジックを core.Spot から分離し、差し替え可能なポリシーオブジェクトに 委譲する。@mark(save_blob=True/False) の明示指定はポリシーより優先されるが、 save_blob=None(デフォルト)時にポリシーが判定を行う。

WarningOnlyPolicy をデフォルトにした理由

v2.0 との後方互換性を維持するため。v2.0 では全データが DB 直接保存だったため、 デフォルトを ThresholdStoragePolicy にすると既存ユーザーの挙動が変わる。 WARNING ログにより、ユーザーに Blob 分離の恩恵を段階的に周知する。

実装メモ

Ref: src/beautyspot/storage.py
2.12 MsgpackSerializer 実装 IMPL012
SERIAL

実装概要

MsgpackSerializer クラスが SerializerProtocolTypeRegistryProtocol を実装。 dumps(obj) -> bytes / loads(data) -> Any が主要 API。 カスタム型は msgpack の ExtType(code, payload) で拡張する。 _default_packer() がエンコード時のカスタム型ディスパッチ、 _ext_hook() がデコード時の型復元を行う。

設計判断

Copy-on-Write レジストリ

_encoders / _decoders 辞書は register() 時に新しい辞書を作成して アトミックに差し替える(CoW)。読み取り側はスナップショットを参照するため、 ロックなしで安全に読める。GIL に依存せず、PEP 703(free-threading)にも 対応できる設計。世代カウンタ _registry_generationレジストリ差し替えの前に インクリメントすることで、読み取り側が古いキャッシュを使い続けることを防ぐ。

スレッドローカル LRU キャッシュ

MRO スキャンの結果(サブクラス→登録済み親クラスの解決)を threading.local()OrderedDict にキャッシュする。_cache_generation_registry_generation を比較し、 世代が変わったらキャッシュを破棄する。LRU のサイズ上限は max_cache_size で制御。

MRO スキャンによるサブクラス自動解決

Pydantic モデルのサブクラスのように、親クラスが登録済みなら サブクラスも自動的に同じエンコーダで処理できる。type(obj).__mro__ を走査し、 最初にマッチした登録型のエンコーダを使用する。

実装メモ

Ref: src/beautyspot/serializer.py
2.13 カスタム型登録実装 IMPL013
SERIAL

実装概要

2つのレイヤーで構成: - MsgpackSerializer.register(type, code, encoder, decoder): 低レベルの型登録 API - Spot.register() / Spot.register_type(): ユーザー向けの高レベル API

Spot.register() はデコレータ形式、Spot.register_type() は命令形式で、 いずれも内部で serializer.register() に委譲する(TypeRegistryProtocol 準拠時のみ)。

設計判断

二層 API の提供

デコレータ形式 @spot.register(code=1, ...) はクラス定義と同時に登録でき、 宣言的で読みやすい。命令形式 spot.register_type(MyClass, code=1, ...) は 外部ライブラリのクラスなどデコレートできない場合に使用する。

TypeRegistryProtocol による条件分岐

シリアライザが TypeRegistryProtocol を実装していない場合(カスタムシリアライザ)、 register() / register_type()ConfigurationError を送出する。 これにより、型登録非対応のシリアライザを使用する場合のエラーメッセージが明確になる。

実装メモ

Ref: src/beautyspot/serializer.py, src/beautyspot/core.py
2.14 BackgroundLoop 実装 IMPL014
BGIO

実装概要

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

設計判断

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

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

独立したイベントループ

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

実装メモ

Ref: src/beautyspot/core.py
2.15 save_sync / flush / drain 実装 IMPL015
BGIO

実装概要

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

設計判断

flush と drain の使い分け

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

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

実装メモ

Ref: src/beautyspot/core.py
2.16 LifecyclePolicy 実装 IMPL016
LIFE

実装概要

LifecyclePolicy クラスは Rule オブジェクトのリストを保持し、 関数名に基づいてキャッシュの保持期間を決定する。 resolve() メソッドが fnmatch.fnmatch() を使って関数名をパターンと照合し、 最初にマッチしたルールの保持期間を返す。

設計判断

First-Match セマンティクス

ルールのリストは順序が意味を持ち、最初にマッチしたものが採用される。 これにより、特定プレフィックスの関数には短い保持期間を設定し、 最後に *(ワイルドカード)でデフォルトポリシーを設定するような フォールバック構造が簡単に記述できる。

resolve_with_fallback の導入

後方互換性と柔軟性のため、まず func_identifier (モジュール名付きの完全修飾名) で マッチングを試み、マッチしなかった場合は func_name (短い関数名) で再度マッチングを 試みる resolve_with_fallback() を提供している。

default_retention の導入

LifecyclePolicy コンストラクタに default_retention パラメータを追加。 どのルールにもマッチしない場合に返す保持期間を指定できる。 LifecyclePolicy.default()default_retention="30d" で生成し、 デフォルトで30日の保持期間を設定する。

実装メモ

Ref: src/beautyspot/lifecycle.py
2.17 Retention 実装 IMPL017
LIFE

実装概要

Retention クラスを名前空間として使用し、保持期間のパースや 特殊な定数(INDEFINITE, FOREVER)を管理する。 parse_retention() 関数は文字列("7d", "12h"等)、timedelta、 または秒数(int/float)を受け取り、標準化された timedelta オブジェクトに変換する。

設計判断

_ForeverSentinel シングルトン

Retention.FOREVER はポリシーを強制的にバイパスするための特殊値。 PEP 703 (free-threading) 環境での安全性を考慮し、 _ForeverSentinelthreading.Lock を用いたダブルチェックロッキングで 厳密なシングルトンとして実装されている。

直感的な文字列表現のサポート

"7d", "12h", "30m", "10s" のような文字列フォーマットを 正規表現 _TIME_PATTERN でパースすることで、設定ファイルや ハードコード時の可読性を高めている。

実装メモ

Ref: src/beautyspot/lifecycle.py
2.18 TokenBucket 実装 IMPL018
LIMIT

実装概要

LimiterProtocol を実装する TokenBucket クラス。 GCRA (Generic Cell Rate Algorithm) に基づき、スレッドセーフおよび 非同期対応のスムーズなレートリミッタを提供する。 内部状態として Theoretical Arrival Time (TAT) を保持し、 consume() で同期的スリープ、consume_async() で非同期的スリープを行う。

設計判断

GCRAアルゴリズムの採用

伝統的なトークンバケットとは異なり、長時間アイドル後に 一気にバーストを許容しない「Strict Pacing」を実現できる。 TATの更新と現在時刻の比較だけで待機時間を計算できるため、 メモリ効率と計算効率に優れる。

モノトニッククロックの利用

時刻の取得に time.monotonic() を使用し、システム時刻の変更(NTP同期など)の 影響を受けない堅牢な設計としている。

実装メモ

Ref: src/beautyspot/limiter.py
2.19 HookBase 実装 IMPL019
HOOK

実装概要

タスク実行ライフサイクルに介入するインターフェース HookBase と、 そのスレッドセーフ版 ThreadSafeHookBase の実装。 pre_execute, on_cache_hit, on_cache_miss の各コールバックが提供される。 ThreadSafeHookBase__init_subclass__ を利用し、サブクラスで定義された フックメソッドを自動的にロックでラップする。

設計判断

init_subclass による透過的ロッキング

ユーザーが手動でロックを書く手間を省くため、メタクラス的なアプローチを採用。 クラス定義時にフックメソッドを抽出し、_wrap_with_lock デコレータで包むことで、 ユーザーコードを汚さずに完全な排他制御を実現している。

RLock(再入可能ロック)の使用

当初の Lock から RLock に変更された。 サブクラスが super().pre_execute(...) のように親のメソッドを呼び出した際、 同一スレッドが再度ロックを取得しようとしてデッドロックする問題を防ぐため。

実装メモ

Ref: src/beautyspot/hooks.py
2.20 MaintenanceService 実装 IMPL020
MAINT

実装概要

MaintenanceService クラスがDBとBlobストレージ間の整合性チェック、 期限切れキャッシュの削除、孤立ファイルの検出といったガベージコレクション(GC)を担当。 主にCLIの beautyspot gc コマンドから呼び出される。 対象となる DB (TaskDBMaintenable) とストレージ (BlobStorageMaintenable) を受け取り、 複数のフェーズに分けてクリーンアップを実行する。

設計判断

5フェーズのクリーンアッププロセス

clean_garbage() は以下の順序で安全にGCを実行する: 1. 期限切れDBレコードの削除 2. Blobストレージの一時ファイル(.spot_tmp)のクリーンアップ 3. 孤立したBlobファイル(DBに存在しないファイル)の特定 4. 孤立ファイルの削除 5. 空ディレクトリの剪定

Grace Period (猶予期間) の導入

実行中のタスクがBlobを書き込んだ直後で、まだDBにメタデータが保存されていない タイミングでGCが走ると、必要なファイルが孤立と誤認されるリスクがある。 これを防ぐため、orphan_grace_seconds(デフォルト60秒)より新しいファイルは 孤立判定から除外する設計とした。

実装メモ

Ref: src/beautyspot/maintenance.py
2.21 CLI コマンド実装 IMPL021
CLI

実装概要

Typer を利用したコマンドラインインターフェースの実装。 list, show, stats, clear, clean, gc, prune, version 等の コマンドを提供し、rich ライブラリを用いてコンソール出力(テーブル、パネル、 プログレスバー等)をリッチにフォーマットしている。

設計判断

Typer と Rich の組み合わせ

Typer は型ヒントベースでコマンドやオプションを定義でき、コードの記述量と メンテナンスコストを大幅に削減できる。Rich による出力は、単なるテキストではなく 構造化された情報(JSONやMarkdown)の視認性を劇的に向上させ、DXを高める。

安全性を考慮した対話的プロンプト

clearclean など、データを大規模に削除するコマンドについては、 --force オプションが指定されない限り、rich.prompt.Confirm を用いて ユーザーに明示的な確認を求めることで、誤操作によるデータ喪失を防いでいる。

実装メモ

Ref: src/beautyspot/cli.py
2.22 ファクトリ関数実装 IMPL022
DI

実装概要

beautyspot パッケージのメインエントリポイントとなる Spot ファクトリ関数の実装。 引数として渡された各コンポーネント(DB、Serializer、Storage等)を依存性注入(DI)で 解決し、デフォルトのコンポーネント(SQLiteTaskDB, MsgpackSerializer, LocalStorage等)を インスタンス化して CacheManager_Spot コアエンジンを組み立てる。

設計判断

Factory 関数による Composition

_Spot クラス自体のコンストラクタは複雑な依存関係を要求するが、 ユーザー向けに Spot() 関数を提供することで、通常は name を渡すだけで 「ゼロ設定」で動作するようにカプセル化している。 同時に、高度なユーザーは各コンポーネントを自由に差し替え可能な DI アーキテクチャを維持している。

DB ライフサイクルの委譲

Spot() 関数内でデフォルトのDB(SQLiteTaskDB)を自動生成した場合、 spot._owns_db = True フラグを立て、Spotエンジンのシャットダウン時に DBも自動でクローズされるようにする。一方、ユーザーが明示的に db= を 渡した場合は、DBのライフサイクル管理は呼び出し元に委ねる(勝手に閉じない)。

実装メモ

Ref: src/beautyspot/__init__.py
2.23 Thundering Herd 実装 IMPL023
HERD

実装概要

CacheManager クラス内で、Thundering Herd(キャッシュミス時に同一キーへの 大量アクセスが同時に発生する問題)を防止する直列化機構を実装。 _inflight 辞書で実行中のキーを管理し、最初の1スレッド/タスクだけが関数を実行し、 後続の呼び出しは wait_herd_sync / wait_herd_async でその完了を待機する。

設計判断

同期・非同期の統合的保護

_inflight 辞書の値として (threading.Event, list[asyncio.Future], list[result]) の タプルを保持し、スレッドベースの待機 (Event.wait) と asyncio ベースの待機 (Future) の 両方を単一の管理下で混在できるようにしている。

タイムアウトと再試行(自己回復)

関数の実行がハングアップした場合に待機側が永遠にブロックされるのを防ぐため、 HERD_TIMEOUT(300秒)と HERD_MAX_RETRIES(3回)を設定。 タイムアウト時には警告ログを出しつつ再試行し、上限を超えれば TimeoutError で フェイルファストする堅牢な設計とした。

実装メモ

Ref: src/beautyspot/cache.py
2.24 BlobStorageBase 抽象インターフェース実装 IMPL024
STORE

実装概要

storage.py 内の BlobStorageBase ABC が SPEC024 の中核実装。 5つの抽象メソッド(save, load, delete, list_keys, get_mtime)を定義し、 LocalStorageS3Storage が具象実装として継承する。

加えて、ランタイム型チェックのために BlobStorageCoreMaintenableBlobStorageMaintenable の3つの Protocol クラスを定義し、 isinstance() での型判定を可能にしている(@runtime_checkable)。

設計判断

ABC と Protocol の併用

BlobStorageBase は ABC として抽象メソッドを強制する一方、 BlobStorageCore / Maintenable は Protocol として構造的部分型を提供する。 これにより、BlobStorageBase を継承しないサードパーティ実装でも isinstance(obj, BlobStorageCore) で利用可能になる柔軟性を確保した。

3層の Protocol 分割

これにより、save/load/delete のみを実装した軽量バックエンドも受け入れ可能。

ReadableBuffer 型エイリアス

bytes | bytearray | memoryviewReadableBuffer として定義。 memoryview を受け入れることでゼロコピー書き込みが可能になり、 大きなデータを保存する際のメモリ効率を改善している。

実装メモ

Ref: src/beautyspot/storage.py