Doorstop Specifications

Generated at 2026-03-16 14:05:04

REQ Specification

1.0 序文 REQ014

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

背景・動機

プロジェクト全体の要求事項を体系化し、設計と実装の指針となる要求の全容と境界(スコープ)を明確にするため。

2.0 背景・動機 REQ015

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

背景・動機

本プロジェクトが存在する意義と解決すべき課題を明確にし、すべてのステークホルダー間でプロダクトの方向性を共有するため。

3.0 用語定義 REQ016

本プロジェクト(Beautyspot)で使用される主要な用語の定義です。

背景・動機

プロジェクトに関わる全メンバー(開発者、利用者)が同一の「ユビキタス言語」を用いることで、ドキュメント、コード、およびコミュニケーションにおける認識の齟齬を防ぐため。

3.1 用語集 REQ018
COMMON Non-normative

仕様書の機能グループ (Groups)

仕様書アイテムに付与される groups 属性の意味は以下の通りです。

グループ名

意味 / 対象領域

CACHE

関数キャッシュの基本機能(同期・非同期のデコレータなど)

KEY

キャッシュキーの生成ロジック(引数のハッシュ化など)

DB

メタデータDB(有効期限、メタ情報の管理)

STORE

Blobストレージ(ペイロードの保存)

SERIAL

シリアライゼーション(データの変換処理)

BGIO

バックグラウンドIO(非同期書き込み処理)

LIFE

ライフサイクル管理(有効期限の判定、無効化ルールなど)

LIMIT

レート制限(トークンバケット方式等)

HOOK

コールバックフックの仕組み

MAINT

メンテナンス(キャッシュの削除、ガベージコレクション)

CLI

コマンドラインインターフェース

DI

依存注入の仕組みとデフォルトファクトリ

HERD

Thundering Herd対策(直列化、重複実行防止)

COMMON

共通定義(用語集など、特定の機能に縛られないもの)

背景・動機

ドキュメント内で使われる概念を定義し、チーム内のコミュニケーション齟齬を未然に防ぐため。

4.0 機能要件 REQ017

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

背景・動機

システムが満たすべき具体的な振る舞いや機能を明確にし、設計やテストの入力とするため。

4.1 関数キャッシュ REQ001
CACHE

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

背景・動機

データパイプラインや機械学習の実験、LLMのAPI呼び出しにおいて、同一入力に対する高コストな再実行を回避し、システムの応答速度向上とAPI課金の節約を同時に実現するため。

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

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

背景・動機

Pythonの多様なオブジェクト(辞書、リスト、カスタムクラス等)を入力とする関数において、意味的に同一の入力を正確に識別し、キャッシュのヒット率を最大化するとともに、意図しないキャッシュミスや誤ヒットを防ぐため。

4.3 メタデータDB REQ003
DB

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

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

背景・動機

メタデータ(関数名、引数ハッシュ等)をRDBMSなどで管理することで、キャッシュの検索・有効期限管理・統計情報の取得を高速かつ一元的に行うため。また、柔軟なDI構造により、開発環境(SQLite)から本番環境(Redis/PostgreSQL等)へスケールアップ可能にする。

4.4 Blobストレージ REQ004
STORE

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

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

背景・動機

LLMの生成テキストや機械学習モデルの推論結果など、数MB〜数GBに及ぶ結果データをデータベース(SQLite等)に直接保存すると、DBの肥大化とパフォーマンス劣化を招くため、メタデータと実データを分離して管理するアーキテクチャが必要不可欠なため。

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

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

背景・動機

Pythonのオブジェクト(辞書やカスタムクラス等)を、バイト列としてDBやストレージに安全かつ効率的に永続化・復元するため。また、任意のオブジェクト(例: Pandas DataFrameやPydanticモデル)にも対応できる拡張性を提供するため。

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

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

背景・動機

キャッシュの保存処理(DBへのインサートやファイル書き込み)がメインスレッドをブロックすると、関数の応答速度(レイテンシ)が悪化し、ユーザー体験を損なうため。キャッシュ保存の非同期化により、キャッシュ適用によるパフォーマンスのペナルティを実質ゼロにする。

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

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

背景・動機

キャッシュデータは時間経過とともに価値が低下したり、ストレージ容量を圧迫したりするため。手動による削除運用を不要にし、自動的かつ柔軟に古いデータを無効化・削除する仕組みを提供することで、メンテナンスコストを削減する。

4.8 レート制限 REQ008
LIMIT

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

背景・動機

外部API(特にLLMのAPIなど)は、利用制限(Rate Limit)が厳しく設定されていることが多く、短時間での過剰なリクエストによるエラー(HTTP 429等)を防ぐため。キャッシュだけでなくAPI呼び出し自体の安定性を高める。

4.9 フック機能 REQ009
HOOK

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

背景・動機

ユーザーが関数のビジネスロジックを汚すことなく、キャッシュヒット率の計測、LLMのトークン消費量のトラッキング、あるいはカスタムログの出力などの横断的関心事(Cross-cutting concerns)を柔軟に差し込めるようにするため。

4.10 メンテナンス REQ010
MAINT

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

背景・動機

運用が長期化するにつれて、無効になったキャッシュデータや孤立したファイルがストレージを逼迫するため。運用者がストレージを健全に保つための管理機能(定期クリーンアップ等)を提供し、システムダウンを防ぐ。

4.11 CLI REQ011
CLI

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

背景・動機

開発時や運用時に、現在のキャッシュの状況確認や不要なキャッシュの手動削除を、Pythonコードを書かずにコマンドラインから直感的に素早く実行できるようにするため。

4.12 依存注入設計 REQ012
DI

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

背景・動機

プロジェクトの規模や要件(ローカルでのプロトタイピングから、クラウド上での分散デプロイまで)の変化に合わせて、キャッシュバックエンドやストレージを柔軟に入れ替え可能にし、ライブラリの汎用性を極大化するため。

4.13 Thundering Herd対策 REQ013
HERD

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

背景・動機

同一の重い計算処理やAPI呼び出しに対して、複数スレッド/プロセスから同時にリクエストが殺到した際、キャッシュミスとなって無駄な再実行が多重に発生する(Thundering Herd問題)のを防ぎ、システムリソースを保護するため。

4.14 データベース操作の可用性とフェイルファスト REQ019
DB

データベース操作が指定されたタイムアウト時間内に完了しない場合、呼び出し元を無期限にブロックせずにエラーを返し、システムの可用性を維持すること。特にSQLiteの書き込みスレッドがスタックした場合でも、フェイルファストによって制御を戻せること。

背景・動機

重い計算処理やLLM呼び出しのキャッシュ層がボトルネックとなり、システム全体の停止を招くことを防ぐため。キャッシュの保存や取得の失敗はログに留め、メインの処理は続行させる「グレースフル・デグラデーション」を保証する。

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

非機能要件方針

設計根拠

キャッシュ機能の複雑さ(DB、ストレージ、シリアライズ、ライフサイクル)をユーザーから隠蔽しつつ、内部コンポーネントを粗結合に保つため。Spotクラスを中心としたDI(依存性注入)アーキテクチャにより、柔軟な拡張とテスト容易性を実現した。

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 衝突確率が極めて低く、暗号学的に安全かつ広くサポートされているため

非機能要件方針

設計根拠

入力引数の正規化・ハッシュ化は複雑になりがちなため、PolicyとStrategyのパターンを用いて「どう正規化するか」と「どの引数を対象にするか」を分離。これにより、ユーザーが特定の引数をハッシュ計算から除外するなどのカスタマイズを容易にした。

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等に容易に差し替え可能

非機能要件方針

設計根拠

メタデータの永続化において、高速な検索とトランザクション管理が必要なため。RDBMSベース(SQLite)をデフォルトとしつつ、Protocolに基づく抽象化により将来的なRedisや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) 業界標準のプロトコルによる高い互換性とスケーラビリティ

非機能要件方針

設計根拠

データサイズに応じた保存先の自動切り替え(小サイズはDB、大サイズはBlob)を透過的に実現するため。また、ローカルディスクと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バイトのオーバーヘッドで表現可能

非機能要件方針

設計根拠

データのシリアライズにおいて、パフォーマンスと拡張性を両立させるため。MessagePackを基盤とし、型レジストリパターンを組み合わせることで、高速かつコンパクトなデータ変換とカスタム型のサポートを同時に実現した。

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

非機能要件方針

設計根拠

同期処理に影響を与えずに非同期での保存を完了させるため。専用のバックグラウンドループ(デーモンスレッド)を用意し、メインスレッドのブロックを回避しつつ、アプリケーション終了時には確実にキューを消化する(atexit)仕組みを採用した。

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) 書き込み・読み込み時のオーバーヘッドを最小化

非機能要件方針

設計根拠

関数ごとの有効期限管理を宣言的かつ柔軟に行うため。正規表現(fnmatch)によるパターンマッチングとパースロジックを分離し、一元的にポリシーを管理できる構成とした。

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 同期・非同期の両方のコンテキストで正確なスロットリングを実現
コスト計算 ダイナミックコスト 引数に応じて消費トークン数を動的に変更可能

非機能要件方針

設計根拠

レート制限を正確かつ低オーバーヘッドで実現するため。TokenBucketモデルとGCRA(Generic Cell Rate Algorithm)を採用し、タイムスタンプベースでの計算によって不要なスレッドロックを排除したアーキテクチャとした。

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) ユーザーが意識せずに、サブクラス化するだけで安全なフックを実装可能
実行制御 準同期実行 フック内の例外をキャッチしログ出力に留めることで、主処理を保護

非機能要件方針

設計根拠

コアロジックを肥大化させずにイベント駆動の拡張を可能にするため。ObserverパターンのバリエーションとしてHookBaseを定義し、各フェーズのコンテキスト(状態)をカプセル化して渡すことで、安全に状態を参照・変更できる構成とした。

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をキックし、手動メンテなしでの健康度を維持

非機能要件方針

設計根拠

運用保守の責務をアプリケーションロジックから分離するため。TaskDBとBlobStorageの両方のインターフェースを統合操作する専用サービスを設け、自動(確率的)と手動の両方から安全にクリーンアップを実行可能にした。

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 インスタンスを組み立て

非機能要件方針

設計根拠

ライブラリのコアロジックを汚染せずにコマンドラインからの操作を提供するため。Typerを用いてCLI層を分離し、MaintenanceServiceに処理を委譲することで、ビジネスロジックの再利用とUIの関心事の分離を実現した。

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/ ディレクトリを基点とした標準パスを自動生成

非機能要件方針

設計根拠

エンドユーザーへの「ゼロ設定で動く使いやすさ」と「高度なカスタマイズ性」を両立するため。ファクトリ関数(bs.Spot)でデフォルトコンポーネントを自動ワイヤリングしつつ、個別のProtocol実装の差し替えを許容する構成とした。

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負荷(ビジーループ)を回避スコープキャッシュキー単位異なる関数の実行は妨げず、同一入力のみを直列化安全策タイムアウト & リトライ実行者がハングした場合に、待機側がデッドロックしないよう保護

非機能要件方針

設計根拠

キャッシュミスの瞬間に並行リクエストが殺到した際のリソース枯渇を防ぐため。CacheManager内でIn-flight(実行中)タスクを追跡し、後続リクエストをイベント(threading.Event / asyncio.Future)で待機させる構成とした。

SPEC Specification

1.0 序文 SPEC025

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

設計判断

システムの具体的な振る舞いとインターフェースを明確にし、実装の指針およびテスト設計の基準を提供するため。

2.0 詳細仕様 SPEC026

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

設計判断

個々の機能の詳細な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 の自動判定

実行フロー

エラーハンドリング

エッジケース

設計判断

現代のPython開発では同期(I/Oバウンドでない処理)と非同期(asyncio)が混在するため。利用者が関数の種類を意識することなく同一のAPIでキャッシュを利用できる透過的なインターフェースを提供するため。

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 バインド済み引数の正規化値リスト

エラーハンドリング

エッジケース

設計判断

生成されたキャッシュキーが環境や実行ごとにブレることなく、常に一意であることを保証するため。ハッシュ衝突のリスクが極めて低く、暗号学的に安全で広く利用されるSHA-256を採用した。

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 情報を使用 大容量ファイルの高速な変更検知

エラーハンドリング

エッジケース

設計判断

「DBの接続オブジェクト」や「テンポラリファイル」など、キャッシュキーに含めるべきではない引数を柔軟に除外できるようにするため。ユーザーに細かい制御手段を提供し、キャッシュの誤動作を防ぐ。

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, cache_key: str, *, include_expired: bool = False) -> TaskRecord | None: ...

振る舞い

ライタースレッド方式

読み取り並行性

スキーマ管理

タイムアウトとフェイルファスト

パラメータ詳細

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

エラーハンドリング

エッジケース

設計判断

デフォルトのメタデータ保存先として、追加のサーバー構築が不要で、かつ高速なSQLiteを使用するため。WALモードなどを駆使して並行書き込み性能を確保する具体的な実装方針を定義した。

2.8 DB プロトコル階層 SPEC008
DB

インターフェース

@runtime_checkable
class TaskDBCore(Protocol):
    def get(self, cache_key: str, *, include_expired: bool = False) -> 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() 実行履歴(統計)を取得する

エラーハンドリング

エッジケース

設計判断

データベースの実装(SQLite, Redis等)に依存しない共通の操作インターフェースを定義し、DIによるバックエンドの差し替えを可能にするため。

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) に渡される設定

エラーハンドリング

エッジケース

設計判断

クラウドネイティブな環境において、キャッシュデータをS3等のオブジェクトストレージに分散保存し、複数インスタンス間で安全に共有できるようにするため。

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

インターフェース

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

振る舞い

判定ロジック

パラメータ詳細

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

エラーハンドリング

エッジケース

設計判断

パフォーマンスとストレージ容量のトレードオフを最適化するため。小さなデータはDBに直接保存してI/Oを減らし、大きなデータはBlobストレージにオフロードする閾値判定の仕組みを定義した。

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キャッシュの最大サイズ

エラーハンドリング

エッジケース

設計判断

JSONよりも高速でコンパクトであり、バイナリデータにもネイティブに対応できるMessagePackをデフォルトのシリアライザとして採用し、キャッシュのI/Oオーバーヘッドを最小化するため。

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、戻り値オブジェクト プリミティブ構造からオブジェクトを復元する関数

エラーハンドリング

エッジケース

設計判断

標準では対応できないユーザー定義のクラス(Pandas DataFrameやPydanticモデル等)を、MessagePack経由で安全にシリアライズ・デシリアライズするための登録インターフェースを提供するため。

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

エラーハンドリング

エッジケース

設計判断

関数のメイン処理をブロックすることなく、背後で安全にキャッシュデータを永続化するため。専用のイベントループにより、I/O待ちによるパフォーマンス低下を防ぐ。

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保持期間の定義

デフォルト保持期間

エラーハンドリング

エッジケース

設計判断

関数名ごとのパターンマッチを用いて、特定の関数群に対して一括でキャッシュの有効期限(Retention)を適用できる柔軟なルールベースのライフサイクル管理を実現するため。

2.17 Retention 仕様 SPEC017
LIFE

インターフェース

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

振る舞い

文字列パース

数値・オブジェクト

パラメータ詳細 (単位)

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

エラーハンドリング

エッジケース

設計判断

ユーザーが直感的に理解しやすい文字列(例: '7d', '12h')で有効期限を指定できるようにし、内部で秒数に正規化して統一的に処理するため。

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 許容されるバースト数(トークン単位)

エラーハンドリング

エッジケース

設計判断

外部APIのレート制限(例: 1分間に100リクエスト)を正確に守るため。GCRA(Generic Cell Rate Algorithm)により、バーストを許容しつつ、滑らかで厳密な流量制御を実現する。

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

エラーハンドリング

エッジケース

設計判断

キャッシュヒット時のログ出力や、LLMのトークン消費量の集計など、メインのビジネスロジックに手を加えずにカスタム処理を介入させるための、安全な拡張ポイント(フック)を提供するため。

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 で削除対象とする経過日数

エラーハンドリング

エッジケース

設計判断

長期間の運用で蓄積される不要なキャッシュデータや期限切れデータを、手動または自動で安全に削除・整理し、システムをクリーンな状態に保つためのメンテナンスAPIを定義するため。

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 ターミナルベースのインタラクティブダッシュボードを起動

エラーハンドリング

エッジケース

設計判断

運用者がPythonコードを書くことなく、CLIから手軽にキャッシュの統計情報確認やガベージコレクションを実行できるようにし、運用保守のコストを下げるため。

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)

エラーハンドリング

エッジケース

設計判断

Spotクラスの初期化において、DIコンテナとして機能するファクトリを提供し、ユーザーが複雑なコンポーネント構成を意識することなく、シンプルにライブラリを利用開始できるようにするため。

2.23 Thundering Herd Protection SPEC023
HERD

インターフェース

class CacheManager:
    def herd_sync(
        self, key: str, serializer: Optional[SerializerProtocol] = None
    ) -> Generator[HerdWaitResult, None, None]: ...

    async def herd_async(
        self,
        key: str,
        serializer: Optional[SerializerProtocol],
        loop: asyncio.AbstractEventLoop,
        executor: Any,
    ) -> AsyncGenerator[HerdWaitResult, None]: ...

振る舞い

同時実行制御

  1. キャッシュキーをキーとした _inflight 辞書をチェックする
  2. 存在しない場合: HerdWaitResult を新規作成し、自分が「実行者 (Executor)」となる
  3. 存在する場合: 既存の HerdWaitResult を返し、自分は「待機者 (Waiter)」となる
  4. 完了通知: 実行者が処理を終えたら、HerdWaitResult の Event (または Future) をセットし、結果を共有する
  5. 自動クリーンアップ: with または async with ブロックを抜ける際、実行者は自動的に notify_and_cleanup_inflight を呼び出し、インフライト状態を解除する。これにより例外発生時も状態が残留しないことが保証される。

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

目的

実行中のタスク状態(イベント・結果・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. 作成: herd_sync / herd_async コンテキスト開始時に _inflight にキーが存在しない場合、 _inflight_lock を保持した状態で新規タプルを挿入する
  2. 保持: 実行者が処理を完了するまで、_inflight 辞書がタプルへの唯一の管理参照を保持する。 待機者はロック取得時にタプル要素への参照を取得するが、_inflight エントリが正規の所有者である
  3. 解放: コンテキスト終了(__exit__ / __aexit__)時に 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 実行者が失敗またはタイムアウトした際のリトライ回数

エラーハンドリング

エッジケース

設計判断

キャッシュミスの際に、複数のスレッド/プロセスが同時に重い関数を実行してしまうのを防ぎ、最初の1つの実行結果を他の要求にも共有させることで、システムの過負荷を回避するため。

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等)

エラーハンドリング

エッジケース

設計判断

ローカルファイルやS3といった具体的なストレージ実装に依存しない抽象インターフェースを定義し、ユーザーが要件に合わせて独自の保存先を簡単に実装・注入できるようにするため。

TST Specification

1.0 序文 TST025

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

目的

システムの各機能が要件や仕様通りに動作することを客観的に証明し、リファクタリングや機能追加時のデグレ(後退)を検知するテスト基盤を構築するため。

2.0 詳細仕様 TST026

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

目的

エッジケースや異常系を含む具体的なテストシナリオを網羅し、コンポーネントの堅牢性を保証するため。

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

目的

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

検証観点

Ref: tests/integration/core/test_basic.py
2.2 cached_run テスト TST002
CACHE

目的

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

検証観点

Ref: tests/integration/core/test_cached_run.py
2.3 同期・非同期サポートテスト TST003
CACHE

目的

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

検証観点

Ref: tests/integration/core/test_async_save.py
2.4 SHA-256 キャッシュキーテスト TST004
KEY

目的

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

検証観点

Ref: tests/unit/test_cachekey.py
2.5 再帰的正規化テスト TST005
KEY

目的

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

検証観点

Ref: tests/unit/test_cachekey.py
2.6 KeyGenPolicy テスト TST006
KEY

目的

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

検証観点

Ref: tests/unit/test_cachekey.py
2.7 SQLiteTaskDB テスト TST007
DB

目的

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

検証観点

Ref: tests/unit/test_db_robustness.py, tests/unit/test_db_timeout_hard.py
2.8 DB プロトコル階層テスト TST008
DB

目的

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

検証観点

Ref: tests/unit/test_db_writer_queue.py
2.9 LocalStorage テスト TST009
STORE

目的

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

検証観点

Ref: tests/integration/storage/test_local.py
2.10 S3Storage テスト TST010
STORE

目的

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

検証観点

Ref: tests/integration/storage/test_s3.py
2.11 ストレージポリシーテスト TST011
STORE

目的

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

検証観点

Ref: tests/unit/test_storage_policy.py
2.12 MsgpackSerializer テスト TST012
SERIAL

目的

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

検証観点

Ref: tests/unit/test_serializer.py
2.13 カスタム型登録テスト TST013
SERIAL

目的

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

検証観点

Ref: tests/unit/test_type_registry.py
2.14 BackgroundLoop テスト TST014
BGIO

目的

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

検証観点

Ref: tests/integration/core/test_background_loop.py
2.15 save_sync / flush / drain テスト TST015
BGIO

目的

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

検証観点

Ref: tests/integration/core/test_exit_drain.py
2.16 LifecyclePolicy テスト TST016
LIFE

目的

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

検証観点

Ref: tests/unit/test_lifecycle.py
2.17 Retention テスト TST017
LIFE

目的

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

検証観点

Ref: tests/unit/test_lifecycle_extended.py
2.18 TokenBucket テスト TST018
LIMIT

目的

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

検証観点

Ref: tests/unit/test_limiter.py
2.19 Hook テスト TST019
HOOK

目的

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

検証観点

Ref: tests/integration/core/test_hooks.py
2.20 MaintenanceService テスト TST020
MAINT

目的

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

検証観点

Ref: tests/unit/test_maintenance.py
2.21 CLI テスト TST021
CLI

目的

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

検証観点

Ref: tests/integration/cli/test_cli.py
2.22 ファクトリ関数・DI テスト TST022
DI

目的

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

検証観点

Ref: tests/integration/core/test_dependency_injection.py
2.23 Thundering Herd テスト TST023
HERD

目的

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

検証観点

Ref: tests/integration/core/test_thundering_herd.py
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 モードにより読み取りと書き込みの並行性を確保している。

スキーマ初期化(RAII)

__init__ の末尾で init_schema() を呼び出し、構築後のインスタンスは即座に使用可能な状態とする。 呼び出し元(core.Spot やファクトリ)が明示的に init_schema() を呼ぶ必要はない。 init_schema() は冪等であり、複数回呼び出しても安全である。

タイムアウトの実装

設計判断

SQLite特有のデータベースロック(Database Is Locked)を回避するため。単一のWriter Threadで書き込みを直列化し、WALモードとスレッドローカルなReaderを組み合わせることで、高い並行読み取り性能と安全な書き込みを両立する実装とした。

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スレッド/タスクだけが関数を実行し、 後続の呼び出しは herd_sync / herd_async コンテキスト内でその完了を待機する。

設計判断

コンテキストマネージャによる例外安全性

実行者 (Executor) が処理中に例外(KeyboardInterrupt や asyncio.CancelledError を含む)で 中断された場合でも、_inflight 状態が残留して後続タスクを永続的にブロックすることを防ぐため、 herd_sync / herd_async をコンテキストマネージャとして提供する。 クリーンアップ処理(notify_and_cleanup_inflight)は finally ブロック内で 自動的に実行される。

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

_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

ADR Specification

1.0 このADRのタイトル ADR001
BGIO

インスタンスの所有権と弱参照によるエグゼキュータのライフサイクル管理

コンテキストと課題

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

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

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 4.

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

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

技術詳細: なぜ atexit ではなく weakref.finalize なのか?

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

# 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)

影響・結果

1.1 関数引数のための安定したハッシュ化 ADR002
KEY

関数引数のための安定したハッシュ化

コンテキストと課題

beautyspot のコア機能である「関数のメモ化(キャッシュ)」において、関数の引数 (args, kwargs) から一意なキャッシュキーを生成する必要があります。

初期実装 (v0.1.0) では、json.dumps が失敗した場合のフォールバックとして str((args, kwargs)) のハッシュ値を使用していました。

しかし、Pythonのデフォルトの __str__ / __repr__ 実装は、オブジェクトのメモリアドレス(例: <MyObject at 0x10a...>)を含むことが多くあります。

これにより以下の問題が発生していました:

  1. 再起動ごとのキャッシュ無効化: プロセスを再起動するとメモリアドレスが変わり、同じ入力値でもハッシュが変わってしまう。
  2. 分散環境での不整合: 異なるマシン(あるいは異なるプロセス)で実行した場合、キャッシュキーが一致しない。

外部ライブラリ(joblib 等)を使えば解決しますが、beautyspot は軽量な「黒子」ライブラリを目指しており、依存関係を増やしたくありません。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

標準ライブラリの json モジュールを使用し、独自の default シリアライザ (_stable_serialize_default) を実装することで、依存関係なしで 堅牢なハッシュ生成を実現します。

具体的には以下の戦略を採用します:

  1. Set/Frozensetのソート: JSONは順序を持たない集合を扱えないため、sorted(list(obj)) でリスト化し、順序を保証します。
  2. カスタムオブジェクトの辞書化: __dict__ または __slots__ を参照し、オブジェクトの「中身の値」をシリアライズ対象とします。これにより、メモリアドレスへの依存を排除します。
  3. Bytes型: 16進数文字列 (hex) に変換します。
  4. 最終手段: それでもシリアライズできない型(循環参照など)については、例外的に str() を使用します(この場合のみ不安定になるリスクを許容します)。

影響・結果

1.2 堅牢なレートリミッターの実装 ADR003
LIMIT

スムーズなレートリミッター(GCRA)

コンテキストと課題

beautyspot のレート制限機能 (limiter) において、以下の課題があった。

  1. Idle Burst: トークンバケット方式では、アイドル中にトークンが溜まり、再開直後にバースト(集中アクセス)が発生してしまう。これを防ぐために容量(capacity)を小さくすると、今度は巨大なコストを持つタスクが実行できなくなる(デッドロック)問題が発生する。
  2. Start Dash Prevention: プロセス起動直後に複数のタスクが同時に走るのを防ぎたいが、最初の1回目まで待たされるのは避けたい。
  3. Clock Dependency: システム時刻の変更に堅牢である必要がある。
  4. Max Cost Guard: tokens_per_minute を単発タスクのコスト上限とする。
    • これを超えるコストが consume() に渡された場合、即座に ValueError を送出する。
    • 理由: APIの物理的なレート制限を超えるリクエストは、待機したところで成功する見込みが薄く、早期に設定ミスや入力ミスをユーザーに通知すべきであるため。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

アルゴリズムをトークンバケットから GCRA (Generic Cell Rate Algorithm) に変更する。

影響・結果

1.3 耐障害性のあるデシリアライズと明示的なバージョニング ADR004
SERIAL

耐障害性のあるデシリアライズと明示的なバージョニング

コンテキストと課題

Pythonの pickle は、保存されたオブジェクトのクラス定義と現在のコードが一致していることを前提とします。 開発中はクラス定義が頻繁に変更されるため、過去のキャッシュを読み込む際に AttributeError 等が発生し、アプリケーションがクラッシュする問題がありました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

  1. Fail Safe (Auto Recalc): Storage.load() でデシリアライズエラー(クラス不整合や破損)が発生した場合、これを CacheCorruptedError として捕捉し、core.py 側で 「キャッシュミス」 として扱う。これによりアプリをクラッシュさせず、自動的に再計算を行う。
  2. Explicit Versioning: ユーザーが意図的にキャッシュを無効化できるよう、@task デコレータに version 引数を追加する。これを変更するとキャッシュキーが変わり、古い(互換性のない)キャッシュを参照しなくなる。
  3. User Guidance: 自動再計算が発生した際、ログに「コード変更時は version の更新を検討してね」というヒントを出力し、ベストプラクティスへ誘導する。

影響・結果

1.4 エグゼキュータのライフサイクル管理 ADR005
BGIO

インスタンスの所有権と弱参照によるエグゼキュータのライフサイクル管理

コンテキストと課題

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

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

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 4.

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

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

技術詳細: なぜ atexit ではなく weakref.finalize なのか?

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

# 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)

影響・結果

1.5 ダッシュボードのインタラクションモデル ADR006
CLI

ダッシュボードのインタラクションモデル

コンテキストと課題

現状のダッシュボード (dashboard.py) では、タスク一覧の表示と詳細データの復元操作が分離しています。 ユーザーは一覧テーブルで対象を確認した後、別途ドロップダウンメニューから対応する cache_key (ハッシュ値) を手動で探し出す必要があり、認知負荷が高い状態です。 プロジェクトの依存関係として streamlit>=1.51.0 が確保されており、インタラクティブなデータフレーム機能が利用可能となっています。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

st.dataframeon_select 機能を使用し、テーブル行のクリックによって詳細ビューのコンテキストを切り替える方式を採用します。これにより、ユーザーはテーブル上のレコードを直接クリックするだけで、そのタスクの詳細や保存されたデータを復元できるようになります。

影響・結果

1.6 セマンティックなコンテンツタイプのサポート ADR007
DB

セマンティックなコンテンツタイプのサポート

コンテキストと課題

生成AIタスクの出力は、テキストだけでなく、画像、構造化データ、ダイアグラム(Mermaid, Graphviz/DOT, HTML)など多岐にわたります。 現状のデータベーススキーマ(result_type = FILE | DIRECT)は「データの保存形式」しか保持しておらず、「データの意味的種類(Semantic Type)」が不明であるため、ダッシュボードでの復元時に適切な可視化(レンダリング)ができません。

また、beautyspot はライブラリとしてユーザーの手元で動作するため、複雑なマイグレーション手順や重量級の依存関係(Alembicなど)を強制することはUXを損なうという課題があります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

1. Database Schema & Migration

2. Type Definition

3. Interface

4. Rendering Strategy (Dashboard)

5. Dependencies

影響・結果

1.7 Msgpackをデフォルトとしたカスタマイズ可能なシリアライズ戦略 ADR008
SERIAL

Msgpackをデフォルトとしたカスタマイズ可能なシリアライズ戦略

コンテキストと課題

現在、beautyspot はキャッシュデータのシリアライズ(保存)に Python 標準の pickle を使用している。 これには以下の重大な課題がある:

  1. セキュリティリスク (RCE): pickle は信頼できないデータを読み込む際に任意のコード実行(RCE)を引き起こす可能性があり、「共有キャッシュ」としての利用にリスクがある。
  2. 互換性: Python のバージョンやライブラリのバージョンが変わると、クラス定義の不整合によりデシリアライズに失敗しやすい。
  3. 代替案の欠点: 標準ライブラリの json は安全だが、画像などのバイナリデータを効率的に扱えず(Base64化でサイズ増)、タプルなどのPython固有型が失われる。

v1.0.0 リリースにあたり、「デフォルトで安全(Secure by Default)」 かつ 「拡張性(Extensibility)」 のあるシリアライズ戦略が必要である。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 4.

シリアライザのバックエンドとして msgpack (MessagePack) を採用し、さらにユーザーが任意の型変換ロジックを注入できる TypeRegistry パターンを導入する。

  1. 依存関係: msgpack>=1.0.0pyproject.toml に追加する。
  2. デフォルトの挙動:
    • pickle はデフォルトから廃止(またはオプトイン化)する。
    • msgpack を使用し、安全な型のみをシリアライズする。
  3. カスタム型の登録:
    • Project クラスに register_type(type, code, encoder, decoder) メソッドを追加する。
    • ユーザーは、シリアライズできない型(例: カスタムクラス)に対して、一意なID (code) と変換関数を登録することで対応できる。
  4. エラーハンドリング:
    • 未登録の型に遭遇した場合、標準の TypeError ではなく、具体的なオブジェクト情報と register_type の利用を促す親切なエラーメッセージ (SerializationError) を送出する。

Technical Details / 技術詳細

msgpackdefault (pack時) と ext_hook (unpack時) を活用し、拡張型を msgpack.ExtType としてラップする。

# Implementation Sketch

class Project:
    def __init__(self, ...):
        # ...
        self.serializer = MsgpackSerializer()

    def register_type(self, type_: type, code: int, encoder: Callable, decoder: Callable):
        """
        Register a custom serializer for a specific type.

        Args:
            type_: The class to handle (e.g. MyClass)
            code: Unique integer ID (0-127) for this type
            encoder: Function that converts obj -> bytes
            decoder: Function that converts bytes -> obj
        """
        self.serializer.register(type_, code, encoder, decoder)

# --- Internal ---

class MsgpackSerializer:
    def dump(self, obj):
        return msgpack.packb(obj, default=self._default_packer, use_bin_type=True)

    def load(self, data):
        return msgpack.unpackb(data, ext_hook=self._ext_hook, raw=False)

    def _default_packer(self, obj):
        # 登録済みの型なら ExtType に変換
        if type(obj) in self._encoders:
            code, encoder = self._encoders[type(obj)]
            return msgpack.ExtType(code, encoder(obj))

        # 未登録なら詳細なエラーを出す
        raise SerializationError(f"Object of type '{type(obj).__name__}' is not serializable...")

影響・結果

Updates (2025-12-11)

See ADR-0009 for further refinements regarding save_blob=False behavior (Msgpack + Base64) and size guardrails.

1.8 データベースの依存性の注入と抽象化 ADR009
DI

データベースの依存性の注入と抽象化

コンテキストと課題

これまでの beautyspot (v0.x) は、Project クラス内部で TaskDB (SQLite実装) をハードコードしてインスタンス化していた。

# v0.x implementation
self.db = TaskDB(self.db_path)

この設計には以下の課題がある:

  1. 拡張性の欠如: SQLite 以外のデータベース(PostgreSQL, DuckDB, In-Memory DB等)を使いたくても、ライブラリのコードを書き換えない限り不可能である。
  2. テストの制約: ユニットテスト時に、ファイルシステムに依存しないモックDBやオンメモリDBへの差し替えが困難である。

v1.0.0 では、ユーザー体験(DX)としての「手軽さ(パスを指定するだけ)」を維持しつつ、アーキテクチャレベルでの柔軟性を確保する必要がある。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

  1. 抽象化: src/beautyspot/db.py に抽象基底クラス TaskDB を定義し、インターフェース(save, get, init_schema 等)を強制する。
  2. 具象化: 従来のSQLite実装を SQLiteTaskDB として再定義する。
  3. 注入 (DI): Project クラスのコンストラクタ引数を db_path: str から db: Union[str, TaskDB] に変更する。
    • str が渡された場合: 内部で SQLiteTaskDB(path) を生成する(Convenience)。
    • TaskDB が渡された場合: そのインスタンスをそのまま使用する(Injection)。

影響・結果

1.9 ネイティブBLOBサポートを伴う全体的なMsgpackの採用 ADR010
DB

ネイティブBLOBサポートを伴う全体的なMsgpackの採用

コンテキストと課題

ADR-0007 で MsgpackSerializer を導入しましたが、SQLiteへの保存方式について以下の課題が残っていました。

  1. JSONの限界: 従来の TEXT カラム(JSON)ではバイナリデータを扱えず、Numpy配列などが保存できない。
  2. Base64の非効率性: TEXT カラムに保存するために Msgpack を Base64 エンコードする案がありましたが、データサイズが約33%増加し、CPUコストもかかる。
  3. 一貫性: 画像や動画などのバイナリデータも、可能な限り変換なしで「そのまま」扱いたい。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

1. Schema Change: Add BLOB Column

tasks テーブルに、バイナリデータをそのまま格納するための result_data (BLOB) カラムを追加します。

2. Msgpack Everywhere Strategy

データの保存先に関わらず、常に MsgpackSerializer を通過させます。

3. Size Guardrails (Unchanged)

save_blob=False であっても、閾値(デフォルト: 1MB)を超えるデータが渡された場合は、警告ログ (WARNING) を出力して save_blob=True の利用を促します。

影響・結果

1.10 MsgpackとSHA-256を用いた正規化された入力シリアライズ ADR011
KEY

MsgpackとSHA-256を用いた正規化された入力シリアライズ

コンテキストと課題

beautyspot はこれまで、キャッシュキーの生成に json.dumps(sort_keys=True)MD5 を使用していました。 しかし、以下の課題が顕在化していました:

  1. バイナリデータの非効率性: Numpy配列や画像データなどをJSON化する際、テキスト変換(tolist()str())による巨大なオーバーヘッドとメモリ消費が発生する。
  2. ハッシュの衝突リスク: str() に依存したフォールバックでは、巨大なNumpy配列が省略表示(...)された際にハッシュが衝突し、誤ったキャッシュヒットを引き起こす危険性がある。
  3. アルゴリズムの老朽化: MD5 は現代のセキュリティ基準では非推奨とされており、コンプライアンス上の懸念がある。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

キャッシュキー生成ロジックを以下のように刷新します:

  1. Canonicalization (正規化):
    • 独自の正規化関数 canonicalize(obj) を実装する。
    • Dict: キーでソートされた「タプルのリスト [[k, v], ...]」に変換し、順序を固定する。
    • Set: ソートされたリストに変換する。
    • Numpy: numpy をインポートせず、Duck Typing(shape, dtype, tobytes 属性の確認)により検知し、生のバイト列を含むタプルに変換する。これにより完全な一意性を保証する。
  2. Serialization:
    • 正規化されたオブジェクトを msgpack でシリアライズする。
  3. Hashing:
    • ハッシュアルゴリズムを SHA-256 に変更する。

影響・結果

1.11 命令型実行、スマートなデフォルト、およびワークスペース管理 ADR012
CACHE

命令型実行、スマートなデフォルト、およびワークスペース管理

コンテキストと課題

これまで beautyspot は、関数のキャッシュ化にデコレータ (@project.task) を使用する方法のみを提供していました。しかし、実際の利用シナリオにおいて以下の課題が浮き彫りになりました:

  1. サードパーティライブラリの利用: ソースコードを変更できない関数(例: pandas.read_csv)や、外部APIクライアントのメソッドをキャッシュしたい場合、わざわざラッパー関数を定義する必要があり、記述が冗長になる。
  2. アドホックな解析: ノートブック環境や一時的なスクリプトで試行錯誤する際、関数定義の手間がオーバーヘッドとなり、開発体験(DX)を損ねる。
  3. リソース管理の曖昧さ: Project インスタンスが内部で保持する ThreadPoolExecutor が適切にシャットダウンされない場合、プロセスがハングするリスクがある。
  4. ディレクトリの汚染: 生成されるデータベースファイル (.db) やBlobディレクトリ (blobs/) がプロジェクトルート直下に作成され、ディレクトリ構成が煩雑になる。
  5. 設定の重複: 複数のタスクで「常にBlob保存したい」「特定のバージョンを一律適用したい」といった場合、都度引数を指定するのは DRY (Don't Repeat Yourself) 原則に反する。
  6. None 結果のキャッシュ: time.sleep のように戻り値が None である関数をキャッシュしようとすると、キャッシュミスと誤認され再実行されてしまうバグが存在した。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

以下の包括的なアーキテクチャ変更を行います。

1. 命令的実行メソッド (project.run) の導入

デコレータを使用せず、関数オブジェクトとその引数を渡すことで即座にキャッシュ付き実行を行う run メソッドを Project クラスに追加します。

result = project.run(func, arg1, arg2, _save_blob=True)

2. コンテキストマネージャとリソース管理

Project クラスをコンテキストマネージャとして設計し、with ブロック内での利用を推奨パターンとします。これにより、ブロック終了時に確実に shutdown() が呼ばれ、スレッドプール等のリソースが解放されることを保証します。

3. 設定の階層化とスマートデフォルト

Project 初期化時にデフォルト設定を受け入れ、個別の実行時にそれを継承・上書きできる仕組みを導入します。優先順位は「個別指定 > プロジェクトデフォルト > システムデフォルト」とします。

4. ワークスペースディレクトリによる集約

すべての生成アーティファクト(DB、Blob)を、デフォルトで隠しディレクトリ .beautyspot/ 配下に集約します。また、初期化時に .gitignore を自動生成し、キャッシュファイルがバージョン管理に含まれるのを防ぎます。

5. 番兵オブジェクトによるキャッシュ判定

キャッシュヒットの判定において None を特別視せず、専用の番兵オブジェクト (CACHE_MISS) を導入することで、None を返す関数も正しくキャッシュ可能とします。

影響・結果

1.12 ProjectをSpotに、TaskをMarkに名前変更 ADR013
CACHE

ProjectをSpotに、TaskをMarkに名前変更

コンテキストと課題

現在、beautyspot のメインエントリーポイントとして Project クラスが、タスク定義のデコレータとして @project.task が使用されています。しかし、これらの名称には以下の課題があります。

  1. Project の曖昧さ: ユーザーにとって "Project" は、自身のソースコード全体やリポジトリを指す言葉であることが多いです。ライブラリの管理オブジェクトを Project と呼ぶことで、ユーザーのメンタルモデルとの衝突(「プロジェクトの中にプロジェクトがある?」)を招いています。
  2. task の不一致: task は「仕事・課題」を表す名詞ですが、デコレータの役割は「関数を永続化対象として登録・設定する」という動的な作用です。名詞としての命名は、宣言的なデコレータの性質と完全に一致していません。
  3. ブランド・アイデンティティの欠如: 現在の API は汎用的すぎて、このライブラリ特有の「黒子」や「美点(Beauty Spot)」というコンセプトがコード上で表現されていません。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

ライブラリのコアとなる用語を再定義し、以下のリネームを行います。

1. Rename Project class to Spot

管理クラスの名前を Spot に変更します。これにより、「コード上の特定の場所(Spot)を管理する」というニュアンスを持たせ、ライブラリ名 beautyspot との一貫性を持たせます。

2. Rename @task decorator to @mark

デコレータ名を @spot.mark に変更します。これは "Marking a spot"(地点に印を付ける)というイディオムに基づいており、「この関数を管理対象としてマークする」という宣言的な意図を明確にします。

3. Keep run method as spot.run

命令的な実行メソッドである run は、名前を変更せずそのまま維持します。これは、「mark(宣言・静的)」と「run(実行・動的)」という役割分担を明確にするためです。

影響・結果

1.13 遅延バインディングを伴うデコレータベースの型登録 ADR014
SERIAL

遅延バインディングを伴うデコレータベースの型登録

コンテキストと課題

これまでの spot.register_type() は命令的であり、クラス定義と登録ロジックが分離してしまうため、凝集度が低いという課題がありました。 また、Pydantic モデルのようなクラスメソッド(例: cls.model_validate_json)をデコーダとして登録したい場合、デコレータ評価時にはまだクラスが未定義(NameError)であるため、綺麗に記述できないという技術的な制約がありました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

Spot クラスに register デコレータメソッドを追加し、decoder_factory による遅延バインディング を導入します。

Technical Details / 技術詳細

@spot.register(
    code=10,
    encoder=lambda obj: ...,
    decoder_factory=lambda cls: cls.deserialize  # Class is passed here after definition
)
class MyModel:
    ...
  1. register デコレータは、対象クラスの定義完了後(デコレート時)に実行される。
  2. decoder_factory が指定されている場合、生成された cls オブジェクトを引数としてファクトリを実行し、実際の decoder 関数を取得する。
  3. 取得した decoder を用いて、既存の register_type バックエンドに登録する。

影響・結果

1.14 Strict Scoping for Imperative Execution (Runtime Guard) ADR015
CACHE

命令型実行のための厳密なスコープ管理(ランタイムガード)

コンテキストと課題

spot.cached_run() コンテキストマネージャを使用すると、ユーザーは一時的に関数のキャッシュ挙動を適用できます。しかし、Python の with 文のスコープルールにより、ブロック内でバインドされた変数(例: with ... as task:)はブロック終了後もアクセス可能です。

これにより、ユーザーが特定のコンテキスト設定(version="v1" や一時的なストレージ設定など)を持つ関数ラッパーを、意図しない場所で再利用してしまうリスクがあります。これは微妙なバグやリソースリークの原因となります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

ScopedMark コンテキストマネージャに Runtime Guard パターンを実装します。

  1. State Tracking: コンテキストマネージャはアクティブ状態のフラグを保持する。
  2. Wrapper Guard: cached_run から返される関数は、実行前にこのフラグをチェックするガードでラップされる。
  3. Fail Fast: ラップされた関数が with ブロックの外で呼び出された場合、即座に RuntimeError を送出する。

影響・結果

1.15 対話的な削除とクリーンアップポリシー ADR016
MAINT

対話的な削除とクリーンアップポリシー

コンテキストと課題

beautyspot は「試行錯誤の高速化」を掲げていますが、ユーザーが失敗した実験結果(キャッシュ)を即座に破棄する手段が Dashboard 上に存在しませんでした。 また、キャッシュ削除のロジックが cli.py に直接実装されており、Core API (Spot クラス) として提供されていないため、プログラムからの制御やテストが困難な状態でした。

加えて、削除機能を実現するには BlobStorageBase (Storage backend) に物理ファイルを削除するインターフェースが必要ですが、これまでの定義には saveload しか存在しませんでした。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

  1. Core API への delete メソッドの追加 Spot.delete(cache_key) を正式な API として追加します。このメソッドは、「DBレコードの削除」と「Blobファイルの削除」をアトミック(ベストエフォート)に行う責任を持ちます。

  2. Storage Interface への拡張 BlobStorageBase 抽象基底クラスに delete(location) メソッドを追加します。

  3. Breaking Change: ユーザーが独自の Storage Backend を実装している場合、delete メソッドの実装が必要となります。
  4. Idempotency: ファイルが既に存在しない場合でもエラーを送出せず、静かに終了する(冪等な挙動)ことを推奨実装とします。

  5. Dashboard への削除機能の露出 Dashboard に削除ボタンを配置します。誤操作を防ぐため、確認ダイアログを経由する UI とし、Spot.delete を呼び出します。

影響・結果

1.16 ネストされたMsgpackプロトコル ADR017
SERIAL

title: Nested Msgpack Protocol via Serializer Wrapping status: Accepted date: 2026-02-02 context: Improving DX for Custom Types


シリアライザラッパーによるネストされたMsgpackプロトコル

コンテキストと課題

これまでは、カスタム型のエンコーダは ExtType の仕様に合わせて「生のバイト列」を返す必要がありました。 これにより、ユーザーは msgpack ライブラリを直接操作する必要があり、Pydantic モデルなどの扱いが煩雑になっていました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

Serializer Wrapping (Nested Protocol) を採用します。

  1. MsgpackSerializer は、ユーザー定義のエンコーダ/デコーダに対するラッパー(Wrapper)として機能する。
  2. Encode時: エンコーダが返したオブジェクト(中間表現)を、ライブラリが自動的に packb して ExtType に格納する。
  3. Decode時: ExtType のデータを、ライブラリが自動的に unpackb してからデコーダに渡す。
  4. これにより、ユーザーは import msgpack をする必要がなくなり、直感的な実装が可能になる。

影響・結果

1.17 テスト戦略の拡張:高レベル統合テストの導入 ADR018
MAINT

テスト戦略の拡張:高レベル統合テストの導入

コンテキストと課題

現在、beautyspot のテストスイート(tests/)は、主にユニットテストや特定の機能(シリアライザー、リミッターなど)に焦点を当てたテストで構成されている。これらは個々のコンポーネントの正確性を保証する上では有効だが、以下の課題がある:

  1. 連携の検証不足: 複数のタスクが依存関係を持つパイプライン(Load -> Process -> Train)において、バージョニングやキャッシュの伝播が正しく行われるかどうかの検証が不十分である。
  2. 実環境との乖離: 多くの場合、CliRunner やモックオブジェクトを使用しているため、実際のファイルシステムや SQLite データベースに対する副作用(ファイルの生成、削除、ロック競合など)を検証できていない。
  3. ユーザー体験の保証: CLI コマンド(stats, clean など)と Python API による実行結果の整合性を、ユーザーの一連の操作フローとして検証する仕組みがない。

「個々の部品は正しいが、組み合わせると意図した通りに動かない」という統合レベルのバグを防ぐため、テスト戦略を見直す必要がある。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

テストピラミッドの上層として、以下の戦略でインテグレーションテストを導入する:

  1. ディレクトリ構成: tests/integration/ を新設し、ユニットテストとは明確に分離する。
  2. テスト範囲 (Scope):
    • Pipeline E2E: データのロードから学習までの一連のタスクフローを定義し、初回の実行、キャッシュヒット、バージョニングによる部分再計算(依存関係の解決)を検証する。
    • CLI Integration: Python コードで生成された DB ファイルに対し、beautyspot CLI コマンド(stats, list 等)を実行し、出力が正しいことを検証する。
  3. 技術的制約:
    • モックの使用は最小限に留め、原則として実ファイル(SQLite DB, Blobファイル)を使用する(tmp_path フィクスチャを活用)。
    • beautyspot.Spot クラスを実際にインスタンス化し、ユーザーコードと同じインターフェース経由で操作を行う。

Example Scenario / 想定シナリオ

def test_ml_pipeline_lifecycle(spot_env):
    # 1. Pipeline Definition (Load -> Process -> Train)
    # 2. Initial Run (Assert all executed)
    # 3. Cache Hit Run (Assert none executed)
    # 4. Version Upgrade of Middle Task (Assert downstream re-executed)
    # 5. CLI "stats" command check (Assert DB integrity)

影響・結果

1.18 タスクごとのシリアライザの上書き ADR019
SERIAL

タスクごとのシリアライザの上書き

コンテキストと課題

プロジェクト全体としては安全性と互換性の観点から msgpack を標準シリアライザとしています。 しかし、探索的データ分析(EDA)やプロトタイピング、あるいは msgpack でのシリアライズが困難なサードパーティ製オブジェクトを扱う特定のタスクにおいては、Python標準の pickle のような柔軟なシリアライザを局所的に使用したいというニーズがあります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

  1. Override Mechanism: Spot.mark() および Spot.cached_run() に、オプショナル引数 serializer を追加します。
  2. Protocol: ユーザーは dumps(obj) -> bytes および loads(bytes) -> obj メソッドを持つ任意のオブジェクトを渡すことができます。
  3. Fallback Behavior: 指定されたシリアライザでデシリアライズに失敗した場合、ADR-0003 の方針に従い、キャッシュ破損として扱い、タスクを再実行します。

影響・結果

1.19 タスククリーンアップのための寛容な削除ポリシー ADR020
MAINT

タスククリーンアップのための寛容な削除ポリシー

コンテキストと課題

spot.delete(key) 機能は、特定のタスクに関連する「キャッシュレコード(DB)」と「実データ(Blob/File)」の両方を削除することを目的としています。

ローカルファイルシステムやS3などの外部ストレージにおいて、Blobの削除操作は様々な理由(ネットワーク障害、一時的な権限エラー、ファイルが既に手動で削除されている等)で失敗する可能性があります。

このとき、厳密な整合性を求めて「Blob削除に失敗したらDBレコードの削除もロールバック(中断)する」という実装にすると、以下の問題が発生します:

  1. ゾンビレコード: 物理ファイルが見つからないだけなのに、DBからレコードを消せず、ユーザーは永遠にそのタスクを「無効化」できない。
  2. 再計算の阻害: 破損したキャッシュエントリが残り続けることで、新しい計算結果での上書きや、クリーンな状態からの再実行が妨げられる。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

削除操作において 「メタデータの削除を優先する」 ポリシーを採用します。

  1. まず、Blob(実データ)の削除を試みる。
  2. Blobの削除中に例外が発生した場合、処理を中断せずWARNING レベルのログを出力してエラーを捕捉する。
  3. Blob削除の成否に関わらず、DB上のタスクレコードの削除を必ず実行する。

影響・結果

1.20 コードの可視化と品質メトリクス戦略 ADR021
MAINT

コードの可視化と品質メトリクス戦略

コンテキストと課題

プロジェクトの規模拡大に伴い、以下の課題が発生しています:

  1. 構造の把握困難: モジュール間の依存関係が複雑化し、全体像を掴みにくい。
  2. 品質の定量化: コードの複雑さが主観で語られており、客観的なリファクタリング基準がない。
  3. アーキテクチャの健全性: 「安定依存の原則(SDP)」が守られているか(不安定なモジュールが安定したモジュールに依存していないか)を確認する手段がない。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

以下のツールを導入し、開発フローに統合します。

  1. Radon:
    • 循環的複雑度 (Cyclomatic Complexity) と保守性指数 (Maintainability Index) を計測する。
  2. Pydeps:
    • モジュール間のインポート依存関係を可視化するグラフ(SVG)を生成する。
  3. Custom Stability Analyzer:
    • 不安定度 ($I$) を算出する独自スクリプトを tools/ に配置する。
    • Graphviz を用いて、安定度に基づいた色分け(青=安定、赤=不安定)を行ったアーキテクチャ図を生成する。

影響・結果

1.21 シングルディスパッチによる複雑なモジュールのリファクタリング ADR022
KEY

シングルディスパッチによる複雑なモジュールのリファクタリング

コンテキストと課題

quality_report により、src/beautyspot/cachekey.pycanonicalize 関数が非常に高い循環的複雑度(ランク D)を持っていることが判明しました。この関数は、ハッシュ化のためのオブジェクト正規化において、dict, list, set, numpy, type など多岐にわたる型をチェックするために長い if-elif-else チェーンを使用していました。

この構造は開放閉鎖の原則 (Open-Closed Principle) に違反しており、新しい型のサポートを追加するたびにコア関数を修正する必要があるため、退行(デグレード)のリスクを高めていました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

canonicalize 関数を functools.singledispatch を用いて刷新します。

影響・結果

1.22 メンテナンス操作のためのサービスレイヤー ADR023
MAINT

メンテナンス操作のためのサービスレイヤー

コンテキストと課題

現在、src/beautyspot/cli.py には以下のビジネスロジックが直接記述されています: 1. Pruning: タイムスタンプに基づく古いタスクの削除。 2. Cleaning: 孤立した Blob ファイルの特定と削除(ガベージコレクション)。

これらのロジックは sqlite3 ドライバに直接アクセスしたり、ファイルパスを操作したりしており、TaskDBBlobStorageBase の抽象化をバイパスしています。これにより、CLI が SQLite やローカルファイルストレージの詳細に密結合し、他のインターフェース(スクリプトや Web UI)から同じロジックを再利用できないという問題が発生しています。また、S3 などの外部ストレージに対する「掃除」機能の実装も困難になっています。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

メンテナンスロジックを cli.py から抽出し、専用の MaintenanceService (src/beautyspot/maintenance.py) に移動します。

Spot クラスはアプリケーションの実行コンテキストとキャッシュ制御に集中させ、メンテナンス(行政的タスク)は別の関心事として分離します。

影響・結果

1.23 ノンブロッキングなキャッシュの永続化とタスク追跡 ADR024
BGIO

ノンブロッキングなキャッシュの永続化とタスク追跡

コンテキストと課題

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

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

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

影響・結果

1.24 プロトコルベースのシリアライザ登録 ADR025
SERIAL

プロトコルベースのシリアライザ登録

コンテキストと課題

beautyspot では、@spot.register デコレータを使用してカスタム型を登録できます。 当初の実装では、Spot クラス内でシリアライザが MsgpackSerializer のインスタンスであるかどうかを明示的にチェックしていました。

if isinstance(self.serializer, MsgpackSerializer):
    self.serializer.register(...)

これは、高レベルの Spot クラスを特定の具象クラス (MsgpackSerializer) に結合させており、依存関係逆転の原則 (Dependency Inversion Principle) に違反していました。その結果、カスタム型登録をサポートする他のシリアライザ(将来的な JsonSerializer やカスタムラッパーなど)を使用することが不可能でした。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

beautyspot.serializerTypeRegistryProtocol を導入します。

  1. Define Protocol: 型登録に必要な register メソッドのシグネチャを規定するプロトコルを定義します。
  2. Decoupling: Spot クラスは、具象クラスではなくこのプロトコルに対して適合性チェックを行うように変更します。
  3. Interface Segregation: 基本的な SerializerProtocol (dump/load) と TypeRegistryProtocol を分離し、型登録をサポートしないシリアライザも許容できるようにします。

影響・結果

1.25 デフォルトの依存性注入のためのファクトリ関数 ADR026
DI

デフォルトの依存性注入のためのファクトリ関数

コンテキストと課題

v2.0 以降、beautyspot は依存性注入 (DI) アーキテクチャを採用しました。Spot クラス(core.py)の初期化には、TaskDB, Serializer, Storage の各インスタンスを明示的に渡す必要があります。

これはテスト容易性と柔軟性の観点では優れていますが、単に「すぐに使い始めたい」だけのユーザーにとっては、初期化が非常に冗長になってしまうという課題がありました。

# 一般的なスクリプトには冗長すぎる
db = SQLiteTaskDB(...)
storage = LocalStorage(...)
serializer = MsgpackSerializer()
spot = Spot(name="app", db=db, storage=storage, serializer=serializer)

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

beautyspot/__init__.py において、具象クラスのインスタンス化を伴うファクトリ関数 Spot を公開します。

# beautyspot/__init__.py
def Spot(name: str, db=None, ...):
    resolved_db = db or SQLiteTaskDB(...)
    # ... 他の解決ロジック ...
    return _Spot(name, db=resolved_db, ...)

これにより、ライブラリのトップレベルからインポートされる Spot は関数となり、内部で core.py_Spot クラスを組み立てて返します。

影響・結果

1.26 実践的なCLIリファクタリングポリシー ADR027
CLI

実践的なCLIリファクタリングポリシー

コンテキストと課題

quality_report.md において、src/beautyspot/cli.py 内の複数の関数(show_cmd, prune_cmd, stats_cmd)が循環的複雑度(CC)で Rank C の警告を受けています。一般的にプロジェクトでは CC の低減を推奨していますが、CLI モジュールに関してはその優先度と費用対効果を再検討する必要があります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

CLI モジュールの Rank C スコアについては、以下の理由から「即時の抜本的なリファクタリングは行わず、現状の構造を維持する」ことを決定しました。

影響・結果

1.27 CLIのスコープ定義と明示的なストレージリンク ADR028
CLI

CLIのスコープ定義と明示的なストレージリンク

コンテキストと課題

beautyspotclean コマンドやガベージコレクション機能は、DBファイルとBlobストレージディレクトリの対応関係が「自明」であることを前提としています。標準構成(.beautyspot/ 配下)ではこの前提は成立しますが、ユーザーがカスタムパスを設定したり、外部バックエンド(S3等)を使用した場合、CLIツールは安全に依存関係を特定できません。この状態で推測に基づく削除を行うと、誤って無関係なデータを削除するリスクがあります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

1. CLI Scope Limitation (Policy)

CLIが提供する「ストレージの自動クリーニング・削除機能」のサポート範囲を、標準ディレクトリ構成(.beautyspot/ 配下)を使用しているプロジェクト に限定します。カスタム構成を利用している場合は、CLI による自動削除は保証されず、ユーザー自身の責任で管理を行う必要がある旨を明記します。

2. Explicit Storage Linkage (Roadmap)

「対応関係が自明でない」問題を根本解決するため、将来のバージョンで TaskDB内に使用しているStorageの情報をメタデータとして記録する 仕組みを導入します。DB初期化時に storage_uri を保存し、メンテナンス時に「操作対象のDBが参照しているストレージと、今操作しようとしている実体が一致するか」を検証可能にします。

影響・結果

1.28 再帰的なストレージクリーンアップとゾンビプロジェクトの回収 ADR029
MAINT

再帰的なストレージクリーンアップとゾンビプロジェクトの回収

コンテキストと課題

これまでの beautyspot clean コマンドの実装には、以下の課題がありました。

  1. ディレクトリの残留: clean コマンドは個別のファイル削除のみを行い、空になったディレクトリを削除していませんでした。
  2. 隠しファイルの阻害: macOS の .DS_Store などのシステムファイルが存在する場合、ディレクトリが空とみなされず、削除できないケースがありました。
  3. ゾンビプロジェクト: ユーザーが手動で DB ファイル (.db) のみを削除した場合、対応する Blob ディレクトリが管理外のゴミ(ゾンビプロジェクト)として残り続け、削除する手段がありませんでした。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

ストレージのクリーンアップ戦略を以下のように刷新します。

1. Two-Phase Cleanup Strategy (clean command)

clean コマンドを2段階に分割します。 * Phase 1 (File Deletion): DB参照のない孤立ファイルを削除。 * Phase 2 (Directory Pruning): LocalStorageprune_empty_dirs() メソッドを追加し、再帰的に空ディレクトリを一括削除。

2. Robust Directory Pruning

システム生成ファイル(.DS_Store, Thumbs.db, desktop.ini)のみが存在する場合、それらを「実質的な空」とみなし、強制的に削除した上で親ディレクトリを削除します。

3. Zombie Project Garbage Collection (gc command)

DBファイルが失われたプロジェクトを回収するための gc コマンドを実装します。対応する .db ファイルが存在しないディレクトリを「ゾンビプロジェクト」と判定し、shutil.rmtree で強制的に一括削除します。

影響・結果

1.29 宣言的ストレージポリシー ADR030
STORE

宣言的ストレージポリシー

コンテキストと課題

これまでの beautyspot では、関数の実行結果を Blob ストレージ(ファイル)に保存するか、DB のレコードに直接埋め込むかは、ユーザーが save_blob=True フラグで明示的に指定する必要がありました。 また、データサイズが大きい場合に警告を出す機能はありましたが、自動的に対処する機能はありませんでした。これにより、ユーザーはデータのサイズを予測してフラグを管理するという負担を強いられていました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

ストレージ保存方式の決定ロジックを抽象化した StoragePolicy プロトコルを導入します。

  1. StoragePolicy Interface:

    • should_save_as_blob(data: bytes) -> bool を持つプロトコルを定義します。
    • 配置場所は src/beautyspot/storage.py とし、ストレージ関連の責務を凝集させます。
  2. Standard Implementations:

    • ThresholdStoragePolicy: 指定したバイト数を超えた場合に Blob 保存を選択する(推奨デフォルト)。
    • WarningOnlyPolicy: 従来動作互換。Blob 化はせず、閾値超えでログ警告のみ行う。
  3. Precedence (優先順位):

    • 優先度1: 関数ごとの明示的指定 (@mark(save_blob=...))。
    • 優先度2: ポリシーによる自動判定。

影響・結果

1.30 リミッターの依存性の注入 ADR031
LIMIT

リミッターの依存性の注入

コンテキストと課題

以前、core.pySpot クラスは TokenBucket 実装と密結合していました。Spottpm (tokens per minute) 整数引数を受け取り、内部で TokenBucket をインスタンス化していました。

この設計にはいくつかの制限がありました: 1. Testing: ユニットテストが TokenBucket 内の本物の time.sleep 呼び出しに依存するため、実行が遅くなっていました。 2. Extensibility: ユーザーがカスタムレートリミッター(例:Redisベースの分散リミッター)や異なるアルゴリズムを提供できませんでした。 3. Separation of Concerns: core.Spot がリミッターのライフサイクルと設定を管理しており、単一責任の原則に違反していました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

レートリミッターを core.Spot から切り離し、依存性注入 (DI) を使用します。

  1. Protocol Definition: beautyspot.limiterconsume(cost: int)consume_async(cost: int) メソッドを規定する LimiterProtocol を定義します。
  2. Explicit Inheritance: デフォルトの TokenBucket 実装は、型安全性と明確さのために LimiterProtocol を明示的に継承します。
  3. Injection: core.Spot.__init__ を、tpm の代わりに limiter: LimiterProtocol インスタンスを受け取るように変更します。
  4. Factory Responsibility: __init__.pySpot ファクトリ関数が、カスタムリミッターが提供されない場合のデフォルトの TokenBucket 作成を担当します。

影響・結果

1.31 宣言的ライフサイクルポリシー ADR032
LIFE

宣言的ライフサイクルポリシー

コンテキストと課題

機械学習や生成AIの実験プロセスにおいて、生成されるデータの重要度は均一ではありません。数ヶ月保持すべき「最終モデル」もあれば、数時間で不要になる「一時的なデバッグ出力」もあります。

現在、これらの古いデータを削除するには、ユーザーが MaintenanceService.prune(older_than=...) を呼び出すスクリプトを自作し、定期実行する必要があります。これはユーザーにとって負担であり、設定を忘れるとディスク容量を圧迫する原因となります。また、beautyspot は常駐プロセスを持たないため、クリーンアップのトリガー設計が課題となります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

ユーザーが「データの寿命(What)」を宣言するだけで済むよう、以下の仕組みを導入します。

1. retention Parameter for @mark

デコレータおよび run メソッドに retention 引数を追加します(例: "7d", "1h", None)。

2. Database Schema Change (expires_at)

タスク作成時に寿命を計算し、tasks テーブルの expires_at カラムに保存します。

3. Lazy Expiration (Access-time Check)

キャッシュ取得時(spot.db.get)に、expires_at < current_time であれば「キャッシュミス」とみなして None を返します。この時点では物理削除は行わず、レイテンシへの影響を最小限にします。

4. Explicit Garbage Collection (CLI)

期限切れデータの物理削除は、CLI コマンド $ beautyspot gc --expired によって一括で行います。

影響・結果

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

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

コンテキストと課題

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

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

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

影響・結果

1.33 cached_run スコープ制限の廃止 ADR034
CACHE

cached_run スコープ制限の廃止

コンテキストと課題

ADR-0014 では、cached_run が返すラッパー関数を with ブロック外から呼び出した場合に RuntimeError を送出する Runtime Guard パターンを導入しました。しかし、ADR-0023 で Spot インスタンス自体の再利用性(with spot: はフラッシュのみを目的とし、インスタンスの無効化ではない)が確立されたことにより、cached_run のラッパーについても同様にスコープ外での利用を妨げない方針が自然な帰結となりました。

また、Runtime Guard の実装において ContextVar を使用していましたが、呼び出しごとに新規の ContextVar を作成する構造になっており、コンテキスト分離の恩恵が得られていませんでした。さらに、asyncio の一部のパターンにおいてガードが機能しないバグも内包していました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

cached_run の Runtime Guard を廃止し、返されたラッパー関数は with ブロック外でも呼び出し可能とします。

  1. 簡略化: ContextVar, is_active, make_scoped_guard などの関連ロジックを全て削除します。
  2. 実装の純粋化: cached_run は単に self.mark() を対象関数に適用して返すだけの、直感的な実装に変更します。

影響・結果

1.34 クラスベース・フックシステムと並列実行サポート ADR035
HOOK

クラスベース・フックシステムと並列実行サポート

コンテキストと課題

beautyspot のコアロジックを汚染することなく、ユーザーが「トークン計算」や「レイテンシ計測」などのカスタムメトリクスを収集できる仕組みが必要です。また、並列実行環境においても、競合状態を避けつつ安全に状態を共有・更新できることが求められます。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

1. クラスベースのフックインターフェース (HookBase)

状態(開始時間や累計カウンタ)を保持しやすいよう、クラスベースのインターフェースを採用します。

2. 並列実行のための自動ロック機構 (ThreadSafeHookBase)

サブクラスでオーバーライドされたメソッドを自動でロックラッパーで包む仕組みを導入します。これにより、ユーザーは明示的なロック制御なしでスレッドセーフな集計が可能になります。

3. 型安全なコンテキストオブジェクトの分離

実行フェーズごとに最適化された 3つの専用コンテキストクラス を提供します。 * PreExecuteContext: 関数実行前。 * CacheHitContext: キャッシュ取得成功時。 * CacheMissContext: 関数実行完了時。

4. ゼロオーバーヘッドの原則

フックが登録されていない場合は、コンテキストオブジェクトの生成を完全にスキップします。

影響・結果

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

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

コンテキストと課題

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

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

影響・結果

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

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

コンテキストと課題

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

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

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

影響・結果

1.37 ストレージメンテナンスのための確率的自動エビクション ADR038
LIFE

ストレージメンテナンスのための確率的自動エビクション

コンテキストと課題

beautyspot はキャッシュの TTL をサポートしており、期限切れデータは論理的に無効化されます。しかし、ユーザーが明示的にクリーンアップを呼び出さない限り、有効期限切れのメタデータや巨大な孤立 Blob ファイルは物理的に削除されず、ストレージが肥大化し続けるという問題がありました。

ユーザーにインフラ管理を意識させない「黒子」としての哲学を維持しつつ、自動でガベージコレクション(エビクション)を行う仕組みが必要になりました。

判断基準

検討した選択肢

  1. Capacity-based LRU (Least Recently Used) Policy:
    • ストレージ容量やレコード数の上限に達した際に古いデータを削除する。
    • 評価: 却下。キャッシュヒットのたびにタイムスタンプを更新する必要があり、DB の書き込み競合やレイテンシ低下を招く。
  2. Load-Adaptive Background Eviction:
    • アイドル状態のときにのみクリーンアップタスクを投入する。
    • 評価: 却下。実装が複雑になりすぎる(オーバーエンジニアリング)。
  3. Probabilistic Auto-Eviction (確率的自動実行):
    • 新しい結果の保存直後、設定された確率に基づいてバックグラウンドでクリーンアップタスクを投入する。
    • 評価: 採用。 シンプルかつ低コスト。

決定事項

採用した選択肢: Option 3.

確率的自動エビクションを採用します。

影響・結果

1.38 シリアライザのサブクラス解決向け境界付きLRUキャッシュ ADR039
SERIAL

シリアライザのサブクラス解決向け境界付きLRUキャッシュ

コンテキストと課題

MsgpackSerializer_default_packer において、クラスの解決結果を保存するキャッシュ辞書が、動的に型が生成される環境(動的な Pydantic モデル等)で無制限に肥大化し、メモリリークを引き起こす懸念がありました。 また、この辞書へのアクセスは非常に高頻度であるため、厳密なスレッドセーフティ(ロック)を導入すると、シリアライズ性能のボトルネック(ロック競合)を招く恐れがありました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

標準ライブラリの collections.OrderedDict を利用し、上限サイズ(デフォルト 1024)を持つ LRU (Least Recently Used) キャッシュ を導入します。マルチスレッド環境下でのアクセスに対しては、意図的にロックフリーなアプローチを維持します。

影響・結果

1.39 バックグラウンドループのシャットダウン戦略とスレッディングモデル ADR040
BGIO

バックグラウンドループのシャットダウン戦略とスレッディングモデル

コンテキストと課題

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

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

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

影響・結果

1.40 破棄された一時ファイルのガベージコレクション戦略 ADR041
MAINT

破棄された一時ファイルのガベージコレクション戦略

コンテキストと課題

Blob データの保存時、アトミックな書き込みを実現するために一時ファイルを作成し、完了後にリネームする設計を採用しています。しかし、アンチウイルスソフトやバックアッププロセスの介入によりリネームが失敗するエッジケースが存在します。この際、一時ファイルの削除もロックにより失敗すると、一時ファイルがストレージ上に永久に残留し続ける(ストレージリーク)という問題がありました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

アトミック書き込みのフェイルセーフとして、以下の機構を導入します。

  1. 専用サフィックスの導入: 一時ファイルの生成時に .spot_tmp という専用のサフィックスを付与し、追跡を容易にします。
  2. 遅延ガベージコレクション (GC): MaintenanceService.clean_garbage に、一時ファイルの削除処理を統合します。
  3. Grace Period (猶予期間) の設定: 更新時刻が指定時間(デフォルト24時間)を経過しているもののみを削除対象とし、並行プロセスへの影響を回避します。

影響・結果

1.41 シリアライザ向けスレッドセーフなLRUキャッシュ ADR042
SERIAL

シリアライザ向けスレッドセーフなLRUキャッシュ

コンテキストと課題

MsgpackSerializer は動的型生成時のメモリリークを防ぐために、内部で OrderedDict を用いた LRU キャッシュを運用しています。しかし、バックグラウンド保存や Web フレームワークでの利用など、マルチスレッド環境からの並行アクセスが日常的に発生します。ロックを持たない OrderedDict への並行操作は、RuntimeError やデータの破損を引き起こす致命的なバグの温床となっていました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

MsgpackSerializer の内部状態(キャッシュおよびレジストリ)に対するすべての読み書き操作を threading.Lock() で保護します。

  1. 安定性の優先: 確実な排他制御により、マルチスレッド下での信頼性を担保します。
  2. 限定的なロック範囲: ロックの範囲を「型の解決とキャッシュの更新」というメモリアクセス領域に限定し、重いシリアライズ処理(I/O や計算)はロック外で実行します。

影響・結果

1.42 ライターキューによるSQLite書き込みの直列化 ADR043
DB

ライターキューによるSQLite書き込みの直列化

コンテキストと課題

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 4.

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

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

Implementation Sketch / 実装概要

影響・結果

Notes / 補足

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

1.43 LocalStorageにおける絶対パスサポートの廃止 ADR044
STORE

LocalStorageにおける絶対パスサポートの廃止

コンテキストと課題

v1.x 系の LocalStorage では、ファイルパス(location)として絶対パスを許容・解決する挙動が含まれていました。v2.0 の開発において、パストラバーサル脆弱性を防ぐために base_dir に対する厳密なセキュリティチェックを導入しましたが、これが旧来の絶対パスの挙動と競合し、意図せぬクラッシュを引き起こす可能性がありました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

LocalStorage.load() および関連するファイルアクセスにおいて、絶対パスによる後方互換性の維持を公式に 放棄 します。

すべての location パラメータは base_dir に対する相対パスとしてのみ解釈されます。絶対パスの形式であっても、それが base_dir のサブディレクトリ内に解決されない限り、セキュリティチェックにより ValueError として処理されます。

影響・結果

1.44 明示的なDBキューフラッシュの導入 ADR045
DB

明示的なDBキューフラッシュの導入

コンテキストと課題

SQLiteTaskDB は、ライタースレッドとキューを用いた非同期書き込みを行っています。しかし、メインプロセスの終了時や Spot.flush() 実行時に、ストレージへの I/O は待機するものの、DB への書き込みキューのフラッシュ(全タスクの完了)を明示的に待機する仕組みがありませんでした。これにより、短命なスクリプト等で DB へのデータ書き込みが完了する前にプロセスが終了し、メタデータが消失するリスクがありました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

TaskDBBaseflush(timeout) インターフェースを追加し、SQLiteTaskDB で実装します。

SQLiteTaskDB の実装では、「何もしない (No-op) 書き込みタスク」をエンキューし、そのタスクの完了イベントを待機するというアプローチを採用します。この flush メソッドを Spot.flush() から呼び出すことで、ストレージと DB の両方の完了を確実に待機させます。

影響・結果

1.45 ワークスペース初期化のストレージバックエンドへの委譲 ADR046
STORE

ワークスペース初期化のストレージバックエンドへの委譲

コンテキストと課題

これまで beautyspot では、 Factory 関数 Spot() の初期化時において、デフォルトのキャッシュディレクトリ(.beautyspot/)の作成と .gitignore の配置を一律で行っていました。しかし、ユーザーがカスタムのパスを指定した場合でも、意図せずカレントディレクトリに .beautyspot/ が作成されてしまうという課題がありました。また、コンポーネント(DB、ストレージ)が自身の永続化先の詳細を自己管理できておらず、関心の分離の観点で不完全な設計となっていました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

__init__.py および core.py からワークスペース初期化ロジック(_setup_workspace)を完全に削除します。代わりに、ローカルファイルシステムに依存する各バックエンドコンポーネント(LocalStorage および SQLiteTaskDB)の初期化処理内で、自身が使用するディレクトリの作成と .gitignore の配置を行うように責務を委譲します。

影響・結果

1.46 ガベージコレクション時のバックグラウンドループシャットダウン戦略 ADR047
BGIO

ガベージコレクション時のバックグラウンドループシャットダウン戦略

コンテキストと課題

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

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

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

影響・結果

1.47 SQLite ライタースレッドの死活監視におけるポーリング間隔の維持 ADR048
DB

SQLite ライタースレッドの死活監視におけるポーリング間隔の維持

コンテキストと課題

SQLiteTaskDB は、専用のライタースレッドを用いて書き込みを直列化しています。メインスレッドが書き込み完了を待機する際(または flush 実行時)、ライタースレッドが予期せず死亡(セグメンテーションフォールトや深刻なエラー等)した場合に、メインスレッドが永久にハングアップするのを防ぐ必要があります。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 3.

0.5 秒というポーリング間隔を維持し、ループ内で task.event.wait(timeout=0.5) とスレッドの生存確認を行う設計を継続します。

  1. 安全装置: ライタースレッドが異常終了しても、最大 0.5 秒以内に異常を検知してメインスレッドを解放できます。
  2. 正常系パフォーマンス: threading.Event.wait はイベントがセットされた瞬間に即座に復帰するため、正常な書き込み時には 0.5 秒という数値はレイテンシに一切影響しません。
  3. リソース効率: 間隔を適切に保つことで、不要なコンテキストスイッチや CPU 浪費を防ぎます。

影響・結果

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

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

コンテキストと課題

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

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

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

影響・結果

1.49 データベースのライフサイクル管理の呼び出し元への委譲 ADR050
DB

データベースのライフサイクル管理の呼び出し元への委譲

コンテキストと課題

Spot クラスは TaskDBBase インスタンスを DI で受け取っているにもかかわらず、シャットダウン処理内で強制的に db.shutdown() を呼び出していました。これにより、複数の Spot インスタンスで 1 つの DB を共有する場合、一方の Spot が破棄されると他の Spot も DB にアクセスできなくなるという重大なバグが生じていました。これは DI の導入と、GC 時の強制破棄戦略が複雑に絡み合った結果の技術的負債でした。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

Spot クラス内部から db.shutdown() の呼び出しを完全に削除します。リソース(DB インスタンス)を生成して注入した側が、そのライフサイクルの全責任を負うという原則を厳格に適用します。

影響・結果

1.50 スレッドセーフなSQLite接続のクローズとロック管理 ADR051
DB

スレッドセーフなSQLite接続のクローズとロック管理

コンテキストと課題

beautyspotSQLiteTaskDB は、並行読み取り性能を高めるため、各スレッドごとに専用のコネクションを保持する設計となっています。シャットダウン時には WAL のチェックポイントを妨げないよう、これらの接続を一括で閉じようとしていましたが、以下の深刻な問題がありました。 1. check_same_thread の制約: 別スレッドから close() を呼ぶとエラーが発生する。 2. クラッシュの危険性: クエリ実行中に強制クローズされるとセグメンテーションフォールトを引き起こす。 3. リカバリ時のデッドロック: エラー発生時の再接続処理で、再帰的なロック取得によりハングする。 4. GC 時のブロック: ロック解放を待機する設計では、GC 時にメインスレッドがフリーズし、ADR-0045 の「GC 時は絶対にメインスレッドをブロックさせない」という原則に違反する。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

相反する要件を解決するため、以下の機構を導入します。

  1. リエントラントロック (threading.RLock) の導入: 同一スレッド内でのリカバリ処理によるデッドロックを防ぎます。
  2. 安全なクローズの許可: check_same_thread=False を指定し、別スレッドからのクローズを許可します。
  3. ノンブロッキング・クローズ機構 (Fail-fast Close):
  4. シャットダウンや GC 時の close() 呼び出しは、ロック取得を ノンブロッキング (blocking=False) で試行します。
  5. 他のスレッドがクエリを実行中でロックを取得できなかった場合、安全を最優先してクローズを諦め、Python の自然な GC 管理に委ねます。

影響・結果

1.51 SQLite書き込みタスクのキャンセル戦略 ADR052
DB

SQLite書き込みタスクのキャンセル戦略

コンテキストと課題

db.py (SQLiteTaskDB) では、バックグラウンドでの書き込みを直列化するために専用の Writer スレッドとキューを使用しています。シャットダウン時 (shutdown(wait=True)) には、キューに積まれた全てのタスクの完了を待機しますが、タイムアウトが発生した場合の振る舞いが問題となります。特に、現在実行中 (RUNNING) の書き込みタスクを強制的にキャンセルすべきかどうかの設計判断が求められました。

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

実行中の書き込みタスク (RUNNING) は、タイムアウト時間を超過した場合でも強制的にキャンセルしない設計としました。

Rationale / 理由

SQLite のトランザクション実行中にスレッドを強制終了させたり、非同期的に接続を閉じたりすると、データベースファイルが破損する(Corruption)、あるいはWALファイルが不適切な状態になるリスクがあります。

キューの処理ロジックにおいて、すでにキューから取り出され実行状態に入ったタスクは「不可分なアトミック操作」と見なします。シャットダウンのタイムアウトは「新たなタスクの取り出し」を停止するためには機能しますが、実行中のI/O処理を途中で引き裂くことはしません。これにより、システムの安全性とデータ一貫性を最優先しています。

影響・結果

1.52 バックグラウンドループとエグゼキュータのシャットダウン順序 ADR053
BGIO

バックグラウンドループとエグゼキュータのシャットダウン順序

コンテキストと課題

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

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

判断基準

検討した選択肢

決定事項

採用した選択肢: Option 2.

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

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

影響・結果

1.53 SQLiteTaskDB のスキーマ初期化を RAII パターンに移行 ADR054
DB

コンテキスト(背景)

core.Spot.__init__self.cache.db.init_schema() を呼び出しており、以下の問題があった:

  1. Law of Demeter 違反: Spotcache を経由して db の内部メソッドを呼び出す2段階アクセス
  2. 二重管理: maintenance.pyfrom_path() でも防御的に db.init_schema() を呼び出しており、呼び忘れ対策コードが増殖していた
  3. 構築後に未完成なオブジェクト: SQLiteTaskDB のインスタンスは init_schema() を誰かが呼ぶまで使用不可能な状態だった

外部から受けたリファクタリング提案(ファクトリへの移動、Lazy初期化、オプション引数等)を検討した結果、根本原因は SQLiteTaskDB が構築後にすぐ使えない設計にあると判断した。

決定

SQLiteTaskDB.__init__ の末尾で self.init_schema() を呼び出し、RAII パターンに従いコンストラクタ完了時点でインスタンスが即座に使用可能な状態とする。

これに伴い、以下の明示的な init_schema() 呼び出しを削除した: - core.Spot.__init__self.cache.db.init_schema() - maintenance.pydb.init_schema() + try/except による graceful degradation

init_schema() メソッド自体はプロトコル (TaskDBCore) に残し、冪等性を保証する。カスタム DB 実装が独自のタイミングで呼び出すことは引き続き可能。

結果(トレードオフ)

棄却された代替案